Migrate v3 data; WIP on journal items
This commit is contained in:
parent
4aa6e832c7
commit
4ea55d4d25
|
@ -1,3 +1,50 @@
|
|||
<?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;
|
||||
|
|
|
@ -22,6 +22,16 @@ class Auth
|
|||
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
|
||||
*
|
||||
|
@ -50,4 +60,20 @@ class Auth
|
|||
header('Location: ' . self::client()->logout($_ENV['AUTH0_BASE_URL']));
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -13,4 +15,11 @@ class History
|
|||
* @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 jsonSerialize(): mixed
|
||||
{
|
||||
$values = ['asOf' => $this->asOf, 'action' => $this->action->value];
|
||||
if (!is_null($this->text)) $values['text'] = $this->text;
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,4 +114,15 @@ class Layout
|
|||
</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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,23 @@
|
|||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* The recurrence for a prayer request
|
||||
*/
|
||||
class Recurrence
|
||||
class Recurrence implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param RecurrencePeriod $period The recurrence period
|
||||
* @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 jsonSerialize(): mixed
|
||||
{
|
||||
$values = ['period' => $this->period->value];
|
||||
if (!is_null($this->interval)) $values['interval'] = $this->interval;
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@ namespace MyPrayerJournal;
|
|||
/**
|
||||
* The type of recurrence a request can have
|
||||
*/
|
||||
enum RecurrencePeriod
|
||||
enum RecurrencePeriod: string
|
||||
{
|
||||
/** Requests, once prayed, are available again immediately */
|
||||
case Immediate;
|
||||
case Immediate = 'Immediate';
|
||||
|
||||
/** 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 */
|
||||
case Days;
|
||||
case Days = 'Days';
|
||||
|
||||
/** Requests, once prayed, appear again in a number of weeks */
|
||||
case Weeks;
|
||||
case Weeks = 'Weeks';
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@ namespace MyPrayerJournal;
|
|||
|
||||
use BitBadger\PDODocument\{DocumentException, Find};
|
||||
use Exception;
|
||||
use JsonSerializable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* A prayer request
|
||||
*/
|
||||
class Request
|
||||
class Request implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @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 $showAfter The date/time the current recurrence period is over (null = immediate)
|
||||
* @param Recurrence $recurrence The recurrence for this request
|
||||
* @param array|History[] $history The history of this request
|
||||
* @param array|Note[] $notes Notes regarding this request
|
||||
* @param History[] $history The history of this request
|
||||
* @param Note[] $notes Notes regarding this request
|
||||
* @throws Exception If the ID generation fails
|
||||
*/
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@ namespace MyPrayerJournal;
|
|||
/**
|
||||
* An action taken on a prayer request
|
||||
*/
|
||||
enum RequestAction
|
||||
enum RequestAction: string
|
||||
{
|
||||
/** The request was created */
|
||||
case Created;
|
||||
case Created = 'Created';
|
||||
|
||||
/** The request was marked as having been prayed for */
|
||||
case Prayed;
|
||||
case Prayed = 'Prayed';
|
||||
|
||||
/** The request was updated */
|
||||
case Updated;
|
||||
case Updated = 'Updated';
|
||||
|
||||
/** The request was marked as answered */
|
||||
case Answered;
|
||||
case Answered = 'Answered';
|
||||
}
|
||||
|
|
64
src/public/components/journal-items.php
Normal file
64
src/public/components/journal-items.php
Normal 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> </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 “Active” link above for snoozed or deferred requests, and
|
||||
the “Answered” link for answered requests
|
||||
TEXT);
|
||||
}
|
||||
|
||||
bare_foot();
|
48
src/public/journal.php
Normal file
48
src/public/journal.php
Normal 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' ? '' : '’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…
|
||||
<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();
|
||||
|
|
@ -7,5 +7,5 @@ require '../../../start.php';
|
|||
Auth::client()->exchange($_ENV['AUTH0_BASE_URL'] . '/user/log-on/success');
|
||||
|
||||
// TODO: get the possible redirect URL
|
||||
header('Location: /');
|
||||
header('Location: /journal');
|
||||
exit();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use BitBadger\PDODocument\{Configuration, Definition, Mode};
|
||||
use Dotenv\Dotenv;
|
||||
use MyPrayerJournal\{Auth, Layout};
|
||||
use MyPrayerJournal\{Auth, Layout, Table};
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
|
@ -16,6 +17,10 @@ if (!is_null($auth0_session)) {
|
|||
$_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?
|
||||
*
|
||||
|
@ -33,6 +38,16 @@ function page_link(string $href, string $text, array $attrs = []): void
|
|||
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
|
||||
{
|
||||
Layout::htmlHead($title);
|
||||
|
@ -42,6 +57,11 @@ function page_head(string $title): void
|
|||
echo '<main role=main>';
|
||||
}
|
||||
|
||||
function bare_head(): void
|
||||
{
|
||||
echo '<!DOCTYPE html><html lang=en><head><title></title></head><body>';
|
||||
}
|
||||
|
||||
function page_foot(): void
|
||||
{
|
||||
echo '</main>';
|
||||
|
@ -51,3 +71,8 @@ function page_foot(): void
|
|||
}
|
||||
echo '</body></html>';
|
||||
}
|
||||
|
||||
function bare_foot(): void
|
||||
{
|
||||
echo '</body></html>';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user