566 lines
15 KiB
Forth
566 lines
15 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
|
|
|
|
/// Convert this to a single-character code
|
|
override this.ToString() =
|
|
match this with
|
|
| NoDisplay -> "N"
|
|
| ShortDate -> "S"
|
|
| LongDate -> "L"
|
|
|
|
/// <summary>Create an <c>AsOfDateDisplay</c> from a single-character code</summary>
|
|
static member Parse code =
|
|
match code with
|
|
| "N" -> NoDisplay
|
|
| "S" -> ShortDate
|
|
| "L" -> LongDate
|
|
| _ -> invalidArg "code" $"Unknown code {code}"
|
|
|
|
|
|
/// Acceptable e-mail formats
|
|
type EmailFormat =
|
|
/// HTML e-mail
|
|
| HtmlFormat
|
|
/// Plain-text e-mail
|
|
| PlainTextFormat
|
|
|
|
/// Convert this to a single-character code
|
|
override this.ToString() =
|
|
match this with
|
|
| HtmlFormat -> "H"
|
|
| PlainTextFormat -> "P"
|
|
|
|
/// <summary>Create an <c>EmailFormat</c> from a single-character code</summary>
|
|
static member Parse code =
|
|
match code with
|
|
| "H" -> HtmlFormat
|
|
| "P" -> PlainTextFormat
|
|
| _ -> invalidArg "code" $"Unknown code {code}"
|
|
|
|
|
|
/// Expiration for requests
|
|
type Expiration =
|
|
/// Follow the rules for normal expiration
|
|
| Automatic
|
|
/// Do not expire via rules
|
|
| Manual
|
|
/// Force immediate expiration
|
|
| Forced
|
|
|
|
/// Convert this to a single-character code
|
|
override this.ToString() =
|
|
match this with
|
|
| Automatic -> "A"
|
|
| Manual -> "M"
|
|
| Forced -> "F"
|
|
|
|
/// <summary>Create an <c>Expiration</c> from a single-character code</summary>
|
|
static member Parse code =
|
|
match code with
|
|
| "A" -> Automatic
|
|
| "M" -> Manual
|
|
| "F" -> Forced
|
|
| _ -> invalidArg "code" $"Unknown code {code}"
|
|
|
|
|
|
/// Types of prayer requests
|
|
type PrayerRequestType =
|
|
/// Current requests
|
|
| CurrentRequest
|
|
/// Long-term/ongoing request
|
|
| LongTermRequest
|
|
/// Expectant couples
|
|
| Expecting
|
|
/// Praise reports
|
|
| PraiseReport
|
|
/// Announcements
|
|
| Announcement
|
|
|
|
/// Convert this to a single-character code
|
|
override this.ToString() =
|
|
match this with
|
|
| CurrentRequest -> "C"
|
|
| LongTermRequest -> "L"
|
|
| Expecting -> "E"
|
|
| PraiseReport -> "P"
|
|
| Announcement -> "A"
|
|
|
|
/// <summary>Create a <c>PrayerRequestType</c> from a single-character code</summary>
|
|
static member Parse code =
|
|
match code with
|
|
| "C" -> CurrentRequest
|
|
| "L" -> LongTermRequest
|
|
| "E" -> Expecting
|
|
| "P" -> PraiseReport
|
|
| "A" -> Announcement
|
|
| _ -> invalidArg "code" $"Unknown code {code}"
|
|
|
|
|
|
/// How requests should be sorted
|
|
type RequestSort =
|
|
/// Sort by date, then by requestor/subject
|
|
| SortByDate
|
|
/// Sort by requestor/subject, then by date
|
|
| SortByRequestor
|
|
|
|
/// Convert this to a single-character code
|
|
override this.ToString() =
|
|
match this with
|
|
| SortByDate -> "D"
|
|
| SortByRequestor -> "R"
|
|
|
|
/// <summary>Create a <c>RequestSort</c> from a single-character code</summary>
|
|
static member Parse code =
|
|
match code with
|
|
| "D" -> SortByDate
|
|
| "R" -> SortByRequestor
|
|
| _ -> invalidArg "code" $"Unknown code {code}"
|
|
|
|
|
|
/// Type for a time zone ID
|
|
type TimeZoneId =
|
|
| TimeZoneId of string
|
|
|
|
override this.ToString() =
|
|
match this with
|
|
| TimeZoneId it -> it
|
|
|
|
|
|
open System
|
|
|
|
/// PK type for the Church entity
|
|
type ChurchId =
|
|
| ChurchId of Guid
|
|
|
|
/// The GUID value of the church ID
|
|
member this.Value =
|
|
this
|
|
|> function
|
|
| ChurchId guid -> guid
|
|
|
|
override this.ToString() =
|
|
this.Value.ToString "N"
|
|
|
|
|
|
/// PK type for the Member entity
|
|
type MemberId =
|
|
| MemberId of Guid
|
|
|
|
/// The GUID value of the member ID
|
|
member this.Value =
|
|
this
|
|
|> function
|
|
| MemberId guid -> guid
|
|
|
|
override this.ToString() =
|
|
this.Value.ToString "N"
|
|
|
|
|
|
/// PK type for the PrayerRequest entity
|
|
type PrayerRequestId =
|
|
| PrayerRequestId of Guid
|
|
|
|
/// The GUID value of the prayer request ID
|
|
member this.Value =
|
|
this
|
|
|> function
|
|
| PrayerRequestId guid -> guid
|
|
|
|
override this.ToString() =
|
|
this.Value.ToString "N"
|
|
|
|
|
|
/// PK type for the SmallGroup entity
|
|
type SmallGroupId =
|
|
| SmallGroupId of Guid
|
|
|
|
/// The GUID value of the small group ID
|
|
member this.Value =
|
|
this
|
|
|> function
|
|
| SmallGroupId guid -> guid
|
|
|
|
override this.ToString() =
|
|
this.Value.ToString "N"
|
|
|
|
|
|
/// PK type for the User entity
|
|
type UserId =
|
|
| UserId of Guid
|
|
|
|
/// The GUID value of the user ID
|
|
member this.Value =
|
|
this
|
|
|> function
|
|
| UserId guid -> guid
|
|
|
|
override this.ToString() =
|
|
this.Value.ToString "N"
|
|
|
|
(*-- SPECIFIC VIEW TYPES --*)
|
|
|
|
open Microsoft.Data.Sqlite
|
|
|
|
/// 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
|
|
[<CLIMutable; 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
|
|
}
|
|
|
|
/// Map a row to a Small Group information set
|
|
static member FromReader (rdr: SqliteDataReader) =
|
|
{ Id = Giraffe.ShortGuid.fromGuid ((rdr.GetOrdinal >> rdr.GetString >> Guid.Parse) "id")
|
|
Name = (rdr.GetOrdinal >> rdr.GetString) "groupName"
|
|
ChurchName = (rdr.GetOrdinal >> rdr.GetString) "churchName"
|
|
TimeZoneId = (rdr.GetOrdinal >> rdr.GetString >> TimeZoneId) "timeZoneId"
|
|
IsPublic = (rdr.GetOrdinal >> rdr.GetBoolean) "isPublic" }
|
|
|
|
|
|
(*-- ENTITIES --*)
|
|
|
|
open NodaTime
|
|
|
|
/// This represents a church
|
|
[<CLIMutable; 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
|
|
}
|
|
|
|
/// An empty church
|
|
// aww... how sad :(
|
|
static member 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 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
|
|
}
|
|
|
|
/// 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
|
|
|
|
/// A set of preferences with their default values
|
|
static member 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
|
|
[<CLIMutable; 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
|
|
}
|
|
|
|
/// An empty member
|
|
static member Empty =
|
|
{ Id = MemberId Guid.Empty
|
|
SmallGroupId = SmallGroupId Guid.Empty
|
|
Name = ""
|
|
Email = ""
|
|
Format = None }
|
|
|
|
|
|
/// This represents a small group (Sunday School class, Bible study group, etc.)
|
|
[<CLIMutable; 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
|
|
}
|
|
|
|
/// The DateTimeZone for the time zone ID for this small group
|
|
member this.TimeZone =
|
|
let tzId = string this.Preferences.TimeZoneId
|
|
|
|
if DateTimeZoneProviders.Tzdb.Ids.Contains tzId then
|
|
DateTimeZoneProviders.Tzdb[tzId]
|
|
else
|
|
DateTimeZone.Utc
|
|
|
|
/// Get the local date/time for this group
|
|
member this.LocalTimeNow(clock: IClock) =
|
|
if isNull clock then
|
|
nullArg (nameof clock)
|
|
|
|
clock.GetCurrentInstant().InZone(this.TimeZone).LocalDateTime
|
|
|
|
/// Get the local date for this group
|
|
member this.LocalDateNow clock = this.LocalTimeNow(clock).Date
|
|
|
|
/// An empty small group
|
|
static member Empty =
|
|
{ Id = SmallGroupId Guid.Empty
|
|
ChurchId = ChurchId Guid.Empty
|
|
Name = ""
|
|
Preferences = ListPreferences.Empty }
|
|
|
|
|
|
/// This represents a single prayer request
|
|
[<CLIMutable; 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
|
|
}
|
|
|
|
/// Is this request expired?
|
|
member this.IsExpired (asOf: LocalDate) (group: SmallGroup) =
|
|
match this.Expiration, this.RequestType with
|
|
| Forced, _ -> true
|
|
| Manual, _
|
|
| Automatic, LongTermRequest
|
|
| Automatic, Expecting -> false
|
|
| Automatic, _ ->
|
|
// Automatic expiration
|
|
Period
|
|
.Between(this.UpdatedDate.InZone(group.TimeZone).Date, asOf, PeriodUnits.Days)
|
|
.Days
|
|
>= group.Preferences.DaysToExpire
|
|
|
|
/// Is an update required for this long-term request?
|
|
member this.UpdateRequired asOf group =
|
|
if this.IsExpired asOf group then
|
|
false
|
|
else
|
|
asOf.PlusWeeks -group.Preferences.LongTermUpdateWeeks
|
|
>= this.UpdatedDate.InZone(group.TimeZone).Date
|
|
|
|
/// An empty request
|
|
static member 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 }
|
|
|
|
|
|
/// This represents a user of PrayerTracker
|
|
[<CLIMutable; 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
|
|
|
|
/// The small groups to which this user is authorized
|
|
SmallGroups: SmallGroupId list
|
|
}
|
|
|
|
/// The full name of the user
|
|
member this.Name = $"{this.FirstName} {this.LastName}"
|
|
|
|
/// An empty user
|
|
static member Empty =
|
|
{ Id = UserId Guid.Empty
|
|
FirstName = ""
|
|
LastName = ""
|
|
Email = ""
|
|
IsAdmin = false
|
|
PasswordHash = ""
|
|
LastSeen = None
|
|
SmallGroups = [] }
|