Daniel J. Summers 13ace6ca61
Version 8 (#43)
* Use htmx for front end (#36)
* Use short GUIDs in URLs and forms (#1)
* Refresh theme (#38)
* Use ASP.NET Core for log on/off (#39)
* Fix date/time / time zone issues (#41)
* Update help for v8 (#42)
* Add FAKE build script (#37)
2022-08-19 15:08:30 -04:00

558 lines
16 KiB
Forth

namespace PrayerTracker.Entities
(*-- SUPPORT TYPES --*)
/// How as-of dates should (or should not) be displayed with requests
type AsOfDateDisplay =
/// No as-of date should be displayed
| NoDisplay
/// The as-of date should be displayed in the culture's short date format
| ShortDate
/// The as-of date should be displayed in the culture's long date format
| LongDate
/// Functions to support as-of date display options
module AsOfDateDisplay =
/// Convert to a DU case from a single-character string
let fromCode code =
match code with
| "N" -> NoDisplay
| "S" -> ShortDate
| "L" -> LongDate
| _ -> invalidArg "code" $"Unknown code {code}"
/// Convert this DU case to a single-character string
let toCode = function NoDisplay -> "N" | ShortDate -> "S" | LongDate -> "L"
/// Acceptable e-mail formats
type EmailFormat =
/// HTML e-mail
| HtmlFormat
/// Plain-text e-mail
| PlainTextFormat
/// Functions to support e-mail formats
module EmailFormat =
/// Convert to a DU case from a single-character string
let fromCode code =
match code with
| "H" -> HtmlFormat
| "P" -> PlainTextFormat
| _ -> invalidArg "code" $"Unknown code {code}"
/// Convert this DU case to a single-character string
let toCode = function HtmlFormat -> "H" | PlainTextFormat -> "P"
/// Expiration for requests
type Expiration =
/// Follow the rules for normal expiration
| Automatic
/// Do not expire via rules
| Manual
/// Force immediate expiration
| Forced
/// Functions to support expirations
module Expiration =
/// Convert to a DU case from a single-character string
let fromCode code =
match code with
| "A" -> Automatic
| "M" -> Manual
| "F" -> Forced
| _ -> invalidArg "code" $"Unknown code {code}"
/// Convert this DU case to a single-character string
let toCode = function Automatic -> "A" | Manual -> "M" | Forced -> "F"
/// Types of prayer requests
type PrayerRequestType =
/// Current requests
| CurrentRequest
/// Long-term/ongoing request
| LongTermRequest
/// Expectant couples
| Expecting
/// Praise reports
| PraiseReport
/// Announcements
| Announcement
/// Functions to support prayer request types
module PrayerRequestType =
/// Convert to a DU case from a single-character string
let fromCode code =
match code with
| "C" -> CurrentRequest
| "L" -> LongTermRequest
| "E" -> Expecting
| "P" -> PraiseReport
| "A" -> Announcement
| _ -> invalidArg "code" $"Unknown code {code}"
/// Convert this DU case to a single-character string
let toCode =
function
| CurrentRequest -> "C"
| LongTermRequest -> "L"
| Expecting -> "E"
| PraiseReport -> "P"
| Announcement -> "A"
/// How requests should be sorted
type RequestSort =
/// Sort by date, then by requestor/subject
| SortByDate
/// Sort by requestor/subject, then by date
| SortByRequestor
/// Functions to support request sorts
module RequestSort =
/// Convert to a DU case from a single-character string
let fromCode code =
match code with
| "D" -> SortByDate
| "R" -> SortByRequestor
| _ -> invalidArg "code" $"Unknown code {code}"
/// Convert this DU case to a single-character string
let toCode = function SortByDate -> "D" | SortByRequestor -> "R"
open System
/// PK type for the Church entity
type ChurchId =
| ChurchId of Guid
with
/// The GUID value of the church ID
member this.Value = this |> function ChurchId guid -> guid
/// PK type for the Member entity
type MemberId =
| MemberId of Guid
with
/// The GUID value of the member ID
member this.Value = this |> function MemberId guid -> guid
/// PK type for the PrayerRequest entity
type PrayerRequestId =
| PrayerRequestId of Guid
with
/// The GUID value of the prayer request ID
member this.Value = this |> function PrayerRequestId guid -> guid
/// PK type for the SmallGroup entity
type SmallGroupId =
| SmallGroupId of Guid
with
/// The GUID value of the small group ID
member this.Value = this |> function SmallGroupId guid -> guid
/// PK type for the TimeZone entity
type TimeZoneId = TimeZoneId of string
/// Functions to support time zone IDs
module TimeZoneId =
/// Convert a time zone ID to its string value
let toString = function TimeZoneId it -> it
/// PK type for the User entity
type UserId =
| UserId of Guid
with
/// The GUID value of the user ID
member this.Value = this |> function UserId guid -> guid
(*-- SPECIFIC VIEW TYPES --*)
/// Statistics for churches
[<NoComparison; NoEquality>]
type ChurchStats =
{ /// The number of small groups in the church
SmallGroups : int
/// The number of prayer requests in the church
PrayerRequests : int
/// The number of users who can access small groups in the church
Users : int
}
/// Information needed to display the public/protected request list and small group maintenance pages
[<NoComparison; NoEquality>]
type SmallGroupInfo =
{ /// The ID of the small group
Id : string
/// The name of the small group
Name : string
/// The name of the church to which the small group belongs
ChurchName : string
/// The ID of the time zone for the small group
TimeZoneId : TimeZoneId
/// Whether the small group has a publicly-available request list
IsPublic : bool
}
(*-- ENTITIES --*)
open NodaTime
/// This represents a church
[<NoComparison; NoEquality>]
type Church =
{ /// The ID of this church
Id : ChurchId
/// The name of the church
Name : string
/// The city where the church is
City : string
/// The 2-letter state or province code for the church's location
State : string
/// Does this church have an active interface with Virtual Prayer Space?
HasVpsInterface : bool
/// The address for the interface
InterfaceAddress : string option
}
/// Functions to support churches
module Church =
/// An empty church
// aww... how sad :(
let empty =
{ Id = ChurchId Guid.Empty
Name = ""
City = ""
State = ""
HasVpsInterface = false
InterfaceAddress = None
}
/// Preferences for the form and format of the prayer request list
[<NoComparison; NoEquality>]
type ListPreferences =
{ /// The Id of the small group to which these preferences belong
SmallGroupId : SmallGroupId
/// The days after which regular requests expire
DaysToExpire : int
/// The number of days a new or updated request is considered new
DaysToKeepNew : int
/// The number of weeks after which long-term requests are flagged for follow-up
LongTermUpdateWeeks : int
/// The name from which e-mails are sent
EmailFromName : string
/// The e-mail address from which e-mails are sent
EmailFromAddress : string
/// The fonts to use in generating the list of prayer requests
Fonts : string
/// The color for the prayer request list headings
HeadingColor : string
/// The color for the lines offsetting the prayer request list headings
LineColor : string
/// The font size for the headings on the prayer request list
HeadingFontSize : int
/// The font size for the text on the prayer request list
TextFontSize : int
/// The order in which the prayer requests are sorted
RequestSort : RequestSort
/// The password used for "small group login" (view-only request list)
GroupPassword : string
/// The default e-mail type for this class
DefaultEmailType : EmailFormat
/// Whether this class makes its request list public
IsPublic : bool
/// The time zone which this class uses (use tzdata names)
TimeZoneId : TimeZoneId
/// The number of requests displayed per page
PageSize : int
/// How the as-of date should be automatically displayed
AsOfDateDisplay : AsOfDateDisplay
}
with
/// The list of fonts to use when displaying request lists (converts "native" to native font stack)
member this.FontStack =
if this.Fonts = "native" then
"""system-ui,-apple-system,"Segoe UI",Roboto,Ubuntu,"Liberation Sans",Cantarell,"Helvetica Neue",sans-serif"""
else this.Fonts
/// Functions to support list preferences
module ListPreferences =
/// A set of preferences with their default values
let empty =
{ SmallGroupId = SmallGroupId Guid.Empty
DaysToExpire = 14
DaysToKeepNew = 7
LongTermUpdateWeeks = 4
EmailFromName = "PrayerTracker"
EmailFromAddress = "prayer@bitbadger.solutions"
Fonts = "native"
HeadingColor = "maroon"
LineColor = "navy"
HeadingFontSize = 16
TextFontSize = 12
RequestSort = SortByDate
GroupPassword = ""
DefaultEmailType = HtmlFormat
IsPublic = false
TimeZoneId = TimeZoneId "America/Denver"
PageSize = 100
AsOfDateDisplay = NoDisplay
}
/// A member of a small group
[<NoComparison; NoEquality>]
type Member =
{ /// The ID of the small group member
Id : MemberId
/// The Id of the small group to which this member belongs
SmallGroupId : SmallGroupId
/// The name of the member
Name : string
/// The e-mail address for the member
Email : string
/// The type of e-mail preferred by this member
Format : EmailFormat option
}
/// Functions to support small group members
module Member =
/// An empty member
let empty =
{ Id = MemberId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
Name = ""
Email = ""
Format = None
}
/// This represents a single prayer request
[<NoComparison; NoEquality>]
type PrayerRequest =
{ /// The ID of this request
Id : PrayerRequestId
/// The type of the request
RequestType : PrayerRequestType
/// The ID of the user who entered the request
UserId : UserId
/// The small group to which this request belongs
SmallGroupId : SmallGroupId
/// The date/time on which this request was entered
EnteredDate : Instant
/// The date/time this request was last updated
UpdatedDate : Instant
/// The name of the requestor or subject, or title of announcement
Requestor : string option
/// The text of the request
Text : string
/// Whether the chaplain should be notified for this request
NotifyChaplain : bool
/// Is this request expired?
Expiration : Expiration
}
// functions are below small group functions
/// This represents a small group (Sunday School class, Bible study group, etc.)
[<NoComparison; NoEquality>]
type SmallGroup =
{ /// The ID of this small group
Id : SmallGroupId
/// The church to which this group belongs
ChurchId : ChurchId
/// The name of the group
Name : string
/// The preferences for the request list
Preferences : ListPreferences
}
/// Functions to support small groups
module SmallGroup =
/// An empty small group
let empty =
{ Id = SmallGroupId Guid.Empty
ChurchId = ChurchId Guid.Empty
Name = ""
Preferences = ListPreferences.empty
}
/// The DateTimeZone for the time zone ID for this small group
let timeZone group =
let tzId = TimeZoneId.toString group.Preferences.TimeZoneId
if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then DateTimeZoneProviders.Tzdb[tzId]
else DateTimeZone.Utc
/// Get the local date/time for this group
let localTimeNow (clock : IClock) group =
if isNull clock then nullArg (nameof clock)
clock.GetCurrentInstant().InZone(timeZone group).LocalDateTime
/// Get the local date for this group
let localDateNow clock group =
(localTimeNow clock group).Date
/// Functions to support prayer requests
module PrayerRequest =
/// An empty request
let empty =
{ Id = PrayerRequestId Guid.Empty
RequestType = CurrentRequest
UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
EnteredDate = Instant.MinValue
UpdatedDate = Instant.MinValue
Requestor = None
Text = ""
NotifyChaplain = false
Expiration = Automatic
}
/// Is this request expired?
let isExpired (asOf : LocalDate) group req =
match req.Expiration, req.RequestType with
| Forced, _ -> true
| Manual, _
| Automatic, LongTermRequest
| Automatic, Expecting -> false
| Automatic, _ ->
// Automatic expiration
Period.Between(req.UpdatedDate.InZone(SmallGroup.timeZone group).Date, asOf, PeriodUnits.Days).Days
>= group.Preferences.DaysToExpire
/// Is an update required for this long-term request?
let updateRequired asOf group req =
if isExpired asOf group req then false
else asOf.PlusWeeks -group.Preferences.LongTermUpdateWeeks
>= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date
/// This represents a user of PrayerTracker
[<NoComparison; NoEquality>]
type User =
{ /// The ID of this user
Id : UserId
/// The first name of this user
FirstName : string
/// The last name of this user
LastName : string
/// The e-mail address of the user
Email : string
/// Whether this user is a PrayerTracker system administrator
IsAdmin : bool
/// The user's hashed password
PasswordHash : string
/// The last time the user was seen (set whenever the user is loaded into a session)
LastSeen : Instant option
}
with
/// The full name of the user
member this.Name =
$"{this.FirstName} {this.LastName}"
/// Functions to support users
module User =
/// An empty user
let empty =
{ Id = UserId Guid.Empty
FirstName = ""
LastName = ""
Email = ""
IsAdmin = false
PasswordHash = ""
LastSeen = None
}
/// Cross-reference between user and small group
[<NoComparison; NoEquality>]
type UserSmallGroup =
{ /// The Id of the user who has access to the small group
UserId : UserId
/// The Id of the small group to which the user has access
SmallGroupId : SmallGroupId
}
/// Functions to support user/small group cross-reference
module UserSmallGroup =
/// An empty user/small group xref
let empty =
{ UserId = UserId Guid.Empty
SmallGroupId = SmallGroupId Guid.Empty
}