Migrate v3 data; WIP on journal items

This commit is contained in:
Daniel J. Summers 2024-06-21 23:22:56 -04:00
parent 4aa6e832c7
commit 4ea55d4d25
12 changed files with 273 additions and 18 deletions

View File

@ -1,3 +1,50 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
// TODO: write migration use BitBadger\PDODocument\{Configuration, Custom, Definition, Document, Mode};
use BitBadger\PDODocument\Mapper\ArrayMapper;
use MyPrayerJournal\{History, Note, Recurrence, RecurrencePeriod, Request, RequestAction, Table};
require 'start.php';
echo 'Retrieving v3 requests...' . PHP_EOL;
Configuration::resetPDO();
Configuration::$pdoDSN = 'pgsql:host=localhost;user=mpj;password=devpassword;dbname=mpj';
$reqs = Custom::array('SELECT data FROM mpj.request', [], new ArrayMapper());
echo 'Found ' . sizeof($reqs) . ' requests; migrating to v4...' . PHP_EOL;
Configuration::resetPDO();
Configuration::$mode = Mode::SQLite;
Configuration::$pdoDSN = 'sqlite:./data/mpj.db';
Definition::ensureTable(Table::REQUEST);
foreach ($reqs as $reqJson) {
$req = json_decode($reqJson['data']);
$notes = array_map(fn(stdClass $note) => new Note($note->asOf, $note->notes), $req->notes ?? []);
$history = array_map(fn(stdClass $hist) =>
new History(
asOf: $hist->asOf,
action: RequestAction::from($hist->status),
text: property_exists($hist, 'text') ? $hist->text : null),
$req->history);
$recurParts = explode(' ', $req->recurrence);
$recurPeriod = RecurrencePeriod::from(end($recurParts));
$recur = match ($recurPeriod) {
RecurrencePeriod::Immediate => new Recurrence(RecurrencePeriod::Immediate),
default => new Recurrence($recurPeriod, (int)$recurParts[0])
};
$v4Req = new Request(
id: $req->id,
enteredOn: $req->enteredOn,
userId: $req->userId,
snoozedUntil: property_exists($req, 'snoozedUntil') ? $req->snoozedUntil : null,
showAfter: property_exists($req, 'showAfter') ? $req->showAfter : null,
recurrence: $recur,
history: $history,
notes: $notes);
Document::insert(Table::REQUEST, $v4Req);
}
echo PHP_EOL . 'done' . PHP_EOL;

View File

@ -22,6 +22,16 @@ class Auth
return self::$auth0; return self::$auth0;
} }
/**
* Get the logged on user information
*
* @return array|null The user information (null if no user is logged on)
*/
public static function user(): ?array
{
return self::client()->getUser();
}
/** /**
* Initiate a log on with Auth0 * Initiate a log on with Auth0
* *
@ -50,4 +60,20 @@ class Auth
header('Location: ' . self::client()->logout($_ENV['AUTH0_BASE_URL'])); header('Location: ' . self::client()->logout($_ENV['AUTH0_BASE_URL']));
exit; exit;
} }
/**
* Require a user be logged on
*
* @param bool $redirect Whether to redirect to log on if there is not a user logged on
* @return void If it returns, there is a user logged on; if not, we will be redirected to log on
* @throws ConfigurationException If the Auth0 client is not configured correctly
*/
public static function requireUser(bool $redirect = true): void
{
if (is_null(self::user())) {
if ($redirect) self::logOn();
http_response_code(403);
die('Not Authorized');
}
}
} }

View File

@ -2,10 +2,12 @@
namespace MyPrayerJournal; namespace MyPrayerJournal;
use JsonSerializable;
/** /**
* A record of an action taken on a request * A record of an action taken on a request
*/ */
class History class History implements JsonSerializable
{ {
/** /**
* @param string $asOf The date/time this entry was made * @param string $asOf The date/time this entry was made
@ -13,4 +15,11 @@ class History
* @param string|null $text The text for this history entry (optional) * @param string|null $text The text for this history entry (optional)
*/ */
public function __construct(public string $asOf, public RequestAction $action, public ?string $text = null) { } public function __construct(public string $asOf, public RequestAction $action, public ?string $text = null) { }
public function jsonSerialize(): mixed
{
$values = ['asOf' => $this->asOf, 'action' => $this->action->value];
if (!is_null($this->text)) $values['text'] = $this->text;
return $values;
}
} }

