Version 3 #40
@ -114,7 +114,7 @@ module EmploymentHistory =
|
||||
|
||||
let empty =
|
||||
{ Employer = ""
|
||||
StartDate = LocalDate.MinIsoValue
|
||||
StartDate = LocalDate.FromDateTime DateTime.Today
|
||||
EndDate = None
|
||||
Position = None
|
||||
Description = None
|
||||
|
@ -3,27 +3,6 @@ module JobsJobsJobs.Profiles.Domain
|
||||
open JobsJobsJobs.Domain
|
||||
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
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
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
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type ProfileSearchForm =
|
||||
@ -166,3 +187,23 @@ type PublicSearchResult =
|
||||
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
|
||||
}
|
||||
|
||||
// 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]
|
||||
let editSkill idx : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
match! Data.findById (currentCitizenId ctx) with
|
||||
@ -146,6 +153,59 @@ let deleteSkill idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ct
|
||||
| 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
|
||||
let view citizenId : HttpHandler = fun next ctx -> task {
|
||||
let citId = CitizenId citizenId
|
||||
@ -167,18 +227,24 @@ open Giraffe.EndpointRouting
|
||||
let endpoints =
|
||||
subRoute "/profile" [
|
||||
GET_HEAD [
|
||||
routef "/%O/view" view
|
||||
route "/edit" edit
|
||||
route "/edit/general" editGeneralInfo
|
||||
routef "/edit/skill/%i" editSkill
|
||||
route "/edit/skills" skills
|
||||
route "/search" search
|
||||
route "/seeking" seeking
|
||||
routef "/%O/view" view
|
||||
route "/edit" edit
|
||||
route "/edit/general" editGeneralInfo
|
||||
routef "/edit/history/%i" editHistory
|
||||
route "/edit/history" history
|
||||
route "/edit/history/list" historyList
|
||||
routef "/edit/skill/%i" editSkill
|
||||
route "/edit/skills" skills
|
||||
route "/edit/skills/list" skillList
|
||||
route "/search" search
|
||||
route "/seeking" seeking
|
||||
]
|
||||
POST [
|
||||
route "/delete" delete
|
||||
routef "/edit/skill/%i" saveSkill
|
||||
routef "/edit/skill/%i/delete" deleteSkill
|
||||
route "/save" saveGeneralInfo
|
||||
route "/delete" delete
|
||||
routef "/edit/history/%i" saveHistory
|
||||
routef "/edit/history/%i/delete" deleteHistory
|
||||
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 =
|
||||
[ h4 [] [ txt $"""{if isNew then "Add a" else "Edit"} Skill""" ]
|
||||
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 "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 "col-12" ] [
|
||||
@ -231,6 +227,120 @@ let skills (skills : Skill list) csrf =
|
||||
let editSkill (skills : Skill list) 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 ~~~ //
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user