Version 3 #40
|
@ -114,7 +114,7 @@ module EmploymentHistory =
|
||||||
|
|
||||||
let empty =
|
let empty =
|
||||||
{ Employer = ""
|
{ Employer = ""
|
||||||
StartDate = LocalDate.MinIsoValue
|
StartDate = LocalDate.FromDateTime DateTime.Today
|
||||||
EndDate = None
|
EndDate = None
|
||||||
Position = None
|
Position = None
|
||||||
Description = None
|
Description = None
|
||||||
|
|
|
@ -3,27 +3,6 @@ module JobsJobsJobs.Profiles.Domain
|
||||||
open JobsJobsJobs.Domain
|
open JobsJobsJobs.Domain
|
||||||
open NodaTime
|
open NodaTime
|
||||||
|
|
||||||
/// The fields required for a skill
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
|
||||||
type SkillForm =
|
|
||||||
{ Description : string
|
|
||||||
|
|
||||||
/// Notes regarding the skill
|
|
||||||
Notes : string
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Functions to support skill forms
|
|
||||||
module SkillForm =
|
|
||||||
|
|
||||||
/// Create a skill form from a skill
|
|
||||||
let fromSkill (skill : Skill) =
|
|
||||||
{ SkillForm.Description = skill.Description; Notes = defaultArg skill.Notes "" }
|
|
||||||
|
|
||||||
/// Create a skill from a skill form
|
|
||||||
let toSkill (form : SkillForm) =
|
|
||||||
{ Skill.Description = form.Description; Notes = if form.Notes = "" then None else Some form.Notes }
|
|
||||||
|
|
||||||
|
|
||||||
/// The data required to update a profile
|
/// The data required to update a profile
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type EditProfileForm =
|
type EditProfileForm =
|
||||||
|
@ -80,6 +59,48 @@ module EditProfileForm =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The form used to add or edit employment history entries
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type HistoryForm =
|
||||||
|
{ /// The name of the employer
|
||||||
|
Employer : string
|
||||||
|
|
||||||
|
StartDate : string
|
||||||
|
|
||||||
|
EndDate : string
|
||||||
|
|
||||||
|
Position : string
|
||||||
|
|
||||||
|
Description : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Support functions for the employment history form
|
||||||
|
module HistoryForm =
|
||||||
|
|
||||||
|
open System
|
||||||
|
|
||||||
|
/// The date format expected by the browser's date input field
|
||||||
|
let dateFormat = Text.LocalDatePattern.CreateWithInvariantCulture "yyyy-MM-dd"
|
||||||
|
|
||||||
|
/// Create an employment history form from an employment history entry
|
||||||
|
let fromHistory (history : EmploymentHistory) =
|
||||||
|
{ Employer = history.Employer
|
||||||
|
StartDate = dateFormat.Format history.StartDate
|
||||||
|
EndDate = match history.EndDate with Some dt -> dateFormat.Format dt | None -> ""
|
||||||
|
Position = defaultArg history.Position ""
|
||||||
|
Description = match history.Description with Some d -> MarkdownString.toString d | None -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an employment history entry from an employment history form
|
||||||
|
let toHistory (history : HistoryForm) : EmploymentHistory =
|
||||||
|
{ Employer = history.Employer
|
||||||
|
StartDate = (dateFormat.Parse history.StartDate).Value
|
||||||
|
EndDate = if history.EndDate = "" then None else Some (dateFormat.Parse history.EndDate).Value
|
||||||
|
Position = if history.Position = "" then None else Some history.Position
|
||||||
|
Description = if history.Description = "" then None else Some (Text history.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The various ways profiles can be searched
|
/// The various ways profiles can be searched
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type ProfileSearchForm =
|
type ProfileSearchForm =
|
||||||
|
@ -166,3 +187,23 @@ type PublicSearchResult =
|
||||||
Skills : string list
|
Skills : string list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The fields required for a skill
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type SkillForm =
|
||||||
|
{ Description : string
|
||||||
|
|
||||||
|
/// Notes regarding the skill
|
||||||
|
Notes : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Functions to support skill forms
|
||||||
|
module SkillForm =
|
||||||
|
|
||||||
|
/// Create a skill form from a skill
|
||||||
|
let fromSkill (skill : Skill) =
|
||||||
|
{ SkillForm.Description = skill.Description; Notes = defaultArg skill.Notes "" }
|
||||||
|
|
||||||
|
/// Create a skill from a skill form
|
||||||
|
let toSkill (form : SkillForm) =
|
||||||
|
{ Skill.Description = form.Description; Notes = if form.Notes = "" then None else Some form.Notes }
|
||||||
|
|
|
@ -108,6 +108,13 @@ let skills : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
| None -> return! notFound ctx
|
| None -> return! notFound ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /profile/edit/skills/list
|
||||||
|
let skillList : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile -> return! Views.skillTable profile.Skills None (csrf ctx) |> renderBare next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET: /profile/edit/skill/[idx]
|
// GET: /profile/edit/skill/[idx]
|
||||||
let editSkill idx : HttpHandler = requireUser >=> fun next ctx -> task {
|
let editSkill idx : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
match! Data.findById (currentCitizenId ctx) with
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
@ -146,6 +153,59 @@ let deleteSkill idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ct
|
||||||
| None -> return! notFound ctx
|
| None -> return! notFound ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /profile/edit/history
|
||||||
|
let history : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile ->
|
||||||
|
return! Views.history profile.History (csrf ctx) |> render "Employment History | Employment Profile" next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: /profile/edit/history/list
|
||||||
|
let historyList : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile -> return! Views.historyTable profile.History None (csrf ctx) |> renderBare next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: /profile/edit/history/[idx]
|
||||||
|
let editHistory idx : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile ->
|
||||||
|
if idx < -1 || idx >= List.length profile.History then return! notFound ctx
|
||||||
|
else return! Views.editHistory profile.History idx (csrf ctx) |> renderBare next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: /profile/edit/history/[idx]
|
||||||
|
let saveHistory idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile ->
|
||||||
|
if idx < -1 || idx >= List.length profile.History then return! notFound ctx
|
||||||
|
else
|
||||||
|
let! form = ctx.BindFormAsync<HistoryForm> ()
|
||||||
|
let entry = HistoryForm.toHistory form
|
||||||
|
let history =
|
||||||
|
if idx = -1 then entry :: profile.History
|
||||||
|
else profile.History |> List.mapi (fun histIdx it -> if histIdx = idx then entry else it)
|
||||||
|
|> List.sortByDescending (fun it -> defaultArg it.EndDate NodaTime.LocalDate.MaxIsoValue)
|
||||||
|
do! Data.save { profile with History = history }
|
||||||
|
return! Views.historyTable history None (csrf ctx) |> renderBare next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: /profile/edit/history/[idx]/delete
|
||||||
|
let deleteHistory idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||||
|
match! Data.findById (currentCitizenId ctx) with
|
||||||
|
| Some profile ->
|
||||||
|
if idx < 0 || idx >= List.length profile.History then return! notFound ctx
|
||||||
|
else
|
||||||
|
let history = profile.History |> List.indexed |> List.filter (fun it -> fst it <> idx) |> List.map snd
|
||||||
|
do! Data.save { profile with History = history }
|
||||||
|
return! Views.historyTable history None (csrf ctx) |> renderBare next ctx
|
||||||
|
| None -> return! notFound ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET: /profile/[id]/view
|
// GET: /profile/[id]/view
|
||||||
let view citizenId : HttpHandler = fun next ctx -> task {
|
let view citizenId : HttpHandler = fun next ctx -> task {
|
||||||
let citId = CitizenId citizenId
|
let citId = CitizenId citizenId
|
||||||
|
@ -167,18 +227,24 @@ open Giraffe.EndpointRouting
|
||||||
let endpoints =
|
let endpoints =
|
||||||
subRoute "/profile" [
|
subRoute "/profile" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O/view" view
|
routef "/%O/view" view
|
||||||
route "/edit" edit
|
route "/edit" edit
|
||||||
route "/edit/general" editGeneralInfo
|
route "/edit/general" editGeneralInfo
|
||||||
routef "/edit/skill/%i" editSkill
|
routef "/edit/history/%i" editHistory
|
||||||
route "/edit/skills" skills
|
route "/edit/history" history
|
||||||
route "/search" search
|
route "/edit/history/list" historyList
|
||||||
route "/seeking" seeking
|
routef "/edit/skill/%i" editSkill
|
||||||
|
route "/edit/skills" skills
|
||||||
|
route "/edit/skills/list" skillList
|
||||||
|
route "/search" search
|
||||||
|
route "/seeking" seeking
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
route "/delete" delete
|
route "/delete" delete
|
||||||
routef "/edit/skill/%i" saveSkill
|
routef "/edit/history/%i" saveHistory
|
||||||
routef "/edit/skill/%i/delete" deleteSkill
|
routef "/edit/history/%i/delete" deleteHistory
|
||||||
route "/save" saveGeneralInfo
|
routef "/edit/skill/%i" saveSkill
|
||||||
|
routef "/edit/skill/%i/delete" deleteSkill
|
||||||
|
route "/save" saveGeneralInfo
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -134,19 +134,15 @@ let editGeneralInfo (m : EditProfileForm) continents csrf =
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// Render the skill edit template and existing skills
|
/// Render the skill edit template
|
||||||
let skillForm (m : SkillForm) isNew =
|
let skillForm (m : SkillForm) isNew =
|
||||||
[ h4 [] [ txt $"""{if isNew then "Add a" else "Edit"} Skill""" ]
|
[ h4 [] [ txt $"""{if isNew then "Add a" else "Edit"} Skill""" ]
|
||||||
div [ _class "col-12 col-md-6" ] [
|
div [ _class "col-12 col-md-6" ] [
|
||||||
div [ _class "form-floating" ] [
|
textBox [ _type "text"; _maxlength "200"; _autofocus ] (nameof m.Description) m.Description "Skill" true
|
||||||
textBox [ _type "text"; _maxlength "200"; _autofocus ] (nameof m.Description) m.Description "Skill" true
|
|
||||||
]
|
|
||||||
div [ _class "form-text" ] [ txt "A skill (language, design technique, process, etc.)" ]
|
div [ _class "form-text" ] [ txt "A skill (language, design technique, process, etc.)" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-6" ] [
|
div [ _class "col-12 col-md-6" ] [
|
||||||
div [ _class "form-floating" ] [
|
textBox [ _type "text"; _maxlength "1000" ] (nameof m.Notes) m.Notes "Notes" false
|
||||||
textBox [ _type "text"; _maxlength "1000" ] (nameof m.Notes) m.Notes "Notes" false
|
|
||||||
]
|
|
||||||
div [ _class "form-text" ] [ txt "A further description of the skill" ]
|
div [ _class "form-text" ] [ txt "A further description of the skill" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
|
@ -231,6 +227,120 @@ let skills (skills : Skill list) csrf =
|
||||||
let editSkill (skills : Skill list) idx csrf =
|
let editSkill (skills : Skill list) idx csrf =
|
||||||
skillTable skills (Some idx) csrf
|
skillTable skills (Some idx) csrf
|
||||||
|
|
||||||
|
open NodaTime
|
||||||
|
|
||||||
|
/// Render the employment history edit template
|
||||||
|
let historyForm (m : HistoryForm) isNew =
|
||||||
|
let maxDate = HistoryForm.dateFormat.Format (LocalDate.FromDateTime System.DateTime.Today)
|
||||||
|
[ h4 [] [ txt $"""{if isNew then "Add" else "Edit"} Employment History""" ]
|
||||||
|
div [ _class "col-12 col-md-6" ] [
|
||||||
|
textBox [ _type "text"; _maxlength "200"; _autofocus ] (nameof m.Employer) m.Employer "Employer" true
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6" ] [
|
||||||
|
textBox [ _type "text"; _maxlength "200" ] (nameof m.Position) m.Position "Title or Position" true
|
||||||
|
]
|
||||||
|
p [ _class "col-12 text-center" ] [
|
||||||
|
txt "Select any date within the month; only the month and year will be displayed"
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 col-xl-4 offset-xl-1" ] [
|
||||||
|
textBox [ _type "date"; _max maxDate ] (nameof m.StartDate) m.StartDate "Start Date" true
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-6 col-xl-4 offset-xl-2" ] [
|
||||||
|
textBox [ _type "date"; _max maxDate ] (nameof m.EndDate) m.EndDate "End Date" false
|
||||||
|
]
|
||||||
|
markdownEditor [] (nameof m.Description) m.Description "Description"
|
||||||
|
div [ _class "col-12" ] [
|
||||||
|
submitButton "content-save-outline" "Save"; txt " "
|
||||||
|
a [ _href "/profile/edit/history/list"; _hxGet "/profile/edit/history/list"; _hxTarget "#historyList"
|
||||||
|
_class "btn btn-secondary" ] [ i [ _class "mdi mdi-cancel"] []; txt " Cancel" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// List the employment history entries for an employment profile
|
||||||
|
let historyTable (history : EmploymentHistory list) editIdx csrf =
|
||||||
|
let editingIdx = defaultArg editIdx -2
|
||||||
|
let isEditing = editingIdx >= -1
|
||||||
|
let monthAndYear = Text.LocalDatePattern.CreateWithInvariantCulture "MMMM yyyy"
|
||||||
|
let renderTable () =
|
||||||
|
let editHistoryForm entry idx =
|
||||||
|
tr [] [
|
||||||
|
td [ _colspan "4" ] [
|
||||||
|
form [ _class "row g-3"; _hxPost $"/profile/edit/history/{idx}"; _hxTarget "#historyList" ] [
|
||||||
|
antiForgery csrf
|
||||||
|
yield! historyForm (HistoryForm.fromHistory entry) (idx = -1)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
table [ _class "table table-sm table-hover pt-3" ] [
|
||||||
|
thead [] [
|
||||||
|
[ "Action"; "Employer"; "Dates"; "Position" ]
|
||||||
|
|> List.map (fun it -> th [ _scope "col" ] [ txt it ])
|
||||||
|
|> tr []
|
||||||
|
]
|
||||||
|
tbody [] [
|
||||||
|
if isEditing && editingIdx = -1 then editHistoryForm EmploymentHistory.empty -1
|
||||||
|
yield! history |> List.mapi (fun idx entry ->
|
||||||
|
if isEditing && editingIdx = idx then editHistoryForm entry idx
|
||||||
|
else
|
||||||
|
tr [] [
|
||||||
|
td [ if isEditing then _class "text-muted" ] [
|
||||||
|
if isEditing then txt "Edit ~ Delete"
|
||||||
|
else
|
||||||
|
let link = $"/profile/edit/history/{idx}"
|
||||||
|
a [ _href link; _hxGet link ] [ txt "Edit" ]; txt " ~ "
|
||||||
|
a [ _href $"{link}/delete"; _hxPost $"{link}/delete"; _class "text-danger"
|
||||||
|
_hxConfirm "Are you sure you want to delete this employment history entry?" ] [
|
||||||
|
txt "Delete"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
td [] [ str entry.Employer ]
|
||||||
|
td [] [
|
||||||
|
str (monthAndYear.Format entry.StartDate); txt " to "
|
||||||
|
match entry.EndDate with Some dt -> str (monthAndYear.Format dt) | None -> txt "Present"
|
||||||
|
]
|
||||||
|
td [ if Option.isNone entry.Position then _class "text-muted fst-italic" ] [
|
||||||
|
str (defaultArg entry.Position "None")
|
||||||
|
]
|
||||||
|
])
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if List.isEmpty history && not isEditing then
|
||||||
|
p [ _id "historyList"; _class "text-muted fst-italic pt-3" ] [
|
||||||
|
txt "Your profile has no employment history defined"
|
||||||
|
]
|
||||||
|
else if List.isEmpty history then
|
||||||
|
form [ _id "historyList"; _hxTarget "this"; _hxPost "/profile/edit/history/-1"; _hxSwap HxSwap.OuterHtml
|
||||||
|
_class "row g-3" ] [
|
||||||
|
antiForgery csrf
|
||||||
|
yield! historyForm (HistoryForm.fromHistory EmploymentHistory.empty) true
|
||||||
|
]
|
||||||
|
else if isEditing then div [ _id "historyList" ] [ renderTable () ]
|
||||||
|
else // not editing, there is history to show
|
||||||
|
form [ _id "historyList"; _hxTarget "this"; _hxSwap HxSwap.OuterHtml ] [
|
||||||
|
antiForgery csrf
|
||||||
|
renderTable ()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// The employment history maintenance page
|
||||||
|
let history (history : EmploymentHistory list) csrf =
|
||||||
|
pageWithTitle "Employment Profile: Employment History" [
|
||||||
|
backToEdit
|
||||||
|
p [] [
|
||||||
|
a [ _href "/profile/edit/history/-1"; _hxGet "/profile/edit/history/-1"; _hxTarget "#historyList"
|
||||||
|
_hxSwap HxSwap.OuterHtml; _class "btn btn-sm btn-outline-primary rounded-pill" ] [
|
||||||
|
txt "Add an Employment History Entry"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
historyTable history None csrf
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// The employment history edit component
|
||||||
|
let editHistory (history : EmploymentHistory list) idx csrf =
|
||||||
|
historyTable history (Some idx) csrf
|
||||||
|
|
||||||
// ~~~ PROFILE SEARCH ~~~ //
|
// ~~~ PROFILE SEARCH ~~~ //
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user