View File

@ -114,4 +114,15 @@ class Layout
</footer><?php </footer><?php
} }
/// Create a card when there are no results found
public static function noResults(string $heading, string $link, string $buttonText, string $text): void
{ ?>
<div class=card>
<h5 class=card-header><?=$heading?></h5>
<div class="card-body text-center">
<p class=card-text><?=$text?></p><?php
page_link($link, $buttonText, ['class' => 'btn btn-primary']); ?>
</div>
</div><?php
}
} }

View File

@ -2,14 +2,23 @@
namespace MyPrayerJournal; namespace MyPrayerJournal;
use JsonSerializable;
/** /**
* The recurrence for a prayer request * The recurrence for a prayer request
*/ */
class Recurrence class Recurrence implements JsonSerializable
{ {
/** /**
* @param RecurrencePeriod $period The recurrence period * @param RecurrencePeriod $period The recurrence period
* @param int|null $interval How many of the periods will pass before the request is visible again * @param int|null $interval How many of the periods will pass before the request is visible again
*/ */
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { } public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { }
public function jsonSerialize(): mixed
{
$values = ['period' => $this->period->value];
if (!is_null($this->interval)) $values['interval'] = $this->interval;
return $values;
}
} }

View File

@ -5,17 +5,17 @@ namespace MyPrayerJournal;
/** /**
* The type of recurrence a request can have * The type of recurrence a request can have
*/ */
enum RecurrencePeriod enum RecurrencePeriod: string
{ {
/** Requests, once prayed, are available again immediately */ /** Requests, once prayed, are available again immediately */
case Immediate; case Immediate = 'Immediate';
/** Requests, once prayed, appear again in a number of hours */ /** Requests, once prayed, appear again in a number of hours */
case Hours; case Hours = 'Hours';
/** Requests, once prayed, appear again in a number of days */ /** Requests, once prayed, appear again in a number of days */
case Days; case Days = 'Days';
/** Requests, once prayed, appear again in a number of weeks */ /** Requests, once prayed, appear again in a number of weeks */
case Weeks; case Weeks = 'Weeks';
} }

View File

@ -4,12 +4,13 @@ namespace MyPrayerJournal;
use BitBadger\PDODocument\{DocumentException, Find}; use BitBadger\PDODocument\{DocumentException, Find};
use Exception; use Exception;
use JsonSerializable;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
/** /**
* A prayer request * A prayer request
*/ */
class Request class Request implements JsonSerializable
{ {
/** /**
* @param string $id The ID for the request * @param string $id The ID for the request
@ -18,8 +19,8 @@ class Request
* @param string|null $snoozedUntil The date/time the snooze expires for this request (null = not snoozed) * @param string|null $snoozedUntil The date/time the snooze expires for this request (null = not snoozed)
* @param string|null $showAfter The date/time the current recurrence period is over (null = immediate) * @param string|null $showAfter The date/time the current recurrence period is over (null = immediate)
* @param Recurrence $recurrence The recurrence for this request * @param Recurrence $recurrence The recurrence for this request
* @param array|History[] $history The history of this request * @param History[] $history The history of this request
* @param array|Note[] $notes Notes regarding this request * @param Note[] $notes Notes regarding this request
* @throws Exception If the ID generation fails * @throws Exception If the ID generation fails
*/ */
public function __construct(public string $id = '', public string $enteredOn = '', public string $userId = '', public function __construct(public string $id = '', public string $enteredOn = '', public string $userId = '',
@ -44,4 +45,19 @@ class Request
$req = Find::byId(Table::REQUEST, $id, self::class); $req = Find::byId(Table::REQUEST, $id, self::class);
return ($req && $req->userId == $_SESSION['user_id']) ? $req : false; return ($req && $req->userId == $_SESSION['user_id']) ? $req : false;
} }
public function jsonSerialize(): mixed
{
$values = [
'id' => $this->id,
'enteredOn' => $this->enteredOn,
'userId' => $this->userId,
'recurrence' => $this->recurrence,
'history' => $this->history,
'notes' => $this->notes
];
if (!is_null($this->snoozedUntil)) $values['snoozedUntil'] = $this->snoozedUntil;
if (!is_null($this->showAfter)) $values['showAfter'] = $this->showAfter;
return $values;
}
} }

