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
import java.sql.ResultSet
import java.text.SimpleDateFormat
import java.util.Date
import java.util.UUID
import kotlin.collections.emptyList
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.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(
var id: String,
var url: String,
var original_url: String?,
var unique_url: String?,
var title: String,
var user_id: String,
var extra_data: String,
var payrange: String,
var status: Int,
var status_id: String?,
var company: String,
var recruiter: String,
var agency: Boolean,
var message: String,
var linked_application: String,
var status_history: String,
var application_time: String,
var create_time: String,
var simple_url: String,
var flairs: List<Flair>,
var views: List<View>,
var events: List<Event>,
) {
companion object : RowMapper<Application> {
@ -51,23 +57,17 @@ data class Application(
return Application(
rs.getString("id"),
rs.getString("url"),
rs.getString("original_url"),
rs.getString("unique_url"),
rs.getString("title"),
rs.getString("user_id"),
rs.getString("extra_data"),
rs.getString("payrange"),
rs.getInt("status"),
rs.getString("status_id"),
rs.getString("company"),
rs.getString("recruiter"),
rs.getBoolean("agency"),
rs.getString("message"),
rs.getString("linked_application"),
rs.getString("status_history"),
rs.getString("application_time"),
rs.getString("create_time"),
emptyList(),
rs.getString("simple_url"),
emptyList(),
emptyList(),
)
@ -77,9 +77,7 @@ data class Application(
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: Int)
data class StatusRequest(val id: String, val status_id: String?)
data class FlairRequest(val id: String, val text: String)
@ -100,7 +98,6 @@ class ApplicationsController(
val sessionService: SessionService,
val applicationService: ApplicationService,
val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService,
) {
@ -118,7 +115,13 @@ class ApplicationsController(
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 */
@ -133,13 +136,10 @@ class ApplicationsController(
Application(
UUID.randomUUID().toString(),
submit.text,
submit.text,
submit.text,
"New Application",
user.id,
"",
"",
0,
null,
"",
"",
@ -147,9 +147,6 @@ class ApplicationsController(
"",
"",
"",
"",
"",
emptyList(),
emptyList(),
emptyList(),
)
@ -199,7 +196,7 @@ class ApplicationsController(
public fun submitText(
@RequestBody submit: SubmitRequest,
@RequestHeader("token") token: String
): Int {
): List<Application> {
val user = sessionService.verifyTokenThrow(token)
var text = submit.text.replace("=\n", "")
@ -263,23 +260,17 @@ class ApplicationsController(
Application(
UUID.randomUUID().toString(),
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",
user.id,
"",
"",
0,
null,
"",
"",
false,
"",
"",
"",
"",
"",
emptyList(),
if (elm.contains("linkedin")) elm.split("?")[0] else "",
emptyList(),
emptyList(),
)
@ -292,26 +283,46 @@ class ApplicationsController(
print(applications.size)
print(" links\n")
return applications.size
return applications
}
@PostMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun list(
@RequestBody info: ListRequest,
@GetMapping(path = ["/list"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun list(@RequestHeader("token") token: String): List<Application> {
val user = sessionService.verifyTokenThrow(token)
return applicationService.findAll(user)
}
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun get(@PathVariable id: String, @RequestHeader("token") token: String): Application? {
val user = sessionService.verifyTokenThrow(token)
val app = applicationService.findApplicationById(user, id)
if (app == null) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Application not found", null)
}
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
): List<Application> {
): Application {
val user = sessionService.verifyTokenThrow(token)
return applicationService.findAll(user, info)
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)
}
@GetMapping(path = ["/active"], produces = [MediaType.APPLICATION_JSON_VALUE])
public fun active(@RequestHeader("token") token: String): Application? {
val user = sessionService.verifyTokenThrow(token)
val possibleApplications = applicationService.findAll(user, ListRequest(1))
if (possibleApplications.size == 0) {
return null
}
return applicationService.findApplicationById(user, possibleApplications[0].id)
applicationService.linkApplications(toLinkApp, app)
applicationService.delete(toLinkApp)
return app
}
@PutMapping(path = ["/status"], produces = [MediaType.APPLICATION_JSON_VALUE])
@ -325,11 +336,11 @@ class ApplicationsController(
throw NotFound()
}
if (application.status == info.status) {
return application;
if (application.status_id == info.status_id) {
return application
}
application.status = info.status
application.status_id = info.status_id
applicationService.updateStatus(application)
@ -362,68 +373,28 @@ class ApplicationsController(
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.unique_url = info.url.split("?")[0]
application.simple_url = info.url.split("?")[0]
var maybe_exists =
applicationService.findApplicationByUrl(
user,
application.url,
application.unique_url
)
if (maybe_exists != null) {
applicationService.findApplicationByUrl(user, application.url)
?: applicationService.findApplicationByUrl(user, application.simple_url)
if (maybe_exists != null && maybe_exists.id != application.id) {
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.events = eventService.listFromApplicationId(maybe_exists.id).toList()
return maybe_exists
}
applicationService.addUrl(application.id, info.url)
applicationService.addUrl(application.id, info.url.split("?")[0])
applicationService.update(application)
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
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)
application.events = eventService.listFromApplicationId(application.id).toList()
return application
}
@ -466,28 +437,13 @@ class ApplicationsController(
class ApplicationService(
val db: JdbcTemplate,
val flairService: FlairService,
val viewService: ViewService,
val eventService: EventService,
) {
public fun findApplicationByUrl(user: UserDb, url: String, unique_url: String?): Application? {
if (unique_url != null) {
val unique: List<Application> =
public fun findApplicationByUrl(user: UserDb, url: String): Application? {
val applications =
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(
"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),
Application
)
@ -516,7 +472,6 @@ class ApplicationService(
var application = applications[0]
application.flairs = flairService.listFromLinkApplicationId(application.id)
application.views = viewService.listFromApplicationId(application.id)
application.events = eventService.listFromApplicationId(application.id).toList()
return application
@ -538,40 +493,51 @@ class ApplicationService(
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 {
if (this.findApplicationByUrl(user, application.url, application.unique_url) != null) {
if (this.findApplicationByUrl(user, application.url) != null) {
return false
}
// Create time is auto created by the database
// The default status is null
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.url,
application.original_url,
application.unique_url,
application.title,
application.user_id,
application.extra_data,
application.payrange,
application.status,
application.status_id,
application.company,
application.recruiter,
application.message,
application.linked_application,
application.status_history,
application.application_time,
application.agency,
application.simple_url,
)
eventService.create(application.id, EventType.Creation)
addUrl(application.id, application.url)
return true
}
private fun internalFindAll(user: UserDb, info: ListRequest): Iterable<Application> {
if (info.status == null) {
private fun internalFindAll(user: UserDb): Iterable<Application> {
return db.query(
"select * from applications where user_id=? order by title asc;",
arrayOf(user.id),
@ -579,30 +545,8 @@ class ApplicationService(
)
}
// 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(
"select * from applications where user_id=? and status=? order by title asc;",
arrayOf(user.id, info.status),
Application,
)
}
public fun findAll(user: UserDb, info: ListRequest): List<Application> {
var iter = internalFindAll(user, info);
if (info.views == true) {
iter = iter.map {
it.views = viewService.listFromApplicationId(it.id)
it
}
}
public fun findAll(user: UserDb): List<Application> {
var iter = internalFindAll(user)
return iter.toList()
}
@ -618,26 +562,11 @@ class ApplicationService(
// TODO how status history works
public fun updateStatus(application: Application): Application {
val status_string = "${application.status}"
var status_history = application.status_history.split(",").filter { it.length >= 1 }
if (status_history.indexOf(status_string) == -1) {
status_history = status_history.plus(status_string)
}
application.status_history = status_history.joinToString(",") { it }
if (application.status == 4) {
val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
application.application_time = sdf.format(Date())
}
eventService.create(application.id, EventType.StatusUpdate, application.status)
eventService.create(application.id, EventType.StatusUpdate, application.status_id)
db.update(
"update applications set status=?, status_history=?, application_time=? where id=?",
application.status,
application.status_history,
application.application_time,
"update applications set status_id=? where id=?",
application.status_id,
application.id,
)
return application
@ -647,10 +576,8 @@ class ApplicationService(
public fun update(application: Application): Application {
// I don't want ot update create_time
db.update(
"update applications set url=?, original_url=?, unique_url=?, title=?, user_id=?, extra_data=?, payrange=?, 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.original_url,
application.unique_url,
application.title,
application.user_id,
application.extra_data,
@ -658,17 +585,29 @@ class ApplicationService(
application.company,
application.recruiter,
application.message,
application.linked_application,
application.agency,
application.simple_url,
application.id,
)
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) {
db.update(
"delete from applications where 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 application_id: String,
var event_type: Int,
var new_status: Int?,
var new_status_id: String?,
var time: Timestamp
) {
companion object : RowMapper<Event> {
@ -34,7 +34,7 @@ data class Event(
rs.getString("id"),
rs.getString("application_id"),
rs.getInt("event_type"),
rs.getInt("new_status"),
rs.getString("new_status_id"),
rs.getTimestamp("time"),
)
}
@ -93,23 +93,19 @@ public class EventService(val db: JdbcTemplate) {
public fun create(
application_id: String,
event_type: EventType,
new_status: Int? = null
new_status_id: String? = null
): Event {
val id = UUID.randomUUID().toString()
if (event_type == EventType.StatusUpdate && new_status == null) {
throw Exception("When event_type == StatusUpdate new_status must be set")
}
var new_event =
Event(id, application_id, event_type.value, new_status, Timestamp(Date().getTime()))
Event(id, application_id, event_type.value, new_status_id, Timestamp(Date().getTime()))
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.application_id,
new_event.event_type,
new_event.new_status,
new_event.new_status_id,
)
return new_event

View File

@ -2,8 +2,8 @@ package com.andr3h3nriqu3s.applications
import java.sql.ResultSet
import java.util.UUID
import org.springframework.http.MediaType
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.stereotype.Service
@ -52,10 +52,6 @@ class FlairController(
throw NotFound()
}
if (old_flair.user_id != user.id) {
throw NotAuth()
}
flair.user_id = old_flair.user_id
return flairService.updateFlair(flair)
@ -88,6 +84,8 @@ data class SimpleFlair(
val name: String,
val description: String,
val color: String,
val sort: Int,
val showFullDescription: Int,
)
data class Flair(
@ -97,6 +95,8 @@ data class Flair(
var name: String,
var expr: String,
var description: String,
var sort: Int,
var showFullDescription: Int,
) {
companion object : RowMapper<Flair> {
override public fun mapRow(rs: ResultSet, rowNum: Int): Flair {
@ -107,12 +107,20 @@ data class Flair(
rs.getString("name"),
rs.getString("expr"),
rs.getString("description"),
rs.getInt("sort"),
rs.getInt("showFullDescription"),
)
}
}
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> =
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),
Flair
)
@ -210,12 +218,14 @@ public class FlairService(val db: JdbcTemplate) {
public fun updateFlair(flair: Flair): Flair {
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.color,
flair.name,
flair.expr,
flair.description,
flair.sort,
flair.showFullDescription,
flair.id,
)
@ -231,16 +241,18 @@ public class FlairService(val db: JdbcTemplate) {
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(
"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.user_id,
new_flair.color,
new_flair.name,
new_flair.expr,
new_flair.description
new_flair.description,
new_flair.showFullDescription,
new_flair.sort,
)
return new_flair

View File

@ -28,7 +28,9 @@ data class UserStatusNode(
var y: Int,
var width: Int,
var height: Int,
var permission: Int
var permission: Int,
var visible: Boolean,
var endable: Boolean,
) {
companion object : RowMapper<UserStatusNode> {
override public fun mapRow(rs: ResultSet, rowNum: Int): UserStatusNode {
@ -42,6 +44,8 @@ data class UserStatusNode(
rs.getInt("width"),
rs.getInt("height"),
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 {
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.icon,
node.x,
@ -219,6 +223,8 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.width,
node.height,
node.permission,
node.visible,
node.endable,
node.id,
)
@ -230,7 +236,7 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.id = id
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.user_id,
node.name,
@ -239,7 +245,9 @@ public class UserStatusNodeService(val db: JdbcTemplate) {
node.y,
node.width,
node.height,
node.permission
node.permission,
node.visible,
node.endable,
)
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

@ -6,37 +6,27 @@ CREATE TABLE IF NOT EXISTS users (
level INT NOT NULL
);
CREATE TABLE IF NOT EXISTS tokens (
token VARCHAR PRIMARY KEY,
user_id VARCHAR
);
CREATE TABLE IF NOT EXISTS tokens (token VARCHAR PRIMARY KEY, user_id VARCHAR);
create table if not exists applications (
id text primary key,
url text not null,
original_url text,
unique_url text,
simple_url text not null,
company text,
recruiter text,
title text,
mesasge text default '',
status_history text default '',
user_id text,
extra_data text,
-- this status will be deprecated in favor of the node style status
status integer,
status_id text default null,
linked_application text default '',
application_time text default '',
agency boolean default false,
create_time timestamp default now()
create_time timestamp default now ()
);
-- Views are deprecated will be removed in the future
create table if not exists views (
id text primary key,
application_id text not null,
time timestamp default now()
create table if not exists applications_urls (
application_id text,
url text not null,
primary key (application_id, url)
);
create table if not exists flair (
@ -45,7 +35,9 @@ create table if not exists flair (
color text default '#ff0000',
name text default 'New 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 (
@ -65,17 +57,14 @@ create table if not exists events (
-- StatusUpdate(1),
-- Page(2)
event_type integer not null,
-- This only matters when event_type == 1
new_status integer,
time timestamp default now()
new_status_id text,
time timestamp default now ()
);
--
-- User Controlled Status
--
create table if not exists user_status_node (
id text primary key,
user_id text not null,
@ -85,25 +74,23 @@ create table if not exists user_status_node (
y integer default 0,
width 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 (
id text primary key,
-- You technically can get this by loking a the source and target nodes but that seams more complicated
user_id text not null,
-- This can be null because null means creation
source_node text,
-- This can be null because null means creation
target_node text,
source_x integer default 0,
source_y integer default 0,
target_x integer default 0,
target_y integer default 0,
-- If this link is bidiretoral
bi boolean
);

View File

@ -1,138 +1,67 @@
import type { Flair } from './FlairStore.svelte';
import { post } from './utils';
import { get } from './utils';
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({
Creation: 0,
StatusUpdate: 1,
View: 2,
View: 2
});
export type ApplicationEvent = {
id: string,
application_id: string,
event_type: AsEnum<typeof EventType>,
new_status: number,
time: string
}
id: string;
application_id: string;
event_type: AsEnum<typeof EventType>;
new_status_id: string;
time: string;
};
export type Application = {
id: string;
url: string;
original_url: string | null;
unique_url: string | null;
simple_url: string;
title: string;
user_id: string;
extra_data: string;
payrange: string;
status: AsEnum<typeof ApplicationStatus>;
status_id: string;
recruiter: string;
agency: boolean;
company: string;
message: string;
linked_application: string;
application_time: string;
create_time: string;
status_history: string;
flairs: Flair[];
views: View[];
events: ApplicationEvent[];
};
function createApplicationStore() {
let applications: Application[] = $state([]);
let applyed: Application[] = $state([]);
let all: Application[] = $state([]);
let dragApplication: Application | undefined = $state(undefined);
let loadItem: Application | undefined = $state(undefined);
let loadItemOpen = $state(true);
let req = false;
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}
*/
async loadAll(force = false) {
if (req) return;
if (!force && all.length > 1) {
return;
}
all = await post('application/list', {});
},
clear() {
applications = [];
req = true;
try {
all = await get('application/list');
} finally {
req = false;
}
},
dragStart(application: Application | undefined) {
@ -146,28 +75,53 @@ function createApplicationStore() {
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() {
return dragApplication;
},
get applications() {
return applications;
},
get applyed() {
return applyed;
},
get 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() {
return loadItem;
},
set loadItem(item: Application | undefined) {
loadItem = item;
loadItemOpen = true;
},
get loadItemOpen() {
return loadItemOpen;
},
set loadItemOpen(_loadItemOpen: boolean) {
loadItemOpen = _loadItemOpen;
}
};
}

View File

@ -7,6 +7,8 @@ export type Flair = {
name: string;
expr: string;
description: string;
showFullDescription: number;
sort: number;
};
function 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 ApplicationsList from './ApplicationsList.svelte';
import WorkArea from './work-area/WorkArea.svelte';
import AppliyedList from './AppliyedList.svelte';
import PApplicationList from './PApplicationList.svelte';
import { ApplicationStatus } from '$lib/ApplicationsStore.svelte';
import ApplicationTypesList from './ApplicationTypesList.svelte';
</script>
<HasUser redirect="/cv">
@ -16,22 +14,8 @@
<ApplicationsList />
<WorkArea />
</div>
<PApplicationList status={ApplicationStatus.FinalInterview}>
Interview Final
</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 />
<ApplicationTypesList />
<div class="p-3"></div>
</div>
</div>
</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('');
onMount(() => {
applicationStore.loadApplications();
applicationStore.loadAll();
});
let apps = $derived(
applicationStore.all.filter((i) => i.status_id === null && !i.linked_application)
);
let internal = $derived(
applicationStore.applications.filter((i) => {
apps.filter((i) => {
if (!filter) {
return true;
}
@ -47,10 +51,7 @@
}}
role="none"
>
<div
class="max-w-full"
class:animate-pulse={applicationStore.dragging?.id === item.id}
>
<div 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">
<div class="flex-grow max-w-[90%]">
<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="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('/submit')}>
Submit text
<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('/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 { get } from '$lib/utils';
import { onMount } from 'svelte';
import ApplicationsList from '../ApplicationsList.svelte';
import Pill from './pill.svelte';
let id: string | undefined | null = $state(undefined);
@ -18,13 +18,15 @@
name: string;
description: string;
color: string;
sort: number;
showFullDescription: number;
};
type Application = {
recruiter: string;
message: string;
company: string;
agency: boolean,
agency: boolean;
flairs: SimpleFlair[];
};
@ -40,16 +42,19 @@
}
application.flairs.sort((a, b) => {
if (a.description && b.description) {
return 0;
const aDesc = a.description && a.showFullDescription === 1;
const bDesc = b.description && b.showFullDescription === 1;
if (aDesc && bDesc) {
return b.sort - a.sort;
}
if (a.description) {
if (aDesc) {
return -1;
}
if (b.description) {
if (bDesc) {
return 1;
}
return 0;
return b.sort - a.sort;
});
loadFlairs();
@ -82,7 +87,7 @@
</svelte:head>
<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="w-full flex">
<h1 class="text-black text-5xl">Andre Henriques</h1>
@ -113,7 +118,7 @@
</ul>
</div>
</div>
<div class="w-full flex py-9">
<div class="w-full flex py-3 print:pb-0">
<div>
I am a dedicated and versatile programmer with four years of professional
experience. <br />
@ -128,9 +133,10 @@
</div>
</div>
</div>
{#if application}
{#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
{#if application.recruiter}
<span class="font-bold">{application.recruiter}</span> @
@ -139,6 +145,8 @@
recruiter @ <span class="font-bold">{application.company}</span>
{/if}
</h2>
{:else}
<div class="p-5 print:hidden"></div>
{/if}
{#if application.message}
@ -153,41 +161,44 @@
{#if application.flairs.length > 0}
<div class="p-3 bg-white w-[190mm] rounded-lg">
<h1 class="flex gap-5 items-end">
Your Ad & My skills {#if flairs.length > 0}<input
placeholder="Search other skills!"
Skills {#if flairs.length > 0}<input
placeholder="Click here to search skills!"
class="flex-grow text-blue-500 print:hidden"
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}
</h1>
<div class="flex flex-wrap gap-2 py-2">
<div class="flex flex-wrap gap-2 py-2 print:py-0">
{#if otherSearch === ''}
{#each application.flairs as flair}
<div class="min-w-0 {flair.description ? 'flex-grow w-full' : ''}">
{#if flair.description}
{@const hasDesc = flair.description && flair.showFullDescription === 1}
<div class="min-w-0 {hasDesc ? 'flex-grow w-full' : ''}">
{#if hasDesc}
<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};"
>
<div
class="rounded-lg print:p-2 print:inline-block"
style="background: {flair.color}; print-color-adjust: exact !important;"
<Pill
text-size="text-base print:text-sm"
class="print:inline-block text-2xl py-0 mb-2"
color={flair.color}
>
{flair.name}
</div>
</Pill>
<span class="hidden print:inline">:</span>
<div class="bg-white my-1 print:inline p-1 rounded-md">
{flair.description}
</div>
</div>
{:else}
<div
class="p-2 rounded-lg forced-color-adjust-none"
style="background: {flair.color}; print-color-adjust: exact !important;"
>
<Pill text-size="text-base print:text-sm" color={flair.color}>
{flair.name}
</div>
</Pill>
{/if}
</div>
{/each}
@ -234,20 +245,18 @@
</div>
{/if}
{/if}
<div class="p-2"></div>
<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
</h2>
</div>
<div class="p-2"></div>
<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>
<div class="ml-5">
<h2>4 years - May 2020 - Present</h2>
<div class="ml-5">
<h3>Developed various projects:</h3>
<ul class="pl-5 list-disc">
<li>Developing various websites using React and Svelte.</li>
@ -255,30 +264,20 @@
<li>Implemented an ORM system using Java Reflection</li>
<li>Implemented automatic deployment with GitLab CI/CD tools.</li>
<li>Linux Server Administration</li>
<li>Technologies used: React, WebRTC, WebSockets, Rest, Google Maps AP</li>
</ul>
</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="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>
<h1>Associate Devops Engineer @ Sky UK</h1>
<div class="ml-5">
<h2>1 year - July 2022 - June 2023</h2>
<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>
<div class="ml-2">
<ul class="pl-5 list-disc">
<li>Developed web-based tools for the DevOps team to use</li>
<li>
@ -298,13 +297,15 @@
</div>
</div>
<div class="p-5"></div>
<div class="bg-white p-3 text-black rounded-lg w-[190mm]">
<h2 class="pb-2 text-3xl">Education</h2>
<div class="w-[190mm]">
<h2 class="p-3 print:p-0 text-4xl print:text-xl font-bold print:font-normal text-white">
Education
</h2>
</div>
<div class="bg-white p-2 text-black rounded-lg w-[190mm] print:text-sm">
<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">
<h2>July 2020 - June 2024</h2>
</div>
@ -312,6 +313,8 @@
</div>
</div>
<div class="p-3 print:hidden"></div>
<!--div class="p-5"></div>
<div>TODO: Previous projetcs</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>
<input required type="color" id="color" bind:value={edit.color} />
</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>
<label class="flabel" for="name">Name</label>
<input required id="name" class="finput w-full" bind:value={edit.name} />
@ -106,12 +118,15 @@
<fieldset>
<label class="flabel" for="description">Description</label>
<textarea
required
id="description"
class="finput w-full"
bind:value={edit.description}
></textarea>
</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">
<button class="btn-confirm">Save</button>
</div>

View File

@ -8,11 +8,6 @@
import { extractLinkNodePosX, extractLinkNodePosY } from './types';
import { deleteR, get, post, put } from '$lib/utils';
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
@ -47,7 +42,8 @@
height: 4,
name: 'Created',
icon: 'plus',
permission: 1
permission: 1,
visible: false
} as Node,
...nodesRequest
];
@ -72,7 +68,7 @@
y: a.target_y
}
}));
console.log("test", linkRequests)
console.log('test', linkRequests);
} catch (e) {
console.log('TODO inform user', e);
}
@ -191,7 +187,8 @@
width,
icon: icon_list[Math.floor(Math.random() * icon_list.length)],
name: 'New Status',
permission: 0
permission: 0,
visible: true
} as Node);
nodes.push(result);
// Tell svelte that nodes is updated
@ -422,12 +419,15 @@
try {
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 = nodes;
} catch (e) {
console.log("TODO inform the user", e)
console.log('TODO inform the user', e);
}
}}
onNodePointClick={async (inNodeX, inNodeY) => {

View File

@ -94,6 +94,43 @@
>
<div class="bi bi-x mt-[2px]"></div>
</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}
</div>

View File

@ -1,6 +1,6 @@
export type Node = {
id: string,
user_id: string,
id: string;
user_id: string;
x: number;
y: number;
width: number;
@ -9,37 +9,42 @@ export type Node = {
icon: string;
// 1 disables editing
permission: number;
visible: boolean;
endable: boolean;
};
export type LinkNode = {
node: Node;
x: number;
y: number;
}
};
export type FullLink = {
sourceNode: LinkNode;
targetNode: LinkNode;
bi?: boolean;
}
};
export type FullLinkApi = {
id: string,
user_id: string,
target_node: string | null,
source_node: string | null,
bi: boolean,
source_x: number,
source_y: number,
target_x: number,
target_y: number
}
id: string;
user_id: string;
target_node: string | null;
source_node: string | null;
bi: boolean;
source_x: number;
source_y: number;
target_x: number;
target_y: number;
};
export type ActionType = undefined | 'drag' | 'create' | 'move' | 'link';
export function extractLinkNodePosX(canvasX: (x: number) => number, node: LinkNode, grid_size: number) {
export function extractLinkNodePosX(
canvasX: (x: number) => number,
node: LinkNode,
grid_size: number
) {
if (node.x === -1) {
return canvasX(node.node.x);
}
@ -49,7 +54,11 @@ export function extractLinkNodePosX(canvasX: (x: number) => number, node: LinkNo
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(
canvasY: (x: number) => number,
node: LinkNode,
grid_size: number
) {
if (node.y === -1) {
return canvasY(node.node.y);
}

View File

@ -11,7 +11,7 @@
async function submit() {
try {
await post('application/text', text);
applicationStore.loadApplications(true);
applicationStore.loadAll(true);
goto('/');
} catch (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">
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 fcomps = $derived(
company === ''
applicationStore.all[index].company === ''
? []
: [...companies.values()].filter((a) => {
// 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>
<fieldset class="grow">
<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">
{#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}
</button>
{/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();
} catch (e) {
// Show message to the user
console.log(e);
console.log('TODO inform the user', e);
}
}
</script>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { ApplicationStatus, ApplicationStatusMaping, type Application } from '$lib/ApplicationsStore.svelte';
import { post, put } from '$lib/utils';
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { statusStore } from '$lib/Types.svelte';
import { post } from '$lib/utils';
let {
application,
@ -13,43 +14,21 @@
} = $props();
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) {
try {
application.linked_application = item.id;
await put('application/update', application);
await put('application/status', {
id: application.id,
status: ApplicationStatus.LinkedApplication
});
await post(`application/link/application/${application.id}/${item.id}`, {});
applicationStore.removeByID(application.id);
dialog.close();
onreload(item);
} catch (e) {
// Show message to the user
// TODO: Show message to the user
console.log(e);
}
}
</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>
{applications.length}
</div>
</div>
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
{#each applications.filter((i) => {
let internal = $derived(
applicationStore.all.filter((i) => {
if (!filter) {
return true;
}
@ -62,7 +41,19 @@
}
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">
<button class="text-left max-w-full" type="button" onclick={() => submit(item)}>
<h2 class="text-lg text-blue-500">
@ -74,7 +65,7 @@
{/if}
</h2>
<span>
{ApplicationStatusMaping[item.status]}
{statusStore.nodesR[item.status_id].name}
</span>
</button>
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { Application } from '$lib/ApplicationsStore.svelte';
import { applicationStore, type Application } from '$lib/ApplicationsStore.svelte';
import { post, preventDefault } from '$lib/utils';
let {
const {
onreload
}: {
onreload: (item: Application) => void;
@ -12,20 +12,50 @@
let link = $state('');
let dialogAddMultiple = $state(false);
let inputField: HTMLTextAreaElement;
async function createApplication() {
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', {
text: link
});
applicationStore.all.push(r);
onreload(r);
dialogElement.close()
dialogElement.close();
link = '';
} catch (e) {
console.log('Inform the user', e);
console.log('TODO: Inform the user', e);
}
}
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();
e.stopPropagation();
e.preventDefault();
@ -41,11 +71,34 @@
});
</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)}>
<fieldset>
<label class="flabel" for="title">Link</label>
<label class="flabel" for="title"
>{#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>
<div class="flex justify-end">
{#if dialogAddMultiple}
<button type="submit" class="btn-primary">Submit</button>
{/if}
</div>
</form>
</dialog>

View File

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

View File

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

View File

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

View File

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