feat: a lot

This commit is contained in:
Andre Henriques 2025-01-21 11:16:32 +00:00
parent 26301d1a13
commit 941875ff21
34 changed files with 956 additions and 1205 deletions

View File

@ -1,8 +1,6 @@
package com.andr3h3nriqu3s.applications package com.andr3h3nriqu3s.applications
import java.sql.ResultSet import java.sql.ResultSet
import java.text.SimpleDateFormat
import java.util.Date
import java.util.UUID import java.util.UUID
import kotlin.collections.emptyList import kotlin.collections.emptyList
import kotlin.collections.setOf import kotlin.collections.setOf
@ -23,27 +21,35 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
data class ApplicationUrl(
var application_id: String,
var url: String,
) {
companion object : RowMapper<ApplicationUrl> {
override public fun mapRow(rs: ResultSet, rowNum: Int): ApplicationUrl {
return ApplicationUrl(
rs.getString("application_id"),
rs.getString("url"),
)
}
}
}
data class Application( data class Application(
var id: String, var id: String,
var url: String, var url: String,
var original_url: String?,
var unique_url: String?,
var title: String, var title: String,
var user_id: String, var user_id: String,
var extra_data: String, var extra_data: String,
var payrange: String, var payrange: String,
var status: Int,
var status_id: String?, var status_id: String?,
var company: String, var company: String,
var recruiter: String, var recruiter: String,
var agency: Boolean, var agency: Boolean,
var message: String, var message: String,
var linked_application: String,
var status_history: String,
var application_time: String,
var create_time: String, var create_time: String,
var simple_url: String,
var flairs: List<Flair>, var flairs: List<Flair>,
var views: List<View>,
var events: List<Event>, var events: List<Event>,
) { ) {
companion object : RowMapper<Application> { companion object : RowMapper<Application> {
@ -51,23 +57,17 @@ data class Application(
return Application( return Application(
rs.getString("id"), rs.getString("id"),
rs.getString("url"), rs.getString("url"),
rs.getString("original_url"),
rs.getString("unique_url"),
rs.getString("title"), rs.getString("title"),
rs.getString("user_id"), rs.getString("user_id"),
rs.getString("extra_data"), rs.getString("extra_data"),
rs.getString("payrange"), rs.getString("payrange"),
rs.getInt("status"),
rs.getString("status_id"), rs.getString("status_id"),
rs.getString("company"), rs.getString("company"),
rs.getString("recruiter"), rs.getString("recruiter"),
rs.getBoolean("agency"), rs.getBoolean("agency"),
rs.getString("message"), rs.getString("message"),
rs.getString("linked_application"),
rs.getString("status_history"),
rs.getString("application_time"),
rs.getString("create_time"), rs.getString("create_time"),
emptyList(), rs.getString("simple_url"),
emptyList(), emptyList(),
emptyList(), emptyList(),
) )
@ -77,9 +77,7 @@ data class Application(
data class SubmitRequest(val text: String) data class SubmitRequest(val text: String)
data class ListRequest(val status: Int? = null, val views: Boolean? = null) data class StatusRequest(val id: String, val status_id: String?)
data class StatusRequest(val id: String, val status: Int)
data class FlairRequest(val id: String, val text: String) data class FlairRequest(val id: String, val text: String)
@ -100,7 +98,6 @@ class ApplicationsController(
val sessionService: SessionService, val sessionService: SessionService,
val applicationService: ApplicationService, val applicationService: ApplicationService,
val flairService: FlairService, val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService, val eventService: EventService,
) { ) {
@ -118,7 +115,13 @@ class ApplicationsController(
val flairs = application.flairs.map { it.toFlairSimple() } val flairs = application.flairs.map { it.toFlairSimple() }
return CVData(application.company, application.recruiter, application.message, application.agency, flairs) return CVData(
application.company,
application.recruiter,
application.message,
application.agency,
flairs
)
} }
/** Create a new application from the link */ /** Create a new application from the link */
@ -133,13 +136,10 @@ class ApplicationsController(
Application( Application(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
submit.text, submit.text,
submit.text,
submit.text,
"New Application", "New Application",
user.id, user.id,
"", "",
"", "",
0,
null, null,
"", "",
"", "",
@ -147,9 +147,6 @@ class ApplicationsController(
"", "",
"", "",
"", "",
"",
"",
emptyList(),
emptyList(), emptyList(),
emptyList(), emptyList(),
) )
@ -199,7 +196,7 @@ class ApplicationsController(
public fun submitText( public fun submitText(
@RequestBody submit: SubmitRequest, @RequestBody submit: SubmitRequest,
@RequestHeader("token") token: String @RequestHeader("token") token: String
): Int { ): List<Application> {
val user = sessionService.verifyTokenThrow(token) val user = sessionService.verifyTokenThrow(token)
var text = submit.text.replace("=\n", "") var text = submit.text.replace("=\n", "")
@ -263,23 +260,17 @@ class ApplicationsController(
Application( Application(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
if (elm.contains("linkedin")) elm.split("?")[0] else elm, if (elm.contains("linkedin")) elm.split("?")[0] else elm,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
if (elm.contains("linkedin")) elm.split("?")[0] else null,
"New Application", "New Application",
user.id, user.id,
"", "",
"", "",
0,
null, null,
"", "",
"", "",
false, false,
"", "",
"", "",
"", if (elm.contains("linkedin")) elm.split("?")[0] else "",
"",
"",
emptyList(),
emptyList(), emptyList(),
emptyList(), emptyList(),
) )
@ -292,26 +283,46 @@ class ApplicationsController(
print(applications.size) print(applications.size)
print(" links\n") print(" links\n")
return applications.size return applications
} }
@PostMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun list( public fun list(@RequestHeader("token") token: String): List<Application> {
@RequestBody info: ListRequest,
@RequestHeader("token") token: String
): List<Application> {
val user = sessionService.verifyTokenThrow(token) val user = sessionService.verifyTokenThrow(token)
return applicationService.findAll(user, info) return applicationService.findAll(user)
} }
@GetMapping(path = ["/active"], produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun active(@RequestHeader("token") token: String): Application? { public fun get(@PathVariable id: String, @RequestHeader("token") token: String): Application? {
val user = sessionService.verifyTokenThrow(token) val user = sessionService.verifyTokenThrow(token)
val possibleApplications = applicationService.findAll(user, ListRequest(1)) val app = applicationService.findApplicationById(user, id)
if (possibleApplications.size == 0) { if (app == null) {
return null throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
} }
return applicationService.findApplicationById(user, possibleApplications[0].id) return app
}
@PostMapping(
path = ["/link/application/{toLink}/{surviving}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
)
public fun get(
@PathVariable toLink: String,
@PathVariable surviving: String,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
val toLinkApp = applicationService.findApplicationById(user, toLink)
val app = applicationService.findApplicationById(user, surviving)
if (app == null || toLinkApp == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
applicationService.linkApplications(toLinkApp, app)
applicationService.delete(toLinkApp)
return app
} }
@PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE]) @PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE])
@ -325,11 +336,11 @@ class ApplicationsController(
throw NotFound() throw NotFound()
} }
if (application.status == info.status) { if (application.status_id == info.status_id) {
return application; return application
} }
application.status = info.status application.status_id = info.status_id
applicationService.updateStatus(application) applicationService.updateStatus(application)
@ -362,68 +373,28 @@ class ApplicationsController(
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null) throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
} }
if (application.unique_url != null) {
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Application already has unique_url",
null
)
}
application.original_url = application.url
application.url = info.url application.url = info.url
application.unique_url = info.url.split("?")[0] application.simple_url = info.url.split("?")[0]
var maybe_exists = var maybe_exists =
applicationService.findApplicationByUrl( applicationService.findApplicationByUrl(user, application.url)
user, ?: applicationService.findApplicationByUrl(user, application.simple_url)
application.url,
application.unique_url if (maybe_exists != null && maybe_exists.id != application.id) {
)
if (maybe_exists != null) {
applicationService.delete(application) applicationService.delete(application)
if (maybe_exists.status == 0 && application.status == 1) {
maybe_exists.status = 1
applicationService.update(maybe_exists)
}
maybe_exists.flairs = flairService.listFromLinkApplicationId(maybe_exists.id) maybe_exists.flairs = flairService.listFromLinkApplicationId(maybe_exists.id)
maybe_exists.events = eventService.listFromApplicationId(maybe_exists.id).toList()
return maybe_exists return maybe_exists
} }
applicationService.addUrl(application.id, info.url)
applicationService.addUrl(application.id, info.url.split("?")[0])
applicationService.update(application) applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id) application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id) application.events = eventService.listFromApplicationId(application.id).toList()
return application
}
@PostMapping(path = ["/reset/url/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun updateUrl(
@PathVariable id: String,
@RequestHeader("token") token: String
): Application {
val user = sessionService.verifyTokenThrow(token)
var application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
if (application.unique_url == null) {
throw BadRequest()
}
application.url = application.original_url!!
application.original_url = null
application.unique_url = null
applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id)
return application return application
} }
@ -466,28 +437,13 @@ class ApplicationsController(
class ApplicationService( class ApplicationService(
val db: JdbcTemplate, val db: JdbcTemplate,
val flairService: FlairService, val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService, val eventService: EventService,
) { ) {
public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? { public fun findApplicationByUrl(user: UserDb, url: String): Application? {
if (unique_url != null) { val applications =
val unique: List<Application> =
db.query(
"select * from applications where unique_url=? and user_id=?",
arrayOf(unique_url, user.id),
Application
)
.toList()
if (unique.size != 0) {
return unique[0]
}
}
val applications: List<Application> =
db.query( db.query(
"select * from applications where url=? and user_id=?", "select * from applications as app inner join applications_urls as app_url on app_url.application_id=app.id where app_url.url=? and user_id=?",
arrayOf(url, user.id), arrayOf(url, user.id),
Application Application
) )
@ -516,7 +472,6 @@ class ApplicationService(
var application = applications[0] var application = applications[0]
application.flairs = flairService.listFromLinkApplicationId(application.id) application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
application.events = eventService.listFromApplicationId(application.id).toList() application.events = eventService.listFromApplicationId(application.id).toList()
return application return application
@ -538,106 +493,80 @@ class ApplicationService(
return application return application
} }
public fun addUrl(id: String, url: String) {
val applications =
db.query(
"select * from applications_urls as app_url where app_url.url=? and app_url.application_id=?",
arrayOf(url, id),
ApplicationUrl
)
.toList()
if (applications.size > 0) {
return
}
db.update("insert into applications_urls (application_id, url) values (?, ?);", id, url)
}
public fun createApplication(user: UserDb, application: Application): Boolean { public fun createApplication(user: UserDb, application: Application): Boolean {
if (this.findApplicationByUrl(user, application.url, application.unique_url) != null) { if (this.findApplicationByUrl(user, application.url) != null) {
return false return false
} }
// Create time is auto created by the database // Create time is auto created by the database
// The default status is null
db.update( db.update(
"insert into applications (id, url, original_url, unique_url, title, user_id, extra_data, payrange, status, company, recruiter, message, linked_application, status_history, application_time, agency) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", "insert into applications (id, url, title, user_id, extra_data, payrange, status_id, company, recruiter, message, agency, simple_url) values (?,?,?,?,?,?,?,?,?,?,?,?);",
application.id, application.id,
application.url, application.url,
application.original_url,
application.unique_url,
application.title, application.title,
application.user_id, application.user_id,
application.extra_data, application.extra_data,
application.payrange, application.payrange,
application.status, application.status_id,
application.company, application.company,
application.recruiter, application.recruiter,
application.message, application.message,
application.linked_application,
application.status_history,
application.application_time,
application.agency, application.agency,
application.simple_url,
) )
eventService.create(application.id, EventType.Creation) eventService.create(application.id, EventType.Creation)
addUrl(application.id, application.url)
return true return true
} }
private fun internalFindAll(user: UserDb, info: ListRequest): Iterable<Application> { private fun internalFindAll(user: UserDb): Iterable<Application> {
if (info.status == null) {
return db.query(
"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
if (info.status == 0) {
return db.query(
"select * from applications where user_id=? and linked_application='' and status=0 order by title asc;",
arrayOf(user.id),
Application
)
}
return db.query( return db.query(
"select * from applications where user_id=? and status=? order by title asc;", "select * from applications where user_id=? order by title asc;",
arrayOf(user.id, info.status), arrayOf(user.id),
Application, Application
) )
} }
public fun findAll(user: UserDb, info: ListRequest): List<Application> { public fun findAll(user: UserDb): List<Application> {
var iter = internalFindAll(user, info); var iter = internalFindAll(user)
if (info.views == true) {
iter = iter.map {
it.views = viewService.listFromApplicationId(it.id)
it
}
}
return iter.toList() return iter.toList()
} }
public fun findAllByUserStatusId(statusId: String, user: UserDb): List<Application> { public fun findAllByUserStatusId(statusId: String, user: UserDb): List<Application> {
return db.query( return db.query(
"select * from applications where status_id=? and user_id=? order by title asc;", "select * from applications where status_id=? and user_id=? order by title asc;",
arrayOf(statusId, user.id), arrayOf(statusId, user.id),
Application Application
) )
} }
// Update the stauts on the application object before giving it to this function // Update the stauts on the application object before giving it to this function
// TODO how status history works // TODO how status history works
public fun updateStatus(application: Application): Application { public fun updateStatus(application: Application): Application {
val status_string = "${application.status}" eventService.create(application.id, EventType.StatusUpdate, application.status_id)
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( db.update(
"update applications set status=?, status_history=?, application_time=? where id=?", "update applications set status_id=? where id=?",
application.status, application.status_id,
application.status_history,
application.application_time,
application.id, application.id,
) )
return application return application
@ -647,10 +576,8 @@ class ApplicationService(
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=?, company=?, recruiter=?, message=?, linked_application=?, agency=? where id=?", "update applications set url=?, title=?, user_id=?, extra_data=?, payrange=?, company=?, recruiter=?, message=?, agency=?, simple_url=? where id=?",
application.url, application.url,
application.original_url,
application.unique_url,
application.title, application.title,
application.user_id, application.user_id,
application.extra_data, application.extra_data,
@ -658,17 +585,29 @@ class ApplicationService(
application.company, application.company,
application.recruiter, application.recruiter,
application.message, application.message,
application.linked_application,
application.agency, application.agency,
application.simple_url,
application.id, application.id,
) )
return application return application
} }
public fun linkApplications(toLink: Application, surviving: Application) {
db.update(
"update applications_urls set application_id=? where application_id=?;",
surviving.id,
toLink.id,
)
}
public fun delete(application: Application) { public fun delete(application: Application) {
db.update( db.update(
"delete from applications where id=?", "delete from applications where id=?",
application.id, application.id,
) )
db.update(
"delete from applications_urls where application_id=?",
application.id,
)
} }
} }

View File

@ -25,7 +25,7 @@ data class Event(
var id: String, var id: String,
var application_id: String, var application_id: String,
var event_type: Int, var event_type: Int,
var new_status: Int?, var new_status_id: String?,
var time: Timestamp var time: Timestamp
) { ) {
companion object : RowMapper<Event> { companion object : RowMapper<Event> {
@ -34,7 +34,7 @@ data class Event(
rs.getString("id"), rs.getString("id"),
rs.getString("application_id"), rs.getString("application_id"),
rs.getInt("event_type"), rs.getInt("event_type"),
rs.getInt("new_status"), rs.getString("new_status_id"),
rs.getTimestamp("time"), rs.getTimestamp("time"),
) )
} }
@ -93,23 +93,19 @@ public class EventService(val db: JdbcTemplate) {
public fun create( public fun create(
application_id: String, application_id: String,
event_type: EventType, event_type: EventType,
new_status: Int? = null new_status_id: String? = null
): Event { ): Event {
val id = UUID.randomUUID().toString() 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 = var new_event =
Event(id, application_id, event_type.value, new_status, Timestamp(Date().getTime())) Event(id, application_id, event_type.value, new_status_id, Timestamp(Date().getTime()))
db.update( db.update(
"insert into events (id, application_id, event_type, new_status) values (?, ?, ? ,?)", "insert into events (id, application_id, event_type, new_status_id) values (?, ?, ? ,?)",
new_event.id, new_event.id,
new_event.application_id, new_event.application_id,
new_event.event_type, new_event.event_type,
new_event.new_status, new_event.new_status_id,
) )
return new_event return new_event

View File

@ -2,8 +2,8 @@ package com.andr3h3nriqu3s.applications
import java.sql.ResultSet import java.sql.ResultSet
import java.util.UUID import java.util.UUID
import org.springframework.http.MediaType
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -52,10 +52,6 @@ class FlairController(
throw NotFound() throw NotFound()
} }
if (old_flair.user_id != user.id) {
throw NotAuth()
}
flair.user_id = old_flair.user_id flair.user_id = old_flair.user_id
return flairService.updateFlair(flair) return flairService.updateFlair(flair)
@ -88,6 +84,8 @@ data class SimpleFlair(
val name: String, val name: String,
val description: String, val description: String,
val color: String, val color: String,
val sort: Int,
val showFullDescription: Int,
) )
data class Flair( data class Flair(
@ -97,6 +95,8 @@ data class Flair(
var name: String, var name: String,
var expr: String, var expr: String,
var description: String, var description: String,
var sort: Int,
var showFullDescription: Int,
) { ) {
companion object : RowMapper<Flair> { companion object : RowMapper<Flair> {
override public fun mapRow(rs: ResultSet, rowNum: Int): Flair { override public fun mapRow(rs: ResultSet, rowNum: Int): Flair {
@ -107,12 +107,20 @@ data class Flair(
rs.getString("name"), rs.getString("name"),
rs.getString("expr"), rs.getString("expr"),
rs.getString("description"), rs.getString("description"),
rs.getInt("sort"),
rs.getInt("showFullDescription"),
) )
} }
} }
fun toFlairSimple(): SimpleFlair { fun toFlairSimple(): SimpleFlair {
return SimpleFlair(this.name, this.description, this.color) return SimpleFlair(
this.name,
this.description,
this.color,
this.sort,
this.showFullDescription
)
} }
} }
@ -160,7 +168,7 @@ public class FlairService(val db: JdbcTemplate) {
public fun listFromLinkApplicationId(id: String): List<Flair> = public fun listFromLinkApplicationId(id: String): List<Flair> =
db.query( db.query(
"select f.id, f.user_id, f.color, f.name, f.expr, f.description from flair_link as fl inner join flair as f on f.id = fl.flair_id where application_id=? order by name asc;", "select f.id, f.user_id, f.color, f.name, f.expr, f.description, f.sort, f.showFullDescription from flair_link as fl inner join flair as f on f.id = fl.flair_id where application_id=? order by name asc;",
arrayOf(id), arrayOf(id),
Flair Flair
) )
@ -210,12 +218,14 @@ public class FlairService(val db: JdbcTemplate) {
public fun updateFlair(flair: Flair): Flair { public fun updateFlair(flair: Flair): Flair {
db.update( db.update(
"update flair set user_id=?, color=?, name=?, expr=?, description=? where id=?;", "update flair set user_id=?, color=?, name=?, expr=?, description=?, sort=?, showFullDescription=? where id=?;",
flair.user_id, flair.user_id,
flair.color, flair.color,
flair.name, flair.name,
flair.expr, flair.expr,
flair.description, flair.description,
flair.sort,
flair.showFullDescription,
flair.id, flair.id,
) )
@ -231,16 +241,18 @@ public class FlairService(val db: JdbcTemplate) {
description = flair.description!! description = flair.description!!
} }
var new_flair = Flair(id, user.id, flair.color, flair.name, flair.expr, description) var new_flair = Flair(id, user.id, flair.color, flair.name, flair.expr, description, 0, 1)
db.update( db.update(
"insert into flair (id, user_id, color, name, expr, description) values (?, ?, ?, ?, ?, ?)", "insert into flair (id, user_id, color, name, expr, description, sort, showFullDescription) values (?, ?, ?, ?, ?, ?, ?, ?)",
new_flair.id, new_flair.id,
new_flair.user_id, new_flair.user_id,
new_flair.color, new_flair.color,
new_flair.name, new_flair.name,
new_flair.expr, new_flair.expr,
new_flair.description new_flair.description,
new_flair.showFullDescription,
new_flair.sort,
) )
return new_flair return new_flair

View File

@ -28,7 +28,9 @@ data class UserStatusNode(
var y: Int, var y: Int,
var width: Int, var width: Int,
var height: Int, var height: Int,
var permission: Int var permission: Int,
var visible: Boolean,
var endable: Boolean,
) { ) {
companion object : RowMapper<UserStatusNode> { companion object : RowMapper<UserStatusNode> {
override public fun mapRow(rs: ResultSet, rowNum: Int): UserStatusNode { override public fun mapRow(rs: ResultSet, rowNum: Int): UserStatusNode {
@ -42,6 +44,8 @@ data class UserStatusNode(
rs.getInt("width"), rs.getInt("width"),
rs.getInt("height"), rs.getInt("height"),
rs.getInt("permission"), rs.getInt("permission"),
rs.getBoolean("visible"),
rs.getBoolean("endable"),
) )
} }
} }
@ -211,7 +215,7 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
public fun update(node: UserStatusNode): UserStatusNode { public fun update(node: UserStatusNode): UserStatusNode {
db.update( db.update(
"update user_status_node set name=?, icon=?, x=?, y=?, width=?, height=?, permission=? where id=?;", "update user_status_node set name=?, icon=?, x=?, y=?, width=?, height=?, permission=?, visible=?, endable=? where id=?;",
node.name, node.name,
node.icon, node.icon,
node.x, node.x,
@ -219,6 +223,8 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.width, node.width,
node.height, node.height,
node.permission, node.permission,
node.visible,
node.endable,
node.id, node.id,
) )
@ -230,7 +236,7 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.id = id node.id = id
db.update( db.update(
"insert into user_status_node (id, user_id, name, icon, x, y, width, height, permission) values (?, ?, ?, ?, ?, ?, ?, ?, ?);", "insert into user_status_node (id, user_id, name, icon, x, y, width, height, permission, visible, endable) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
node.id, node.id,
node.user_id, node.user_id,
node.name, node.name,
@ -239,7 +245,9 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.y, node.y,
node.width, node.width,
node.height, node.height,
node.permission node.permission,
node.visible,
node.endable,
) )
return node return node

View File

@ -1,103 +0,0 @@
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: Timestamp) {
companion object : RowMapper<View> {
override public fun mapRow(rs: ResultSet, rowNum: Int): View {
return View(
rs.getString("id"),
rs.getString("application_id"),
rs.getTimestamp("time"),
)
}
}
}
@RestController
@ControllerAdvice
@RequestMapping("/api/view")
class ViewController(
val sessionService: SessionService,
val applicationService: ApplicationService,
val flairService: FlairService,
val viewService: ViewService,
) {
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun getCV(@PathVariable id: String, @RequestHeader("token") token: String): List<View> {
val user = sessionService.verifyTokenThrow(token)
val application = applicationService.findApplicationById(user, id)
if (application == null) {
throw NotFound()
}
return application.views
}
}
@Service
public class ViewService(val db: JdbcTemplate) {
public fun listFromApplicationId(id: String): List<View> =
db.query("select * from views where application_id=?;", arrayOf(id), View).toList()
public fun getById(id: String): View? {
val items = db.query("select * from views where id=?;", arrayOf(id), View).toList()
if (items.size == 0) {
return null
}
return items[0]
}
public fun deleteById(id: String): View {
val view = this.getById(id)
if (view == null) {
throw NotFound()
}
db.update("delete from views where id=?", id)
return view
}
public fun update(view: View): View {
db.update(
"update views set application_id=?, time=? where id=?;",
view.application_id,
view.time,
view.id,
)
return view
}
public fun create(application_id: String): View {
val id = UUID.randomUUID().toString()
var new_view = View(id, application_id, Timestamp(Date().getTime()))
db.update(
"insert into views (id, application_id) values (?, ?)",
new_view.id,
new_view.application_id
)
return new_view
}
}

View File

@ -1,61 +1,53 @@
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id VARCHAR PRIMARY KEY, id VARCHAR PRIMARY KEY,
username VARCHAR NOT NULL, username VARCHAR NOT NULL,
email VARCHAR NOT NULL, email VARCHAR NOT NULL,
passwd VARCHAR NOT NULL, passwd VARCHAR NOT NULL,
level INT NOT NULL level INT NOT NULL
); );
CREATE TABLE IF NOT EXISTS tokens ( CREATE TABLE IF NOT EXISTS tokens (token VARCHAR PRIMARY KEY, user_id VARCHAR);
token VARCHAR PRIMARY KEY,
user_id VARCHAR
);
create table if not exists applications ( create table if not exists applications (
id text primary key, id text primary key,
url text not null, url text not null,
original_url text, simple_url text not null,
unique_url text, company text,
company text, recruiter text,
recruiter text, title text,
title text, mesasge text default '',
mesasge text default '', user_id text,
status_history text default '', extra_data text,
user_id text, status_id text default null,
extra_data text, agency boolean default false,
-- this status will be deprecated in favor of the node style status create_time timestamp default now ()
status integer,
status_id text default null,
linked_application text default '',
application_time text default '',
agency boolean default false,
create_time timestamp default now()
); );
-- Views are deprecated will be removed in the future create table if not exists applications_urls (
create table if not exists views ( application_id text,
id text primary key, url text not null,
application_id text not null, primary key (application_id, url)
time timestamp default now()
); );
create table if not exists flair ( create table if not exists flair (
id text primary key, id text primary key,
user_id text not null, user_id text not null,
color text default '#ff0000', color text default '#ff0000',
name text default 'New Flair', name text default 'New Flair',
expr text default 'flair', expr text default 'flair',
description text default '' description text default '',
showFullDescription integer default 1,
sort integer default 0
); );
create table if not exists flair_link ( create table if not exists flair_link (
id text primary key, id text primary key,
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 ( create table if not exists events (
id text primary key, id text primary key,
application_id text not null, application_id text not null,
-- --
-- Event Types -- Event Types
@ -64,46 +56,41 @@ create table if not exists events (
-- Creation(0), -- Creation(0),
-- StatusUpdate(1), -- StatusUpdate(1),
-- Page(2) -- Page(2)
event_type integer not null, event_type integer not null,
-- This only matters when event_type == 1 -- This only matters when event_type == 1
new_status integer, new_status_id text,
time timestamp default now ()
time timestamp default now()
); );
-- --
-- User Controlled Status -- User Controlled Status
-- --
create table if not exists user_status_node ( create table if not exists user_status_node (
id text primary key, id text primary key,
user_id text not null, user_id text not null,
name text not null, name text not null,
icon text not null, icon text not null,
x integer default 0, x integer default 0,
y integer default 0, y integer default 0,
width integer default 0, width integer default 0,
height integer default 0, height integer default 0,
permission integer default 0 permission integer default 0,
visible boolean default true,
endable boolean default false
); );
create table if not exists user_status_link ( create table if not exists user_status_link (
id text primary key, id text primary key,
-- You technically can get this by loking a the source and target nodes but that seams more complicated -- You technically can get this by loking a the source and target nodes but that seams more complicated
user_id text not null, user_id text not null,
-- This can be null because null means creation -- This can be null because null means creation
source_node text, source_node text,
-- This can be null because null means creation -- This can be null because null means creation
target_node text, target_node text,
source_x integer default 0,
source_x integer default 0, source_y integer default 0,
source_y integer default 0, target_x integer default 0,
target_y integer default 0,
target_x integer default 0,
target_y integer default 0,
-- If this link is bidiretoral -- If this link is bidiretoral
bi boolean bi boolean
); );

View File

@ -1,138 +1,67 @@
import type { Flair } from './FlairStore.svelte'; import type { Flair } from './FlairStore.svelte';
import { post } from './utils'; import { get } from './utils';
export type AsEnum<T> = T[keyof T]; export type AsEnum<T> = T[keyof T];
export const ApplicationStatus = Object.freeze({
ToApply: 0,
WorkingOnIt: 1,
Ignore: 2,
ApplyedButSaidNo: 3,
Applyed: 4,
Expired: 5,
TasksToDo: 6,
TasksToDo2: 10,
LinkedApplication: 7,
InterviewStep1: 8,
InterviewStep2: 9,
FinalInterview: 11
});
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',
10: 'list-check',
7: 'link-45deg',
8: 'person',
9: 'people',
11: 'badge-vo-fill'
});
export const ApplicationStatusMaping: Record<AsEnum<typeof ApplicationStatus>, string> = Object.freeze({
0: 'To Apply',
1: 'Working On It',
2: 'Ignore',
3: 'Applyed But Said No',
4: 'Applyed',
5: 'Expired',
6: 'Tasks To Do',
10: 'Tasks To Do 2',
7: 'Linked Application',
8: 'Interview 1',
9: 'Interview 2',
11: 'Final Interview'
});
export type View = {
id: string;
application_id: string;
time: string;
};
export const EventType = Object.freeze({ export const EventType = Object.freeze({
Creation: 0, Creation: 0,
StatusUpdate: 1, StatusUpdate: 1,
View: 2, View: 2
}); });
export type ApplicationEvent = { export type ApplicationEvent = {
id: string, id: string;
application_id: string, application_id: string;
event_type: AsEnum<typeof EventType>, event_type: AsEnum<typeof EventType>;
new_status: number, new_status_id: string;
time: string time: string;
} };
export type Application = { export type Application = {
id: string; id: string;
url: string; url: string;
original_url: string | null; simple_url: string;
unique_url: string | null;
title: string; title: string;
user_id: string; user_id: string;
extra_data: string; extra_data: string;
payrange: string; payrange: string;
status: AsEnum<typeof ApplicationStatus>; status_id: string;
recruiter: string; recruiter: string;
agency: boolean; agency: boolean;
company: string; company: string;
message: string; message: string;
linked_application: string; linked_application: string;
application_time: string;
create_time: string; create_time: string;
status_history: string; status_history: string;
flairs: Flair[]; flairs: Flair[];
views: View[];
events: ApplicationEvent[]; events: ApplicationEvent[];
}; };
function createApplicationStore() { function createApplicationStore() {
let applications: Application[] = $state([]);
let applyed: Application[] = $state([]);
let all: Application[] = $state([]); let all: Application[] = $state([]);
let dragApplication: Application | undefined = $state(undefined); let dragApplication: Application | undefined = $state(undefined);
let loadItem: Application | undefined = $state(undefined); let loadItem: Application | undefined = $state(undefined);
let loadItemOpen = $state(true);
let req = false;
return { return {
/**
* @throws {Error}
*/
async loadApplications(force = false) {
if (!force && applications.length > 1) {
return;
}
applications = await post('application/list', { status: 0 });
},
/**
* @throws {Error}
*/
async loadAplyed(force = false) {
if (!force && applyed.length > 1) {
return;
}
applyed = await post('application/list', { status: ApplicationStatus.Applyed, views: true });
},
/** /**
* @throws {Error} * @throws {Error}
*/ */
async loadAll(force = false) { async loadAll(force = false) {
if (req) return;
if (!force && all.length > 1) { if (!force && all.length > 1) {
return; return;
} }
all = await post('application/list', {}); req = true;
}, try {
all = await get('application/list');
clear() { } finally {
applications = []; req = false;
}
}, },
dragStart(application: Application | undefined) { dragStart(application: Application | undefined) {
@ -146,28 +75,53 @@ function createApplicationStore() {
dragApplication = undefined; dragApplication = undefined;
}, },
removeByID(uuid: string) {
all = all.filter((i) => i.id !== uuid);
},
getIndexById(uuid: string): number | undefined {
return all
.map((a, i) => [a, i] as [Application, number])
.filter((i) => i[0].id === uuid)[0]?.[1];
},
get dragging() { get dragging() {
return dragApplication; return dragApplication;
}, },
get applications() {
return applications;
},
get applyed() {
return applyed;
},
get all() { get all() {
return all; return all;
}, },
reset() {
// This just tells svelte that we changed all
all = all;
},
set(item: Application, index?: number) {
if (index === undefined) {
index = this.getIndexById(item.id);
}
if (index === undefined) return;
all[index] = item;
this.reset();
},
get loadItem() { get loadItem() {
return loadItem; return loadItem;
}, },
set loadItem(item: Application | undefined) { set loadItem(item: Application | undefined) {
loadItem = item; loadItem = item;
loadItemOpen = true;
},
get loadItemOpen() {
return loadItemOpen;
},
set loadItemOpen(_loadItemOpen: boolean) {
loadItemOpen = _loadItemOpen;
} }
}; };
} }

View File

@ -1,29 +1,31 @@
import { get } from './utils'; import { get } from './utils';
export type Flair = { export type Flair = {
id: string; id: string;
user_id: string; user_id: string;
color: string; color: string;
name: string; name: string;
expr: string; expr: string;
description: string; description: string;
showFullDescription: number;
sort: number;
}; };
function createFlairStore() { function createFlairStore() {
let flairList: Flair[] = $state([]); let flairList: Flair[] = $state([]);
return { return {
get flairs() { get flairs() {
return flairList; return flairList;
}, },
async loadItems(force?: boolean) { async loadItems(force?: boolean) {
if (flairList.length > 0 && !force) { if (flairList.length > 0 && !force) {
return; return;
} }
flairList = await get('flair/'); flairList = await get('flair/');
} }
}; };
} }
export const flairStore = createFlairStore(); export const flairStore = createFlairStore();

View File

@ -0,0 +1,80 @@
import { get } from './utils';
import type { Node, FullLinkApi } from '../routes/flow/types';
function createStatusStore() {
let nodes: Node[] = $state([]);
let links: FullLinkApi[] = $state([]);
let nodesR: Record<string, Node> = $state({});
let dirLinks: Record<string, Node[]> = $state({});
let request = false;
return {
/**
* @throws {Error}
**/
async load(clear = false) {
if (!clear && nodes.length !== 0) {
return;
}
if (request) return;
request = true;
try {
nodes = await get('user/status/node');
links = await get('user/status/link');
} catch (e) {
request = false;
throw e;
}
dirLinks = {};
nodesR = {};
nodes.sort((a, b) => {
if (a.y !== b.y) return a.y - b.y;
return b.x - a.x;
});
for (const node of nodes) {
nodesR[node.id] = node;
}
const nodesId: string[] = [null as any, ...Object.keys(nodesR)];
nodesR[null as any] = {
icon: 'plus',
id: null as any,
name: 'Created'
} as any;
for (const nodeId of nodesId) {
const targets = [];
for (const link of links) {
if (link.source_node === nodeId) {
targets.push(nodesR[link.target_node as any]);
}
}
dirLinks[nodeId] = targets;
}
request = false;
},
get nodes() {
return nodes;
},
get nodesR() {
return nodesR;
},
get dirLinks() {
return dirLinks;
}
};
}
export const statusStore = createStatusStore();

View File

@ -3,9 +3,7 @@
import HasUser from '$lib/HasUser.svelte'; import HasUser from '$lib/HasUser.svelte';
import ApplicationsList from './ApplicationsList.svelte'; import ApplicationsList from './ApplicationsList.svelte';
import WorkArea from './work-area/WorkArea.svelte'; import WorkArea from './work-area/WorkArea.svelte';
import AppliyedList from './AppliyedList.svelte'; import ApplicationTypesList from './ApplicationTypesList.svelte';
import PApplicationList from './PApplicationList.svelte';
import { ApplicationStatus } from '$lib/ApplicationsStore.svelte';
</script> </script>
<HasUser redirect="/cv"> <HasUser redirect="/cv">
@ -13,25 +11,11 @@
<NavBar /> <NavBar />
<div class="w-full px-4 grow h-full gap-3 flex flex-col"> <div class="w-full px-4 grow h-full gap-3 flex flex-col">
<div class="flex gap-3 flex-grow max-h-[75%]"> <div class="flex gap-3 flex-grow max-h-[75%]">
<ApplicationsList /> <ApplicationsList />
<WorkArea /> <WorkArea />
</div> </div>
<PApplicationList status={ApplicationStatus.FinalInterview}> <ApplicationTypesList />
Interview Final <div class="p-3"></div>
</PApplicationList >
<PApplicationList status={ApplicationStatus.InterviewStep2}>
Interview II
</PApplicationList >
<PApplicationList status={ApplicationStatus.InterviewStep1}>
Interview I
</PApplicationList >
<PApplicationList status={ApplicationStatus.TasksToDo2}>
Tasks To do 2
</PApplicationList >
<PApplicationList status={ApplicationStatus.TasksToDo}>
Tasks To do
</PApplicationList >
<AppliyedList />
</div> </div>
</div> </div>
</HasUser> </HasUser>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import { statusStore } from '$lib/Types.svelte';
import { onMount } from 'svelte';
import SmartApplicationList from './SmartApplicationList.svelte';
onMount(async () => {
await statusStore.load();
});
</script>
{#if statusStore.nodes.length > 0}
{#each statusStore.nodes as node}
{#if node.visible}
<SmartApplicationList title={node.name} status_id={node.id} />
{/if}
{/each}
{/if}

View File

@ -5,11 +5,15 @@
let filter = $state(''); let filter = $state('');
onMount(() => { onMount(() => {
applicationStore.loadApplications(); applicationStore.loadAll();
}); });
let apps = $derived(
applicationStore.all.filter((i) => i.status_id === null && !i.linked_application)
);
let internal = $derived( let internal = $derived(
applicationStore.applications.filter((i) => { apps.filter((i) => {
if (!filter) { if (!filter) {
return true; return true;
} }
@ -47,10 +51,7 @@
}} }}
role="none" role="none"
> >
<div <div class="max-w-full" class:animate-pulse={applicationStore.dragging?.id === item.id}>
class="max-w-full"
class:animate-pulse={applicationStore.dragging?.id === item.id}
>
<h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden"> <h2 class="text-lg text-blue-500 flex gap-2 max-w-full overflow-hidden">
<div class="flex-grow max-w-[90%]"> <div class="flex-grow max-w-[90%]">
<div class="whitespace-nowrap overflow-hidden"> <div class="whitespace-nowrap overflow-hidden">

View File

@ -1,59 +0,0 @@
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
import { get } from '$lib/utils';
import { onMount } from 'svelte';
onMount(() => {
applicationStore.loadAplyed();
});
let filter = $state('');
</script>
<div class="card p-3 rounded-lg">
<h1 class="flex gap-2">
Applied <input bind:value={filter} placeholder="search" class="flex-grow text-blue-500" />
</h1>
<div class="flex flex-wrap gap-4 justify-between">
{#each applicationStore.applyed.filter((i) => {
if (!filter) {
return true;
}
const f = new RegExp(filter, 'ig');
let x = i.title;
if (i.company) {
x = `${x} @ ${i.company}`;
}
return x.match(f);
}) as item}
<button
class="card p-2 my-2 bg-slate-100 text-left"
onclick={async () => {
item.views = await get(`view/${item.id}`);
item.events = await get(`events/${item.id}`);
applicationStore.loadItem = item;
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}}
>
<div>
<h2 class="text-lg text-blue-500">
{item.title}
{#if item.company}
<div class="text-violet-800">
@ {item.company}
</div>
{/if}
</h2>
</div>
</button>
{/each}
</div>
</div>
<div class="min-h-[40px]">
</div>

View File

@ -5,9 +5,8 @@
<div class="p-7 pb-1"> <div class="p-7 pb-1">
<div class="card p-2 rounded-xl flex"> <div class="card p-2 rounded-xl flex">
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}> Home </button> <button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/')}>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/submit')}> Home
Submit text
</button> </button>
<button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flair')}> <button class="text-secudanry hover:text-violet-500 px-2" onclick={() => goto('/flair')}>
Flair Flair

View File

@ -1,52 +0,0 @@
<script lang="ts">
import {
ApplicationStatus,
applicationStore,
type AsEnum
} from '$lib/ApplicationsStore.svelte';
import { get } from '$lib/utils';
import { onMount, type Snippet } from 'svelte';
let { children, status }: { children: Snippet; status: AsEnum<typeof ApplicationStatus> } =
$props();
let applications = $derived(applicationStore.all.filter((item) => item.status == status))
onMount(() => {
applicationStore.loadAll();
});
</script>
{#if applications.length > 0}
<div class="card p-3 rounded-lg flex flex-col">
<h1>{@render children()}</h1>
<div class="overflow-auto flex-grow">
{#each applications as item}
<button
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({
top: 0,
behavior: 'smooth'
});
}}
>
<div>
<h2 class="text-lg text-blue-500">
{item.title}
{#if item.company}
<div class="text-violet-800">
@ {item.company}
</div>
{/if}
</h2>
</div>
</button>
{/each}
</div>
</div>
{/if}

View File

@ -0,0 +1,64 @@
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
import { onMount } from 'svelte';
const { title, status_id }: { title: string; status_id: string } = $props();
onMount(() => {
applicationStore.loadAll();
});
let filter = $state('');
let sorted = $derived(
applicationStore.all.filter((i) => {
return i.status_id === status_id;
})
);
</script>
{#if sorted.length > 0}
<div class="card p-3 rounded-lg">
<h1 class="flex gap-2">
{title} <input bind:value={filter} placeholder="search" class="flex-grow text-blue-500" />
</h1>
<div class="flex flex-wrap gap-4">
{#each sorted.filter((i) => {
if (!filter) {
return true;
}
const f = new RegExp(filter, 'ig');
let x = i.title;
if (i.company) {
x = `${x} @ ${i.company}`;
}
return x.match(f);
}) as item}
<button
class="card p-2 my-2 bg-slate-100 text-left"
onclick={async () => {
applicationStore.loadItem = item;
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}}
>
<div>
<h2 class="text-lg text-blue-500">
{item.title}
{#if item.company}
<div class="text-violet-800">
@ {item.company}
</div>
{/if}
</h2>
</div>
</button>
{/each}
</div>
</div>
{/if}

View File

@ -2,7 +2,7 @@
import { userStore } from '$lib/UserStore.svelte'; import { userStore } from '$lib/UserStore.svelte';
import { get } from '$lib/utils'; import { get } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ApplicationsList from '../ApplicationsList.svelte'; import Pill from './pill.svelte';
let id: string | undefined | null = $state(undefined); let id: string | undefined | null = $state(undefined);
@ -18,13 +18,15 @@
name: string; name: string;
description: string; description: string;
color: string; color: string;
sort: number;
showFullDescription: number;
}; };
type Application = { type Application = {
recruiter: string; recruiter: string;
message: string; message: string;
company: string; company: string;
agency: boolean, agency: boolean;
flairs: SimpleFlair[]; flairs: SimpleFlair[];
}; };
@ -40,16 +42,19 @@
} }
application.flairs.sort((a, b) => { application.flairs.sort((a, b) => {
if (a.description && b.description) { const aDesc = a.description && a.showFullDescription === 1;
return 0; const bDesc = b.description && b.showFullDescription === 1;
if (aDesc && bDesc) {
return b.sort - a.sort;
} }
if (a.description) { if (aDesc) {
return -1; return -1;
} }
if (b.description) { if (bDesc) {
return 1; return 1;
} }
return 0; return b.sort - a.sort;
}); });
loadFlairs(); loadFlairs();
@ -82,7 +87,7 @@
</svelte:head> </svelte:head>
<div class="flex items-center w-full flex-col"> <div class="flex items-center w-full flex-col">
<div class="py-10 w-[190mm]"> <div class="py-5 pb-0 w-[190mm] print:py-0 print:pt-4">
<div class="bg-white rounded-lg p-3"> <div class="bg-white rounded-lg p-3">
<div class="w-full flex"> <div class="w-full flex">
<h1 class="text-black text-5xl">Andre Henriques</h1> <h1 class="text-black text-5xl">Andre Henriques</h1>
@ -113,7 +118,7 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="w-full flex py-9"> <div class="w-full flex py-3 print:pb-0">
<div> <div>
I am a dedicated and versatile programmer with four years of professional I am a dedicated and versatile programmer with four years of professional
experience. <br /> experience. <br />
@ -128,18 +133,21 @@
</div> </div>
</div> </div>
</div> </div>
{#if application} {#if application}
{#if !application.agency} {#if !application.agency}
<h2 class="text-white p-3 text-4xl"> <h2 class="text-white p-6 print:p-0 text-4xl print:text-3xl">
👋 Hello 👋 Hello
{#if application.recruiter} {#if application.recruiter}
<span class="font-bold">{application.recruiter}</span> @ <span class="font-bold">{application.recruiter}</span> @
<span class="font-bold">{application.company}</span> <span class="font-bold">{application.company}</span>
{:else if application.company} {:else if application.company}
recruiter @ <span class="font-bold">{application.company}</span> recruiter @ <span class="font-bold">{application.company}</span>
{/if} {/if}
</h2> </h2>
{/if} {:else}
<div class="p-5 print:hidden"></div>
{/if}
{#if application.message} {#if application.message}
<div class="p-3 bg-white w-[190mm] rounded-lg"> <div class="p-3 bg-white w-[190mm] rounded-lg">
@ -153,41 +161,44 @@
{#if application.flairs.length > 0} {#if application.flairs.length > 0}
<div class="p-3 bg-white w-[190mm] rounded-lg"> <div class="p-3 bg-white w-[190mm] rounded-lg">
<h1 class="flex gap-5 items-end"> <h1 class="flex gap-5 items-end">
Your Ad & My skills {#if flairs.length > 0}<input Skills {#if flairs.length > 0}<input
placeholder="Search other skills!" placeholder="Click here to search skills!"
class="flex-grow text-blue-500 print:hidden" class="flex-grow text-blue-500 print:hidden"
bind:value={otherSearch} bind:value={otherSearch}
/> />
<span class="hidden print:inline text-slate-600 text-sm"><a href="https://www.andr3h3nriqu3s.com/cv?id={id}">Search other skills!</a></span> <span class="hidden print:inline text-slate-600 text-sm"
><a href="https://www.andr3h3nriqu3s.com/cv?id={id}"
>Click here to search other skills!</a
></span
>
{/if} {/if}
</h1> </h1>
<div class="flex flex-wrap gap-2 py-2"> <div class="flex flex-wrap gap-2 py-2 print:py-0">
{#if otherSearch === ''} {#if otherSearch === ''}
{#each application.flairs as flair} {#each application.flairs as flair}
<div class="min-w-0 {flair.description ? 'flex-grow w-full' : ''}"> {@const hasDesc = flair.description && flair.showFullDescription === 1}
{#if flair.description} <div class="min-w-0 {hasDesc ? 'flex-grow w-full' : ''}">
{#if hasDesc}
<div <div
class="p-2 rounded-lg forced-color-adjust-none" class="p-2 rounded-lg forced-color-adjust-none relative print:py-0"
style="background: {flair.color};" style="background: {flair.color};"
> >
<div <Pill
class="rounded-lg print:p-2 print:inline-block" text-size="text-base print:text-sm"
style="background: {flair.color}; print-color-adjust: exact !important;" class="print:inline-block text-2xl py-0 mb-2"
color={flair.color}
> >
{flair.name} {flair.name}
</div> </Pill>
<span class="hidden print:inline">:</span> <span class="hidden print:inline">:</span>
<div class="bg-white my-1 print:inline p-1 rounded-md"> <div class="bg-white my-1 print:inline p-1 rounded-md">
{flair.description} {flair.description}
</div> </div>
</div> </div>
{:else} {:else}
<div <Pill text-size="text-base print:text-sm" color={flair.color}>
class="p-2 rounded-lg forced-color-adjust-none"
style="background: {flair.color}; print-color-adjust: exact !important;"
>
{flair.name} {flair.name}
</div> </Pill>
{/if} {/if}
</div> </div>
{/each} {/each}
@ -196,10 +207,10 @@
a.name.match(new RegExp(otherSearch, 'i')) a.name.match(new RegExp(otherSearch, 'i'))
)} )}
{#if filtered_list.length == 0} {#if filtered_list.length == 0}
<div class="w-full text-center text-blue-500 font-bold text-2xl py-10"> <div class="w-full text-center text-blue-500 font-bold text-2xl py-10">
Could not find the skill you are looking for. Could not find the skill you are looking for.
</div> </div>
{:else} {:else}
{#each filtered_list as flair} {#each filtered_list as flair}
<div class="min-w-0 {flair.description ? 'flex-grow w-full' : ''}"> <div class="min-w-0 {flair.description ? 'flex-grow w-full' : ''}">
{#if flair.description} {#if flair.description}
@ -234,20 +245,18 @@
</div> </div>
{/if} {/if}
{/if} {/if}
<div class="p-2"></div>
<div class="w-[190mm]"> <div class="w-[190mm]">
<h2 class="pb-2 px-2 print:px-5 text-4xl print:text-3xl font-bold text-white"> <h2 class="p-3 print:p-0 text-4xl print:text-2xl font-bold print:font-normal text-white">
Work Expericence Work Expericence
</h2> </h2>
</div> </div>
<div class="p-2"></div>
<div class="w-[100vw] flex items-center flex-col"> <div class="w-[100vw] flex items-center flex-col">
<div class="p-3 bg-white w-[190mm] rounded-lg"> <div class="p-3 print:p-0 bg-white w-[190mm] rounded-lg">
<h1>Senior Software Developer @ Planum Solucoes</h1> <h1>Senior Software Developer @ Planum Solucoes</h1>
<h2>4 years - May 2020 - Present</h2>
<div class="ml-5"> <div class="ml-5">
<h2>4 years - May 2020 - Present</h2>
<h3>Developed various projects:</h3> <h3>Developed various projects:</h3>
<ul class="pl-5 list-disc"> <ul class="pl-5 list-disc">
<li>Developing various websites using React and Svelte.</li> <li>Developing various websites using React and Svelte.</li>
@ -255,30 +264,20 @@
<li>Implemented an ORM system using Java Reflection</li> <li>Implemented an ORM system using Java Reflection</li>
<li>Implemented automatic deployment with GitLab CI/CD tools.</li> <li>Implemented automatic deployment with GitLab CI/CD tools.</li>
<li>Linux Server Administration</li> <li>Linux Server Administration</li>
<li>Technologies used: React, WebRTC, WebSockets, Rest, Google Maps AP</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="p-2"></div> <div class="p-2 print:p-1"></div>
<div class="w-[100vw] flex items-center flex-col"> <div class="w-[100vw] flex items-center flex-col">
<div class="text-black w-[190mm] bg-white p-4 rounded-lg"> <div class="text-black w-[190mm] bg-white p-4 print:p-0 rounded-lg">
<div> <div>
<div> <div>
<h1>Associate Devops Engineer @ Sky UK</h1> <h1>Associate Devops Engineer @ Sky UK</h1>
<div class="ml-5"> <h2>1 year - July 2022 - June 2023</h2>
<h2>1 year - July 2022 - June 2023</h2> <div class="ml-2">
<h3>Working with:</h3>
<ul class="pl-5 list-disc">
<li>Python</li>
<li>Jenkins</li>
<li>GitLab CI</li>
<li>Ansible</li>
<li>Docker</li>
</ul>
<h3>Associated Software Developer / DevOps Engineer:</h3>
<ul class="pl-5 list-disc"> <ul class="pl-5 list-disc">
<li>Developed web-based tools for the DevOps team to use</li> <li>Developed web-based tools for the DevOps team to use</li>
<li> <li>
@ -298,13 +297,15 @@
</div> </div>
</div> </div>
<div class="p-5"></div> <div class="w-[190mm]">
<h2 class="p-3 print:p-0 text-4xl print:text-xl font-bold print:font-normal text-white">
<div class="bg-white p-3 text-black rounded-lg w-[190mm]"> Education
<h2 class="pb-2 text-3xl">Education</h2> </h2>
</div>
<div class="bg-white p-2 text-black rounded-lg w-[190mm] print:text-sm">
<div> <div>
<div> <div>
<h1>Bachelors of science in Computer Science @ University of Surrey</h1> <h1>BCompSc with First Class Honours @ University of Surrey</h1>
<div class="ml-5"> <div class="ml-5">
<h2>July 2020 - June 2024</h2> <h2>July 2020 - June 2024</h2>
</div> </div>
@ -312,6 +313,8 @@
</div> </div>
</div> </div>
<div class="p-3 print:hidden"></div>
<!--div class="p-5"></div> <!--div class="p-5"></div>
<div>TODO: Previous projetcs</div --> <div>TODO: Previous projetcs</div -->
<!-- div class="p-5"></div> <!-- div class="p-5"></div>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte';
const {
children,
color = 'rgb(134 239 172)',
class: className = '',
'text-size': textSize = 'text-xs'
}: { children: Snippet; color?: string; class?: string; 'text-size'?: string } = $props();
</script>
<span
class="px-2 py-1 rounded-lg forced-color-adjust-none bg-green-300 {className} {textSize}"
style="print-color-adjust: exact !important; background-color: {color};"
>
{@render children()}
</span>

View File

@ -95,6 +95,18 @@
<label class="flabel" for="color">Color</label> <label class="flabel" for="color">Color</label>
<input required type="color" id="color" bind:value={edit.color} /> <input required type="color" id="color" bind:value={edit.color} />
</fieldset> </fieldset>
<fieldset>
<label class="flabel" for="color">Show description</label>
<input
type="checkbox"
id="showDescription"
checked={edit.showFullDescription === 1}
onchange={() => {
if (!edit) return;
edit.showFullDescription = (edit.showFullDescription + 1) % 2;
}}
/>
</fieldset>
<fieldset> <fieldset>
<label class="flabel" for="name">Name</label> <label class="flabel" for="name">Name</label>
<input required id="name" class="finput w-full" bind:value={edit.name} /> <input required id="name" class="finput w-full" bind:value={edit.name} />
@ -106,12 +118,15 @@
<fieldset> <fieldset>
<label class="flabel" for="description">Description</label> <label class="flabel" for="description">Description</label>
<textarea <textarea
required
id="description" id="description"
class="finput w-full" class="finput w-full"
bind:value={edit.description} bind:value={edit.description}
></textarea> ></textarea>
</fieldset> </fieldset>
<fieldset>
<label class="flabel" for="sort">Sort</label>
<input type="number" class="finput w-full" bind:value={edit.sort} />
</fieldset>
<div class="btns w-full"> <div class="btns w-full">
<button class="btn-confirm">Save</button> <button class="btn-confirm">Save</button>
</div> </div>

View File

@ -8,11 +8,6 @@
import { extractLinkNodePosX, extractLinkNodePosY } from './types'; import { extractLinkNodePosX, extractLinkNodePosY } from './types';
import { deleteR, get, post, put } from '$lib/utils'; import { deleteR, get, post, put } from '$lib/utils';
import icon_list from './icons-list.json'; import icon_list from './icons-list.json';
import IconPicker from './IconPicker.svelte';
// TODOS: well a lot of stuff but
// - API
// - Automaticaly move to select mode if one of the targets fals inside the bounds
// //
// constatns // constatns
@ -47,7 +42,8 @@
height: 4, height: 4,
name: 'Created', name: 'Created',
icon: 'plus', icon: 'plus',
permission: 1 permission: 1,
visible: false
} as Node, } as Node,
...nodesRequest ...nodesRequest
]; ];
@ -72,7 +68,7 @@
y: a.target_y y: a.target_y
} }
})); }));
console.log("test", linkRequests) console.log('test', linkRequests);
} catch (e) { } catch (e) {
console.log('TODO inform user', e); console.log('TODO inform user', e);
} }
@ -191,7 +187,8 @@
width, width,
icon: icon_list[Math.floor(Math.random() * icon_list.length)], icon: icon_list[Math.floor(Math.random() * icon_list.length)],
name: 'New Status', name: 'New Status',
permission: 0 permission: 0,
visible: true
} as Node); } as Node);
nodes.push(result); nodes.push(result);
// Tell svelte that nodes is updated // Tell svelte that nodes is updated
@ -419,16 +416,19 @@
{linkMode} {linkMode}
linkSource={nodes[i] === linkSource?.node ? linkSource : undefined} linkSource={nodes[i] === linkSource?.node ? linkSource : undefined}
onremove={async () => { onremove={async () => {
try { try {
await deleteR(`user/status/node/${nodes[i].id}`); await deleteR(`user/status/node/${nodes[i].id}`);
links = links.filter(link => link.sourceNode.node.id !== nodes[i].id && link.targetNode.node.id !== nodes[i].id) links = links.filter(
(link) =>
link.sourceNode.node.id !== nodes[i].id && link.targetNode.node.id !== nodes[i].id
);
nodes.splice(i, 1); nodes.splice(i, 1);
nodes = nodes; nodes = nodes;
} catch (e) { } catch (e) {
console.log("TODO inform the user", e) console.log('TODO inform the user', e);
} }
}} }}
onNodePointClick={async (inNodeX, inNodeY) => { onNodePointClick={async (inNodeX, inNodeY) => {
if (mouseAction === undefined) { if (mouseAction === undefined) {
@ -471,7 +471,7 @@
// Tell svelte that the links changed // Tell svelte that the links changed
links = links; links = links;
linkSource = undefined; linkSource = undefined;
mouseAction = undefined; mouseAction = undefined;
} catch (e) { } catch (e) {
console.log('inform the user', e); console.log('inform the user', e);
} }

View File

@ -94,6 +94,43 @@
> >
<div class="bi bi-x mt-[2px]"></div> <div class="bi bi-x mt-[2px]"></div>
</button> </button>
<!-- Controll items -->
<div class="absolute flex gap-1 top-1 left-1">
<button
class="text-purple-400 hover:text-purple-800 rounded-full h-[20px] w-[20px] text-center leading-none"
onclick={async () => {
node.visible = !node.visible;
try {
await put('user/status/node', node);
} catch (e) {
console.log('TODO inform the user', e);
}
}}
>
{#if node.visible}
<div class="bi bi-eye-fill mt-[2px]"></div>
{:else}
<div class="bi bi-eye-slash-fill mt-[2px]"></div>
{/if}
</button>
<button
class="text-purple-400 hover:text-purple-800 rounded-full h-[20px] w-[20px] text-center leading-none"
onclick={async () => {
node.endable = !node.endable;
try {
await put('user/status/node', node);
} catch (e) {
console.log('TODO inform the user', e);
}
}}
>
{#if node.endable}
<div class="bi bi-align-end mt-[2px]"></div>
{:else}
<div class="bi bi-align-start mt-[2px]"></div>
{/if}
</button>
</div>
{/if} {/if}
</div> </div>

View File

@ -1,60 +1,69 @@
export type Node = { export type Node = {
id: string, id: string;
user_id: string, user_id: string;
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
name: string; name: string;
icon: string; icon: string;
// 1 disables editing // 1 disables editing
permission: number; permission: number;
visible: boolean;
endable: boolean;
}; };
export type LinkNode = { export type LinkNode = {
node: Node; node: Node;
x: number; x: number;
y: number; y: number;
} };
export type FullLink = { export type FullLink = {
sourceNode: LinkNode; sourceNode: LinkNode;
targetNode: LinkNode; targetNode: LinkNode;
bi?: boolean; bi?: boolean;
} };
export type FullLinkApi = { export type FullLinkApi = {
id: string, id: string;
user_id: string, user_id: string;
target_node: string | null, target_node: string | null;
source_node: string | null, source_node: string | null;
bi: boolean, bi: boolean;
source_x: number, source_x: number;
source_y: number, source_y: number;
target_x: number,
target_y: number
}
target_x: number;
target_y: number;
};
export type ActionType = undefined | 'drag' | 'create' | 'move' | 'link'; export type ActionType = undefined | 'drag' | 'create' | 'move' | 'link';
export function extractLinkNodePosX(canvasX: (x: number) => number, node: LinkNode, grid_size: number) { export function extractLinkNodePosX(
if (node.x === -1) { canvasX: (x: number) => number,
return canvasX(node.node.x); node: LinkNode,
} grid_size: number
if (node.x === node.node.width) { ) {
return canvasX(node.node.x) + node.node.width * grid_size; if (node.x === -1) {
} return canvasX(node.node.x);
return canvasX(node.node.x) + node.x * grid_size + grid_size / 2; }
if (node.x === node.node.width) {
return canvasX(node.node.x) + node.node.width * grid_size;
}
return canvasX(node.node.x) + node.x * grid_size + grid_size / 2;
} }
export function extractLinkNodePosY(canvasY: (x: number) => number, node: LinkNode, grid_size: number) { export function extractLinkNodePosY(
if (node.y === -1) { canvasY: (x: number) => number,
return canvasY(node.node.y); node: LinkNode,
} grid_size: number
if (node.y === node.node.height) { ) {
return canvasY(node.node.y) + node.node.height * grid_size; if (node.y === -1) {
} return canvasY(node.node.y);
return canvasY(node.node.y) + node.y * grid_size + grid_size / 2; }
if (node.y === node.node.height) {
return canvasY(node.node.y) + node.node.height * grid_size;
}
return canvasY(node.node.y) + node.y * grid_size + grid_size / 2;
} }

View File

@ -11,7 +11,7 @@
async function submit() { async function submit() {
try { try {
await post('application/text', text); await post('application/text', text);
applicationStore.loadApplications(true); applicationStore.loadAll(true);
goto('/'); goto('/');
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { statusStore } from '$lib/Types.svelte';
import { onMount } from 'svelte';
import DropZone from './DropZone.svelte';
import { deleteR, put } from '$lib/utils';
let { index = $bindable() }: { index: number | undefined } = $props();
const derivedItem: Application | undefined = $derived(applicationStore.all[index ?? -1]);
async function moveStatus(status_id: string, endable?: boolean) {
// Deactivate active item
try {
await put('application/status', {
id: derivedItem?.id,
status_id: status_id
});
} catch (e) {
// TODO: Show User
console.log('info data', e);
return;
}
applicationStore.all[index ?? -1].status_id = status_id;
applicationStore.reset();
if (endable) {
index = undefined;
return;
}
applicationStore.loadItem = derivedItem;
applicationStore.loadItemOpen = false;
}
async function remove() {
if (!derivedItem) return;
// Deactivate active item
try {
await deleteR(`application/${derivedItem.id}`);
} catch (e) {
// TODO: Show User
console.log('info data', e);
return;
}
applicationStore.removeByID(derivedItem.id);
index = undefined;
}
onMount(() => {
statusStore.load();
});
</script>
{#if applicationStore.dragging && derivedItem}
<div class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white">
{#each statusStore.dirLinks[derivedItem.status_id] as node}
<DropZone icon={node.icon} ondrop={() => moveStatus(node.id, node.endable)}
>{node.name}</DropZone
>
{/each}
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}>Delete it</DropZone>
</div>
{/if}

View File

@ -1,26 +1,37 @@
<script lang="ts"> <script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte'; import { applicationStore } from '$lib/ApplicationsStore.svelte';
let { save, company = $bindable() }: { company: string; save: () => void } = $props(); let { save, index }: { index: number; save: () => void } = $props();
let companies = $derived(new Set(applicationStore.all.map((a) => a.company))); let companies = $derived(new Set(applicationStore.all.map((a) => a.company)));
let fcomps = $derived( let fcomps = $derived(
company === '' applicationStore.all[index].company === ''
? [] ? []
: [...companies.values()].filter((a) => { : [...companies.values()].filter((a) => {
// TODO improve this a lot I want to make like matching algo // TODO improve this a lot I want to make like matching algo
return a.match(company) && a !== company; return (
a.match(applicationStore.all[index].company) &&
a !== applicationStore.all[index].company
);
}) })
); );
</script> </script>
<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={company} onchange={save} /> <input
class="finput"
id="title"
bind:value={applicationStore.all[index].company}
onchange={save}
/>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{#each fcomps as comp} {#each fcomps as comp}
<button class="bg-blue-200 px-3 p-1 min-h-0 rounded-lg" onclick={() => company = comp}> <button
class="bg-blue-200 px-3 p-1 min-h-0 rounded-lg"
onclick={() => (applicationStore.all[index].company = comp)}
>
{comp} {comp}
</button> </button>
{/each} {/each}

View File

@ -1,198 +0,0 @@
<script lang="ts">
import {
ApplicationStatus,
applicationStore,
type Application,
type AsEnum
} from '$lib/ApplicationsStore.svelte';
import { deleteR, put } from '$lib/utils';
import DropZone from './DropZone.svelte';
let { activeItem = $bindable() }: { activeItem: Application | undefined } = $props();
async function moveStatus(status: AsEnum<typeof ApplicationStatus>, moveOut = true) {
if (!activeItem) return;
// Deactivate active item
try {
await put('application/status', {
id: activeItem.id,
status: status
});
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
applicationStore.loadAplyed(true);
if (moveOut) {
activeItem = undefined;
} else {
activeItem.status = status;
}
//openedWindow?.close();
//openedWindow = undefined;
}
async function remove() {
if (!activeItem) return;
// Deactivate active item
try {
await deleteR(`application/${activeItem.id}`);
} catch (e) {
// Show User
console.log('info data', e);
return;
}
applicationStore.loadApplications(true);
activeItem = undefined;
//openedWindow?.close();
//openedWindow = undefined;
}
</script>
{#if applicationStore.dragging && activeItem}
<div
class="flex w-full flex-grow rounded-lg p-3 gap-2 absolute bottom-0 left-0 right-0 bg-white"
>
<!-- Do nothing -->
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<DropZone
icon="box-arrow-down"
ondrop={async () => {
await moveStatus(ApplicationStatus.ToApply);
applicationStore.loadAplyed(true);
applicationStore.loadAll(true);
}}
>
To apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Ignore -->
<DropZone icon="trash-fill" ondrop={() => moveStatus(ApplicationStatus.Ignore)}>
Ignore it
</DropZone>
{/if}
{#if ([ApplicationStatus.WorkingOnIt, ApplicationStatus.Expired] as number[]).includes(activeItem.status)}
<!-- Expired -->
<DropZone
icon="clock-fill text-orange-500"
ondrop={() => {
if (activeItem && activeItem.status === ApplicationStatus.Expired) {
moveStatus(ApplicationStatus.ToApply, false);
} else {
moveStatus(ApplicationStatus.Expired);
}
}}
>
Mark as expired
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.WorkingOnIt}
<!-- Repeated -->
<DropZone icon="trash-fill text-danger" ondrop={() => remove()}>Delete it</DropZone>
{/if}
{#if ([ApplicationStatus.WorkingOnIt, ApplicationStatus.TasksToDo] as number[]).includes(activeItem.status)}
<!-- Applyed -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.Applyed);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Apply
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.Applyed}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do
</DropZone>
{/if}
{#if activeItem.status === ApplicationStatus.TasksToDo}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.TasksToDo2);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Tasks To Do 2
</DropZone>
{/if}
{#if ([ApplicationStatus.TasksToDo, ApplicationStatus.Applyed] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep1);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 1
</DropZone>
{/if}
{#if ([ApplicationStatus.InterviewStep1] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="server text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.InterviewStep2);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview 2
</DropZone>
{/if}
{#if ([ApplicationStatus.InterviewStep1, ApplicationStatus.InterviewStep2] as number[]).includes(activeItem.status)}
<!-- Tasks to do -->
<DropZone
icon="badge-vo-fill text-confirm"
ondrop={async () => {
await moveStatus(ApplicationStatus.FinalInterview);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
Interview Final
</DropZone>
{/if}
{#if !([ApplicationStatus.ApplyedButSaidNo] as number[]).includes(activeItem.status)}
<!-- Rejected -->
<DropZone
icon="fire text-danger"
ondrop={() => {
moveStatus(ApplicationStatus.ApplyedButSaidNo);
applicationStore.loadAll(true);
applicationStore.loadAplyed(true);
}}
>
I was rejeted :(
</DropZone>
{/if}
</div>
{/if}

View File

@ -22,7 +22,7 @@
onreload(); onreload();
} catch (e) { } catch (e) {
// Show message to the user // Show message to the user
console.log(e); console.log('TODO inform the user', e);
} }
} }
</script> </script>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ApplicationStatus, ApplicationStatusMaping, type Application } from '$lib/ApplicationsStore.svelte'; import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { post, put } from '$lib/utils'; import { statusStore } from '$lib/Types.svelte';
import { post } from '$lib/utils';
let { let {
application, application,
@ -13,43 +14,21 @@
} = $props(); } = $props();
let filter = $state(''); let filter = $state('');
let applications: Application[] = $state([]);
async function getApplicationList() {
const app: Application[] = await post('application/list', {});
applications = app.filter((a) => a.id != application.id);
}
$effect(() => {
getApplicationList();
});
async function submit(item: Application) { async function submit(item: Application) {
try { try {
application.linked_application = item.id; await post(`application/link/application/${application.id}/${item.id}`, {});
await put('application/update', application); applicationStore.removeByID(application.id);
await put('application/status', {
id: application.id,
status: ApplicationStatus.LinkedApplication
});
dialog.close(); dialog.close();
onreload(item); onreload(item);
} catch (e) { } catch (e) {
// Show message to the user // TODO: Show message to the user
console.log(e); console.log(e);
} }
} }
</script>
<dialog class="card max-w-[50vw]" bind:this={dialog}> let internal = $derived(
<div class="flex"> applicationStore.all.filter((i) => {
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{applications.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
{#each applications.filter((i) => {
if (!filter) { if (!filter) {
return true; return true;
} }
@ -62,7 +41,19 @@
} }
return x.match(f); return x.match(f);
}) as item} })
);
</script>
<dialog class="card max-w-[50vw]" bind:this={dialog}>
<div class="flex">
<input placeholder="Filter" class="p-2 flex-grow" bind:value={filter} />
<div>
{internal.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
{#each internal as item}
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none"> <div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
<button class="text-left max-w-full" type="button" onclick={() => submit(item)}> <button class="text-left max-w-full" type="button" onclick={() => submit(item)}>
<h2 class="text-lg text-blue-500"> <h2 class="text-lg text-blue-500">
@ -73,9 +64,9 @@
</div> </div>
{/if} {/if}
</h2> </h2>
<span> <span>
{ApplicationStatusMaping[item.status]} {statusStore.nodesR[item.status_id].name}
</span> </span>
</button> </button>
</div> </div>
{/each} {/each}

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { Application } from '$lib/ApplicationsStore.svelte'; import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { post, preventDefault } from '$lib/utils'; import { post, preventDefault } from '$lib/utils';
let { const {
onreload onreload
}: { }: {
onreload: (item: Application) => void; onreload: (item: Application) => void;
@ -12,20 +12,50 @@
let link = $state(''); let link = $state('');
let dialogAddMultiple = $state(false);
let inputField: HTMLTextAreaElement;
async function createApplication() { async function createApplication() {
try { try {
if (dialogAddMultiple) {
const r: Application[] = await post('application/text', {
text: link
});
for (const app of r) {
applicationStore.all.push(app);
}
if (r.length > 0) {
onreload(r[0]);
}
dialogElement.close();
link = '';
return;
}
const r: Application = await post('application/link', { const r: Application = await post('application/link', {
text: link text: link
}); });
applicationStore.all.push(r);
onreload(r); onreload(r);
dialogElement.close() dialogElement.close();
link = '';
} catch (e) { } catch (e) {
console.log('Inform the user', e); console.log('TODO: Inform the user', e);
} }
} }
function docKey(e: KeyboardEvent) { function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyN') { if (e.ctrlKey && e.shiftKey && e.code === 'KeyN') {
dialogAddMultiple = true;
dialogElement.showModal();
e.stopPropagation();
e.preventDefault();
window.requestAnimationFrame(() => {
inputField?.focus();
});
return;
} else if (e.ctrlKey && e.code === 'KeyN') {
dialogAddMultiple = false;
dialogElement.showModal(); dialogElement.showModal();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -41,11 +71,34 @@
}); });
</script> </script>
<dialog class="card max-w-[50vw]" bind:this={dialogElement}> <dialog
class="card max-w-[50vw]"
bind:this={dialogElement}
onclose={() => {
dialogAddMultiple = false;
}}
>
<form onsubmit={preventDefault(createApplication)}> <form onsubmit={preventDefault(createApplication)}>
<fieldset> <fieldset>
<label class="flabel" for="title">Link</label> <label class="flabel" for="title"
<input class="finput" id="title" bind:value={link} required /> >{#if dialogAddMultiple}Email text{:else}Link{/if}</label
>
{#if dialogAddMultiple}
<textarea
class="finput"
id="title"
bind:value={link}
required
bind:this={inputField}
></textarea>
{:else}
<input class="finput" id="title" bind:value={link} required />
{/if}
</fieldset> </fieldset>
<div class="flex justify-end">
{#if dialogAddMultiple}
<button type="submit" class="btn-primary">Submit</button>
{/if}
</div>
</form> </form>
</dialog> </dialog>

View File

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { Application } from '$lib/ApplicationsStore.svelte'; import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { post, preventDefault } from '$lib/utils'; import { post, preventDefault } from '$lib/utils';
let { let {
dialog = $bindable(), dialog = $bindable(),
onreload, onreload,
id id,
index
}: { }: {
dialog: HTMLDialogElement; dialog: HTMLDialogElement;
onreload: (item: Application) => void; onreload: (item: Application) => void;
id: string; id: string;
openWindow?: Window | null; openWindow?: Window | null;
index?: number;
} = $props(); } = $props();
const data = $state({ const data = $state({
@ -27,6 +29,9 @@
}); });
data.url = ''; data.url = '';
dialog.close(); dialog.close();
if (newItem.id !== applicationStore.all[index ?? -1].id) {
applicationStore.removeByID(applicationStore.all[index ?? -1].id);
}
onreload(newItem); onreload(newItem);
} catch (e) { } catch (e) {
// Show message to the user // Show message to the user

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { ApplicationStatusMaping, type Application } from '$lib/ApplicationsStore.svelte'; import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { post } from '$lib/utils'; import { statusStore } from '$lib/Types.svelte';
let { let {
application, application,
@ -11,18 +11,11 @@
} = $props(); } = $props();
let filter = $state(''); let filter = $state('');
let applications: Application[] = $state([]);
let dialogElement: HTMLDialogElement; let dialogElement: HTMLDialogElement;
async function getApplicationList() {
applications = await post('application/list', {});
}
function docKey(e: KeyboardEvent) { function docKey(e: KeyboardEvent) {
if (e.ctrlKey && e.code === 'KeyK') { if (e.ctrlKey && e.code === 'KeyK') {
applications = [];
getApplicationList();
dialogElement.showModal(); dialogElement.showModal();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -38,7 +31,10 @@
}); });
let internal = $derived( let internal = $derived(
applications.filter((i) => { applicationStore.all.filter((i) => {
if (i.linked_application) {
return false;
}
if (application && i.id == application.id) { if (application && i.id == application.id) {
return false; return false;
} }
@ -96,7 +92,7 @@
</div> </div>
</h2> </h2>
<span> <span>
{ApplicationStatusMaping[item.status]} {statusStore.nodesR[item.status_id]?.name ?? 'NOT FOUND'}
</span> </span>
</button> </button>
</div> </div>

View File

@ -2,13 +2,11 @@
import { import {
type Application, type Application,
type ApplicationEvent, type ApplicationEvent,
ApplicationStatus, EventType
EventType,
ApplicationStatusIconMaping,
ApplicationStatusMaping
} from '$lib/ApplicationsStore.svelte'; } from '$lib/ApplicationsStore.svelte';
import { statusStore } from '$lib/Types.svelte';
let { application, showAll }: { application: Application, showAll: boolean } = $props(); let { application, showAll }: { application: Application; showAll: boolean } = $props();
let events: (ApplicationEvent & { timeDiff: string })[] = $state([]); let events: (ApplicationEvent & { timeDiff: string })[] = $state([]);
@ -36,17 +34,10 @@
} }
$effect(() => { $effect(() => {
let status: number | undefined = undefined;
let lastEvent: ApplicationEvent | undefined = undefined; let lastEvent: ApplicationEvent | undefined = undefined;
let _events: typeof events = []; let _events: typeof events = [];
let checkArray: (number | undefined)[] = [
ApplicationStatus.WorkingOnIt,
ApplicationStatus.ToApply
];
for (let event of application.events) { for (let event of application.events) {
let time = ''; let time = '';
if (lastEvent) { if (lastEvent) {
@ -55,25 +46,6 @@
time = calcDiff(d2 - d1); time = calcDiff(d2 - d1);
} }
if (event.event_type === EventType.Creation) {
status = ApplicationStatus.ToApply;
}
if (event.event_type !== EventType.StatusUpdate || showAll) {
if (time) {
_events[_events.length - 1].timeDiff = time;
}
_events.push({ ...event, timeDiff: '' });
lastEvent = event;
continue;
}
if (
checkArray.includes(status) &&
checkArray.includes(event.new_status) &&
lastEvent?.event_type !== EventType.Creation
) {
continue;
}
if (lastEvent) { if (lastEvent) {
let d1 = new Date(lastEvent.time).getTime(); let d1 = new Date(lastEvent.time).getTime();
let d2 = new Date(event.time).getTime(); let d2 = new Date(event.time).getTime();
@ -84,26 +56,21 @@
_events[_events.length - 1].timeDiff = time; _events[_events.length - 1].timeDiff = time;
} }
status = event.new_status;
_events.push({ ...event, timeDiff: '' }); _events.push({ ...event, timeDiff: '' });
lastEvent = event; lastEvent = event;
} }
if (_events.length > 0 && !endable.includes(_events[_events.length - 1].new_status)) { // Todo endable
/*
if (_events.length > 0 && !endable.includes(_events[_events.length - 1].new_status_id)) {
let d1 = new Date(_events[_events.length - 1].time).getTime(); let d1 = new Date(_events[_events.length - 1].time).getTime();
let d2 = new Date().getTime(); let d2 = new Date().getTime();
_events[_events.length - 1].timeDiff = calcDiff(d2 - d1); _events[_events.length - 1].timeDiff = calcDiff(d2 - d1);
} }
*/
events = _events; events = _events;
}); });
let endable: number[] = [
ApplicationStatus.Expired,
ApplicationStatus.Ignore,
ApplicationStatus.ApplyedButSaidNo,
ApplicationStatus.LinkedApplication
];
</script> </script>
{#if events.length > 0} {#if events.length > 0}
@ -118,28 +85,24 @@
class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-10" 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} {#if event.event_type == EventType.Creation}
<span <span title={`Created @\n ${new Date(event.time).toLocaleString()}`} class="bi bi-plus"
title={`Created @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-plus"
></span> ></span>
{:else if event.event_type == EventType.View} {:else if event.event_type == EventType.View}
<span <span title={`Viewed @\n ${new Date(event.time).toLocaleString()}`} class="bi bi-eye"
title={`Viewed @\n ${new Date(event.time).toLocaleString()}`}
class="bi bi-eye"
></span> ></span>
{:else} {:else}
<span <span
title={`${ApplicationStatusMaping[event.new_status]}\n${new Date( title={`${statusStore.nodesR[event.new_status_id].name}\n${new Date(
event.time event.time
).toLocaleString()}`} ).toLocaleString()}`}
class="bi bi-{ApplicationStatusIconMaping[event.new_status]}" class="bi bi-{statusStore.nodesR[event.new_status_id].icon}"
></span> ></span>
{/if} {/if}
</div> </div>
{#if i != events.length - 1 || !endable.includes(event.new_status)} <!-- TODO -->
<div <!-- || !endable.includes(event.new_status) -->
class="min-w-[70px] h-[18px] bg-blue-500 -mx-[10px] px-[20px] flex-grow text-center" {#if i != events.length - 1 || !statusStore.nodesR[event.new_status_id].endable}
> <div class="min-w-[70px] h-[18px] bg-blue-500 -mx-[10px] px-[20px] flex-grow text-center">
{#if event.timeDiff} {#if event.timeDiff}
<div class="-mt-[3px] text-white"> <div class="-mt-[3px] text-white">
<span class="bi bi-clock"></span> <span class="bi bi-clock"></span>

View File

@ -1,10 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
ApplicationStatus,
ApplicationStatusMaping,
applicationStore,
type Application
} from '$lib/ApplicationsStore.svelte';
import { put, preventDefault, post, get } from '$lib/utils'; import { put, preventDefault, post, get } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -16,10 +11,15 @@
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'; import Timeline from './Timeline.svelte';
import DropingZone from './DropingZone.svelte';
import CompanyField from './CompanyField.svelte'; import CompanyField from './CompanyField.svelte';
import AutoDropZone from './AutoDropZone.svelte';
import { statusStore } from '$lib/Types.svelte';
import { thresholdFreedmanDiaconis } from 'd3';
let activeItem: Application | undefined = $state(); // Not this represents the index in the store array
let activeItem: number | undefined = $state();
let derivedItem: Application | undefined = $derived(applicationStore.all[activeItem ?? -1]);
let extractTokens: HTMLDialogElement; let extractTokens: HTMLDialogElement;
let changeUrl: HTMLDialogElement; let changeUrl: HTMLDialogElement;
@ -32,45 +32,30 @@
let drag = $state(true); let drag = $state(true);
async function activate(item?: Application) { async function activate(item?: Application, ow = true) {
if (!item) { if (!item) {
return; return;
} }
if (activeItem && activeItem.id !== item.id && activeItem.status === 1) {
// Deactivate active item
try {
await put('application/status', {
id: activeItem.id,
status: 0
});
} catch (e) {
// Show User
console.log('info data', e);
return;
}
}
if (item.status === 0) { if (ow && item.id !== derivedItem?.id) {
openWindow(item.url); openWindow(item.url);
openCV(item.id);
} }
openCV(item.id);
try { try {
await put('application/status', { const toLoadItem: Application = await get(`application/${item.id}`);
id: item.id, if (!toLoadItem?.simple_url) {
status: ApplicationStatus.WorkingOnIt
});
activeItem = await get('application/active');
if (!activeItem?.unique_url) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
changeUrl.showModal(); changeUrl.showModal();
}); });
} }
activeItem = applicationStore.getIndexById(toLoadItem.id);
applicationStore.all[activeItem ?? -1] = toLoadItem;
applicationStore.reset();
} catch (e) { } catch (e) {
// Show User // Show User
console.log('info data', e); console.log('info data', e);
} }
applicationStore.loadApplications(true);
autoExtData = false; autoExtData = false;
lastExtData = undefined; lastExtData = undefined;
@ -90,7 +75,7 @@
function openCV(id?: string) { function openCV(id?: string) {
window.open( window.open(
`/cv?id=${id ?? activeItem!.id}`, `/cv?id=${id ?? derivedItem!.id}`,
'cv viwer', 'cv viwer',
`location,height=${window.screen.height},width=${window.screen.width},scrollbars,status,toolbar,menubar,popup` `location,height=${window.screen.height},width=${window.screen.width},scrollbars,status,toolbar,menubar,popup`
); );
@ -100,8 +85,8 @@
// Make the CV open on a new page // Make the CV open on a new page
// //
function docKey(e: KeyboardEvent) { function docKey(e: KeyboardEvent) {
if (activeItem && e.ctrlKey && e.code === 'KeyO') { if (derivedItem && e.ctrlKey && e.code === 'KeyO') {
openCV(activeItem.id); openCV(derivedItem.id);
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
return; return;
@ -117,18 +102,6 @@
// //
// //
async function loadActive() {
try {
activeItem = await get('application/active');
if (activeItem?.unique_url === null) {
changeUrl.showModal();
}
} catch (e) {
// Show User
console.log('info data', e);
}
}
// Load Ext // Load Ext
$effect(() => { $effect(() => {
function onMessage(e: MessageEvent) { function onMessage(e: MessageEvent) {
@ -150,21 +123,22 @@
}); });
function setExtData() { function setExtData() {
if (!lastExtData || !activeItem) return; if (!lastExtData || activeItem === undefined || !derivedItem) return;
activeItem.title = lastExtData.jobTitle; applicationStore.all[activeItem].title = lastExtData.jobTitle;
activeItem.company = lastExtData.company; applicationStore.all[activeItem].company = lastExtData.company;
activeItem.payrange = lastExtData.money; applicationStore.all[activeItem].payrange = lastExtData.money;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
save().then(async () => { save().then(async () => {
if (activeItem === undefined) return;
if (!lastExtData.description) { if (!lastExtData.description) {
lastExtData = undefined; lastExtData = undefined;
return; return;
} }
await post('application/text/flair', { await post('application/text/flair', {
id: activeItem?.id ?? '', id: derivedItem.id ?? '',
text: lastExtData.description text: lastExtData.description
}); });
loadActive(); activate(derivedItem, false);
lastExtData = undefined; lastExtData = undefined;
}); });
}); });
@ -172,20 +146,26 @@
onMount(() => { onMount(() => {
userStore.checkLogin(); userStore.checkLogin();
loadActive(); statusStore.load();
}); });
$effect(() => { $effect(() => {
if (!applicationStore.loadItem) { if (!applicationStore.loadItem) {
return; return;
} }
activeItem = applicationStore.loadItem; (async () => {
applicationStore.loadItem = undefined; await activate(applicationStore.loadItem, applicationStore.loadItemOpen);
applicationStore.loadItem = undefined;
})();
});
$effect(() => {
applicationStore.loadAll();
}); });
async function save() { async function save() {
try { try {
await put('application/update', activeItem); await put('application/update', applicationStore.all[activeItem ?? -1]);
} catch (e) { } catch (e) {
// Show User // Show User
console.log('info data', e); console.log('info data', e);
@ -194,7 +174,7 @@
async function resetUrl() { async function resetUrl() {
try { try {
activeItem = await post(`application/reset/url/${activeItem?.id}`, {}); activeItem = await post(`application/reset/url/${derivedItem?.id}`, {});
} catch (e) { } catch (e) {
// Show User // Show User
console.log('info data', e); console.log('info data', e);
@ -203,11 +183,11 @@
</script> </script>
<div class="flex flex-col w-full gap-2 min-w-0 relative" role="none"> <div class="flex flex-col w-full gap-2 min-w-0 relative" role="none">
{#if activeItem && (!applicationStore.dragging || applicationStore.dragging?.id === activeItem.id)} {#if activeItem !== undefined && (!applicationStore.dragging || applicationStore.dragging?.id === derivedItem.id)}
<div <div
draggable={drag} draggable={drag}
ondrag={() => { ondrag={() => {
applicationStore.dragStart(activeItem); applicationStore.dragStart(derivedItem);
}} }}
ondragend={() => { ondragend={() => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -218,35 +198,34 @@
class="flex flex-col p-2 h-full gap-2 card min-w-0 flex-grow min-h-[50vh]" class="flex flex-col p-2 h-full gap-2 card min-w-0 flex-grow min-h-[50vh]"
> >
<div class="w-full"> <div class="w-full">
{#if activeItem.status != 1} <!-- TODO add item -->
{#if derivedItem.status_id !== null}
<div class="bg-danger text-white p-2 rounded-lg text-lg font-bold"> <div class="bg-danger text-white p-2 rounded-lg text-lg font-bold">
{(ApplicationStatusMaping[ {statusStore.nodesR[derivedItem.status_id].name}
activeItem?.status as (typeof ApplicationStatus)[keyof typeof ApplicationStatus]
] as string) ?? ''}
</div> </div>
{/if} {/if}
{#if showExtraData} {#if showExtraData}
<fieldset class="max-w-full min-w-0 overflow-hidden"> <fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Id</div> <div class="flabel">Id</div>
<div class="finput bg-white w-full break-keep"> <div class="finput bg-white w-full break-keep">
{activeItem.id} {derivedItem.id}
</div> </div>
</fieldset> </fieldset>
<fieldset class="max-w-full min-w-0 overflow-hidden"> <fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Create Time</div> <div class="flabel">Create Time</div>
<div class="finput bg-white w-full break-keep"> <div class="finput bg-white w-full break-keep">
{activeItem.create_time} {derivedItem.create_time}
</div> </div>
</fieldset> </fieldset>
{/if} {/if}
<div class="flex gap-2"> <div class="flex gap-2">
<CompanyField bind:company={activeItem.company} {save} /> <CompanyField index={activeItem} {save} />
<fieldset class="grow"> <fieldset class="grow">
<label class="flabel" for="title">Recruiter</label> <label class="flabel" for="title">Recruiter</label>
<input <input
class="finput" class="finput"
id="title" id="title"
bind:value={activeItem.recruiter} bind:value={applicationStore.all[activeItem].recruiter}
onchange={save} onchange={save}
/> />
</fieldset> </fieldset>
@ -256,7 +235,7 @@
class="finput" class="finput"
id="title" id="title"
type="checkbox" type="checkbox"
bind:checked={activeItem.agency} bind:checked={applicationStore.all[activeItem].agency}
onchange={save} onchange={save}
/> />
</fieldset> </fieldset>
@ -266,7 +245,7 @@
<input <input
class="finput" class="finput"
id="title" id="title"
bind:value={activeItem.title} bind:value={applicationStore.all[activeItem].title}
onchange={save} onchange={save}
/> />
</fieldset> </fieldset>
@ -275,51 +254,36 @@
<input <input
class="finput" class="finput"
id="payrange" id="payrange"
bind:value={activeItem.payrange} bind:value={applicationStore.all[activeItem].payrange}
onchange={save} onchange={save}
/> />
</fieldset> </fieldset>
{#if !activeItem.unique_url || showExtraData} {#if !derivedItem.simple_url || showExtraData}
<fieldset class="max-w-full min-w-0 overflow-hidden"> <fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Url</div> <div class="flabel">Url</div>
<div class="finput bg-white w-full break-keep"> <div class="finput bg-white w-full break-keep">
{activeItem.url} {derivedItem.url}
</div> </div>
</fieldset> </fieldset>
{/if} {/if}
{#if activeItem.unique_url} {#if derivedItem.simple_url}
<fieldset> <fieldset class="max-w-full min-w-0 overflow-hidden">
<div class="flabel">Unique Url</div> <div class="flabel">Simple Url</div>
<div class="finput bg-white"> <div class="finput bg-white w-full break-keep">
{activeItem.unique_url} {derivedItem.simple_url}
</div>
</fieldset>
{/if}
{#if activeItem.linked_application}
<fieldset>
<div class="flabel">Linked Application</div>
<div class="finput bg-white">
{activeItem.linked_application}
</div>
</fieldset>
{/if}
{#if activeItem.application_time}
<fieldset>
<div class="flabel">Application Time</div>
<div class="finput bg-white">
{activeItem.application_time}
</div> </div>
</fieldset> </fieldset>
{/if} {/if}
<div> <div>
<div class="flabel">Tags</div> <div class="flabel">Tags</div>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{#each activeItem.flairs as item} {#each derivedItem.flairs as item}
<Flair <Flair
{item} {item}
allowDelete allowDelete
application={activeItem} application={derivedItem}
updateApplication={(item) => (activeItem = item)} updateApplication={(item) =>
(applicationStore.all[activeItem ?? -1] = item)}
/> />
{/each} {/each}
</div> </div>
@ -329,7 +293,7 @@
<textarea <textarea
class="finput" class="finput"
id="extra" id="extra"
bind:value={activeItem.extra_data} bind:value={applicationStore.all[activeItem].extra_data}
onchange={save} onchange={save}
></textarea> ></textarea>
</fieldset> </fieldset>
@ -338,24 +302,16 @@
<textarea <textarea
class="finput" class="finput"
id="extra" id="extra"
bind:value={activeItem.message} bind:value={applicationStore.all[activeItem].message}
onchange={save} onchange={save}
></textarea> ></textarea>
</fieldset> </fieldset>
{#if activeItem.views.length > 0}
<h1 class="text-sm">Non Loggedin Views Time ({activeItem.views.length})</h1>
{#each activeItem.views as view}
<div>
{view.time}
</div>
{/each}
{/if}
</div> </div>
<div class="flex btns"> <div class="flex btns">
<button <button
class="btn-confirm" class="btn-confirm"
onclick={() => { onclick={() => {
openWindow(activeItem!.url); openWindow(derivedItem!.url);
}} }}
> >
Open Open
@ -378,18 +334,16 @@
Get Ext Data Get Ext Data
</button> </button>
{/if} {/if}
{#if activeItem.original_url == null} <button
<button class="btn-primary" onclick={() => changeUrl.showModal()}> class={derivedItem.simple_url ? 'btn-danger' : 'btn-primary'}
Update Url onclick={() => changeUrl.showModal()}
</button> >
{/if} Update Url
</button>
<div class="px-10"></div> <div class="px-10"></div>
<button class="btn-primary" onclick={() => linkApplication.showModal()}> <button class="btn-primary" onclick={() => linkApplication.showModal()}>
Link Application Link Application
</button> </button>
{#if activeItem.original_url != null}
<button class="btn-danger" onclick={resetUrl}> Reset Url </button>
{/if}
<button <button
class:btn-primary={drag} class:btn-primary={drag}
class:btn-danger={!drag} class:btn-danger={!drag}
@ -405,9 +359,9 @@
🔬 🔬
</button> </button>
</div> </div>
<Timeline application={activeItem} showAll={showExtraData} /> <Timeline application={derivedItem} showAll={showExtraData} />
</div> </div>
<DropingZone bind:activeItem /> <AutoDropZone bind:index={activeItem} />
{:else} {:else}
<div <div
class="p-2 h-full w-full gap-2 flex-grow card grid place-items-center min-h-[50vh]" class="p-2 h-full w-full gap-2 flex-grow card grid place-items-center min-h-[50vh]"
@ -432,32 +386,35 @@
{/if} {/if}
</div> </div>
<ExtractTextDialog id={activeItem?.id ?? ''} bind:dialog={extractTokens} onreload={loadActive} /> <ExtractTextDialog
id={derivedItem?.id ?? ''}
bind:dialog={extractTokens}
onreload={() => activate(derivedItem, false)}
/>
{#if !activeItem?.original_url} {#if derivedItem}
<NewUrlDialog <NewUrlDialog
id={activeItem?.id ?? ''} id={derivedItem?.id ?? ''}
bind:dialog={changeUrl} bind:dialog={changeUrl}
index={activeItem}
onreload={async (item) => { onreload={async (item) => {
item.events = await get(`events/${item.id}`); activate(item, false);
activeItem = item;
}} }}
/> />
{/if} {/if}
{#if activeItem} {#if derivedItem}
<LinkApplication <LinkApplication
application={activeItem} application={derivedItem}
bind:dialog={linkApplication} bind:dialog={linkApplication}
onreload={(item) => (activeItem = item)} onreload={(item) => activate(item, false)}
/> />
{/if} {/if}
<SearchApplication <SearchApplication
application={activeItem} application={derivedItem}
onreload={async (item) => { onreload={async (item) => {
item.events = await get(`events/${item.id}`); activate(item);
activeItem = item;
}} }}
/> />