View File

@ -5,17 +5,17 @@ namespace MyPrayerJournal;
/** /**
* An action taken on a prayer request * An action taken on a prayer request
*/ */
enum RequestAction enum RequestAction: string
{ {
/** The request was created */ /** The request was created */
case Created; case Created = 'Created';
/** The request was marked as having been prayed for */ /** The request was marked as having been prayed for */
case Prayed; case Prayed = 'Prayed';
/** The request was updated */ /** The request was updated */
case Updated; case Updated = 'Updated';
/** The request was marked as answered */ /** The request was marked as answered */
case Answered; case Answered = 'Answered';
} }

View File

@ -0,0 +1,64 @@
<?php declare(strict_types=1);
use BitBadger\PDODocument\{Custom, Query};
use BitBadger\PDODocument\Mapper\DocumentMapper;
use MyPrayerJournal\{Auth,History,Layout,Request,Table};
require '../../start.php';
Auth::requireUser(false);
bare_head();
$reqs = Custom::list(
Query::selectFromTable(Table::REQUEST) . " WHERE data->>'userId' = :userId AND data->>'$.history[0].action' <> 'Answered'",
[':userId' => Auth::user()['sub']], new DocumentMapper(Request::class));
if ($reqs->hasItems()) { ?>
<section id=journalItems class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3" hx-target=this
hx-swap=outerHTML aria-label="Prayer Requests"><?php
$spacer = '<span>&nbsp;</span>';
foreach ($reqs->items() as /** @var Request $req */ $req) {
$withText = array_filter($req->history, fn($hist) => isset($hist->text));
$text = $withText[array_key_first($withText)]->text; ?>
<div class=col>
<div class="card h-100">
<div class="card-header p-0 d-flex" role=toolbar><?php
page_link("/request/edit?id=$req->id", icon('edit'),
['class' => 'btn btn-secondary', 'title' => 'Edit Request']); ?>
<?=$spacer?>
<button type=button class="btn btn-secondary" title="Add Notes" data-bs-toggle=modal
data-bs-target=#notesModal hx-get="/components/request/add-notes?id=<?=$req->id?>"
hx-target=#notesBody hx-swap=innerHTML><?=icon('comment');?></button>
<?=$spacer?>
<button type=button class="btn btn-secondary" title="Snooze Request" data-bs-toggle=modal
data-bs-target=#snoozeModal hx-get="/components/request/snooze?id=<?=$req->id?>"
hx-target=#snoozeBody hx-swap=innerHTML><?=icon('schedule');?></button>
<div class=flex-grow-1></div>
<button type=button class="btn btn-success w-25" hx-patch="/request/prayed?id=<?=$req->id?>"
title="Mark as Prayed"><?=icon('done');?></button>
</div>
<div class=card-body>
<p class=request-text><?=htmlentities($text);?>
</div>
<div class="card-footer text-end text-muted px-1 py-0">
<em>last activity <?=$req->history[0]->asOf?></em>
<?php /*
TODO: relative time
[] [
match req.LastPrayed with
| Some dt -> str "last prayed "; relativeDate dt now tz
| None -> str "last activity "; relativeDate req.AsOf now tz
] */ ?>
</div>
</div>
</div><?php
} ?>
</section><?php
} else {
Layout::noResults('No Active Requests', '/request/edit?id=new', 'Add a Request', <<<'TEXT'
You have no requests to be shown; see the &ldquo;Active&rdquo; link above for snoozed or deferred requests, and
the &ldquo;Answered&rdquo; link for answered requests
TEXT);
}
bare_foot();

48
src/public/journal.php Normal file
View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
use MyPrayerJournal\Auth;
require '../start.php';
Auth::requireUser();
$user = Auth::user();
$name = $user['first_name'] ?? 'Your';
page_head('Welcome'); ?>
<article class="container-fluid mt-3">
<h2 class=pb-3><?=$name?><?=$name == 'Your' ? '' : '&rsquo;s'?> Prayer Journal</h2>
<p class="pb-3 text-center"><?php
page_link('/request/new/edit', icon('add_box') . ' Add a Prayer Request', ['class' => 'btn btn-primary']); ?>
<p hx-get=/components/journal-items hx-swap=outerHTML hx-trigger=load hx-target=this>
Loading your prayer journal&hellip;
<div id=notesModal class="modal fade" tabindex=-1 aria-labelledby=nodesModalLabel aria-hidden=true>
<div class="modal-dialog modal-dialog-scrollable">
<div class=modal-content>
<div class=modal-header>
<h5 class=modal-title id=nodesModalLabel>Add Notes to Prayer Request</h5>
<button type=button class=btn-close data-bs-dismiss=modal aria-label=Close></button>
</div>
<div class=modal-body id=notesBody></div>
<div class=modal-footer>
<button type=button id=notesDismiss class="btn btn-secondary" data-bs-dismiss=modal>Close</button>
</div>
</div>
</div>
</div>
<div id=snoozeModal class="modal fade" tabindex=-1 aria-labelledby=snoozeModalLabel aria-hidden=true>
<div class="modal-dialog modal-sm">
<div class=modal-content>
<div class=modal-header>
<h5 class=modal-title id=snoozeModalLabel>Snooze Prayer Request</h5>
<button type=button class=btn-close data-bs-dismiss=modal aria-label=Close></button>
</div>
<div class=modal-body id=snoozeBody></div>
<div class=modal-footer>
<button type=button id=snoozeDismiss class="btn btn-secondary" data-bs-dismiss=modal>Close</button>
</div>
</div>
</div>
</div>
</article><?php
page_foot();

View File

@ -7,5 +7,5 @@ require '../../../start.php';
Auth::client()->exchange($_ENV['AUTH0_BASE_URL'] . '/user/log-on/success'); Auth::client()->exchange($_ENV['AUTH0_BASE_URL'] . '/user/log-on/success');
// TODO: get the possible redirect URL // TODO: get the possible redirect URL
header('Location: /'); header('Location: /journal');
exit(); exit();

View File

@ -1,7 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
use BitBadger\PDODocument\{Configuration, Definition, Mode};
use Dotenv\Dotenv; use Dotenv\Dotenv;
use MyPrayerJournal\{Auth, Layout}; use MyPrayerJournal\{Auth, Layout, Table};
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
@ -16,6 +17,10 @@ if (!is_null($auth0_session)) {
$_SESSION['user_id'] = $auth0_session->user['sub']; $_SESSION['user_id'] = $auth0_session->user['sub'];
} }
Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']);
Configuration::$mode = Mode::SQLite;
Definition::ensureTable(Table::REQUEST);
/** /**
* Is this an htmx request? * Is this an htmx request?
* *
@ -33,6 +38,16 @@ function page_link(string $href, string $text, array $attrs = []): void
echo ">$text</a>"; echo ">$text</a>";
} }
/**
* Generate a material icon
*
* @param string $name The name of the material icon
* @return string The material icon wrapped in a `span` tag
*/
function icon(string $name): string {
return "<span class=material-icons>$name</span>";
}
function page_head(string $title): void function page_head(string $title): void
{ {
Layout::htmlHead($title); Layout::htmlHead($title);
@ -42,6 +57,11 @@ function page_head(string $title): void
echo '<main role=main>'; echo '<main role=main>';
} }
function bare_head(): void
{
echo '<!DOCTYPE html><html lang=en><head><title></title></head><body>';
}
function page_foot(): void function page_foot(): void
{ {
echo '</main>'; echo '</main>';
@ -51,3 +71,8 @@ function page_foot(): void
} }
echo '</body></html>'; echo '</body></html>';
} }
function bare_foot(): void
{
echo '</body></html>';
}