Notes / snooze, update data migration

- WIP on finishing out request list
This commit is contained in:
Daniel J. Summers 2024-06-22 20:23:37 -04:00
parent 3ebb03d470
commit 2369827033
11 changed files with 189 additions and 24 deletions

View File

@ -527,9 +527,9 @@ let routes = [
subRoute "/components/" [ subRoute "/components/" [
GET_HEAD [ GET_HEAD [
route "journal-items" Components.journalItems // done route "journal-items" Components.journalItems // done
routef "request/%s/add-notes" Components.addNotes routef "request/%s/add-notes" Components.addNotes // done
routef "request/%s/item" Components.requestItem routef "request/%s/item" Components.requestItem // not used
routef "request/%s/notes" Components.notes routef "request/%s/notes" Components.notes // done
routef "request/%s/snooze" Components.snooze routef "request/%s/snooze" Components.snooze
] ]
] ]
@ -558,7 +558,7 @@ let routes = [
] ]
POST [ POST [
route "" Request.add // done route "" Request.add // done
routef "/%s/note" Request.addNote routef "/%s/note" Request.addNote // done
] ]
] ]
subRoute "/user/" [ subRoute "/user/" [

View File

@ -20,12 +20,18 @@ Configuration::$pdoDSN = 'sqlite:./data/mpj.db';
Definition::ensureTable(Table::REQUEST); Definition::ensureTable(Table::REQUEST);
/** Convert dates to the same format */
function convertDate(string $date): string
{
return (new DateTimeImmutable($date))->format('c');
}
foreach ($reqs as $reqJson) { foreach ($reqs as $reqJson) {
$req = json_decode($reqJson['data']); $req = json_decode($reqJson['data']);
$notes = array_map(fn(stdClass $note) => new Note($note->asOf, $note->notes), $req->notes ?? []); $notes = array_map(fn(stdClass $note) => new Note(convertDate($note->asOf), $note->notes), $req->notes ?? []);
$history = array_map(fn(stdClass $hist) => $history = array_map(fn(stdClass $hist) =>
new History( new History(
asOf: $hist->asOf, asOf: convertDate($hist->asOf),
action: RequestAction::from($hist->status), action: RequestAction::from($hist->status),
text: property_exists($hist, 'text') ? $hist->text : null), text: property_exists($hist, 'text') ? $hist->text : null),
$req->history); $req->history);
@ -37,10 +43,10 @@ foreach ($reqs as $reqJson) {
}; };
$v4Req = new Request( $v4Req = new Request(
id: $req->id, id: $req->id,
enteredOn: $req->enteredOn, enteredOn: convertDate($req->enteredOn),
userId: $req->userId, userId: $req->userId,
snoozedUntil: property_exists($req, 'snoozedUntil') ? $req->snoozedUntil : null, snoozedUntil: property_exists($req, 'snoozedUntil') ? convertDate($req->snoozedUntil) : null,
showAfter: property_exists($req, 'showAfter') ? $req->showAfter : null, showAfter: property_exists($req, 'showAfter') ? convertDate($req->showAfter) : null,
recurrence: $recur, recurrence: $recur,
history: $history, history: $history,
notes: $notes); notes: $notes);

View File

@ -3,6 +3,7 @@
namespace MyPrayerJournal; namespace MyPrayerJournal;
use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find, Mapper\DocumentMapper}; use BitBadger\PDODocument\{Custom, DocumentException, DocumentList, Find, Mapper\DocumentMapper};
use DateTimeImmutable;
use Exception; use Exception;
use JsonSerializable; use JsonSerializable;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@ -65,6 +66,30 @@ class Request implements JsonSerializable
return $this->history[0]->action == RequestAction::Answered; return $this->history[0]->action == RequestAction::Answered;
} }
/**
* Is this request currently snoozed?
*
* @return bool True if the request is snoozed, false if not
* @throws Exception If the snoozed until date/time is not valid
*/
public function isSnoozed(): bool
{
return isset($this->snoozedUntil) && new DateTimeImmutable($this->snoozedUntil) > new DateTimeImmutable('now');
}
/**
* Is this request currently not shown due to recurrence?
*
* @return bool True if the request is pending, false if not
* @throws Exception If the snoozed or show-after date/times are not valid
*/
public function isPending(): bool
{
return !$this->isSnoozed()
&& isset($this->showAfter)
&& new DateTimeImmutable($this->showAfter) > new DateTimeImmutable('now');
}
public function jsonSerialize(): mixed public function jsonSerialize(): mixed
{ {
$values = [ $values = [

View File

@ -2,8 +2,7 @@
namespace MyPrayerJournal; namespace MyPrayerJournal;
use BitBadger\PDODocument\DocumentException; use BitBadger\PDODocument\{DocumentException, DocumentList};
use BitBadger\PDODocument\DocumentList;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Exception; use Exception;
@ -44,7 +43,7 @@ class UI
['class' => 'btn btn-secondary', 'title' => 'Edit Request']); ?> ['class' => 'btn btn-secondary', 'title' => 'Edit Request']); ?>
<?=$spacer?> <?=$spacer?>
<button type=button class="btn btn-secondary" title="Add Notes" data-bs-toggle=modal <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?>" data-bs-target=#notesModal hx-get="/components/request/add-note?id=<?=$req->id?>"
hx-target=#notesBody hx-swap=innerHTML><?=self::icon('comment');?></button> hx-target=#notesBody hx-swap=innerHTML><?=self::icon('comment');?></button>
<?=$spacer?> <?=$spacer?>
<button type=button class="btn btn-secondary" title="Snooze Request" data-bs-toggle=modal <button type=button class="btn btn-secondary" title="Snooze Request" data-bs-toggle=modal
@ -110,7 +109,7 @@ class UI
public static function relativeDate(string $date): void public static function relativeDate(string $date): void
{ {
$parsed = new DateTimeImmutable($date); $parsed = new DateTimeImmutable($date);
$inZone = $parsed->setTimezone(new DateTimeZone($_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC')); $inZone = $parsed->setTimezone(new DateTimeZone($_REQUEST['time_zone']));
echo '<span title="' . date_format($inZone, 'l, F j, Y \a\t g:ia T') . '">' echo '<span title="' . date_format($inZone, 'l, F j, Y \a\t g:ia T') . '">'
. self::formatDistance('now', $parsed) . '</span>'; . self::formatDistance('now', $parsed) . '</span>';
} }
@ -172,11 +171,13 @@ class UI
public static function requestList(DocumentList $reqs): void public static function requestList(DocumentList $reqs): void
{ {
$btnClass = "btn btn-light mx-2"; $btnClass = "btn btn-light mx-2";
$restoreBtn = fn(string $id, string $link, string $title) =>
'<button class="' . $btnClass. '" hx-patch="/request/' . $link . '?id=' . $id
. '" title="' . htmlspecialchars($title) . '">' . self::icon('restore') . '</button>';
/// Create a request within the list /// Create a request within the list
/* let reqListItem now tz req = /* let reqListItem now tz req =
let isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false let isFuture instant = defaultArg (instant |> Option.map (fun it -> it > now)) false
let reqId = RequestId.toString req.RequestId let reqId = RequestId.toString req.RequestId
let isSnoozed = isFuture req.SnoozedUntil
let isPending = (not isSnoozed) && isFuture req.ShowAfter let isPending = (not isSnoozed) && isFuture req.ShowAfter
let restoreBtn (link : string) title = let restoreBtn (link : string) title =
button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ] */ ?> button [ btnClass; _hxPatch $"/request/{reqId}/{link}"; _title title ] [ icon "restore" ] */ ?>
@ -190,9 +191,11 @@ class UI
self::pageLink("/request/edit?id=$req->id", self::icon('edit'), self::pageLink("/request/edit?id=$req->id", self::icon('edit'),
['class' => $btnClass, 'title' => 'Edit Request']); ['class' => $btnClass, 'title' => 'Edit Request']);
} }
// if isSnoozed then restoreBtn "cancel-snooze" "Cancel Snooze" if ($req->isSnoozed()) {
// elif isPending then restoreBtn "show" "Show Now" echo $restoreBtn($req->id, 'cancel-snooze', 'Cancel Snooze');
} elseif ($req->isPending()) {
echo $restoreBtn($req->id, 'show', 'Show Now');
}
echo '<p class="request-text mb-0">' . $req->currentText(); echo '<p class="request-text mb-0">' . $req->currentText();
// if isSnoozed || isPending || isAnswered then // if isSnoozed || isPending || isAnswered then
// br [] // br []

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
use MyPrayerJournal\{Auth, Layout, Request};
require '../../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::requireUser(false);
$req = Request::byId($_GET['id']);
if (!$req) not_found();
Layout::bareHead(); ?>
<form hx-post="/request/note?id=<?=$req->id?>">
<div class="form-floating pb-3">
<textarea id=notes name=notes class=form-control style="min-height: 8rem;" placeholder=Notes autofocus
required></textarea>
<label for=notes>Notes</label>
</div>
<p class=text-end><button type=submit class="btn btn-primary">Add Notes</button>
</form>
<hr style="margin: .5rem -1rem">
<div id=priorNotes>
<p class="text-center pt-3">
<button type=button class="btn btn-secondary" hx-get="/components/request/notes?id=<?=$req->id?>"
hx-swap=outerHTML hx-target=#priorNotes>Load Prior Notes</button>
</div><?php
Layout::bareFoot();

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
use MyPrayerJournal\{Auth, Layout, Request, UI};
require '../../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::requireUser(false);
$req = Request::byId($_GET['id']);
if (!$req) not_found();
Layout::bareHead();?>
<p class=text-center><strong>Prior Notes for This Request</strong><?php
if (sizeof($req->notes) > 0) {
foreach ($req->notes as $note) { ?>
<p><small class=text-muted><?php UI::relativeDate($note->asOf); ?></small><br>
<?=htmlentities($note->text)?><?php
}
} else { ?>
<p class="text-center text-muted">There are no prior notes for this request<?php
}
Layout::bareFoot();

View File

@ -0,0 +1,22 @@
<?php declare(strict_types=1);
use MyPrayerJournal\{Auth, Layout, Request};
require '../../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'GET') not_found();
Auth::requireUser(false);
$req = Request::byId($_GET['id']);
if (!$req) not_found();
Layout::bareHead(); ?>
<form hx-patch="/request/snooze?id=<?=$req->id?>" hx-target=#journalItems hx-swap=outerHTML>
<div class="form-floating pb-3">
<input type=date id=until name=until class=form-control
min="<?=(new DateTimeImmutable('now'))->format('Y-m-d')?>" required>
<label for=until>Until</label>
</div>
<p class="text-end mb-0"><button type=submit class="btn btn-primary">Snooze</button>
</form><?php
Layout::bareFoot();

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
use MyPrayerJournal\{Auth, Note, Request, Table};
use BitBadger\PDODocument\Patch;
require '../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'POST') not_found();
Auth::requireUser(false);
$req = Request::byId($_GET['id']);
if (!$req) not_found();
array_unshift($req->notes, new Note((new DateTimeImmutable('now'))->format('c'), $_POST['notes']));
Patch::byId(Table::REQUEST, $req->id, ['notes' => $req->notes]);
hide_modal('notes');
http_response_code(202);

View File

@ -22,9 +22,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
recurrence: $recurrence, recurrence: $recurrence,
history: [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])])); history: [new History($now->format('c'), RequestAction::Created, $_POST['requestText'])]));
//Messages.pushSuccess ctx "Added prayer request" "/journal" //Messages.pushSuccess ctx "Added prayer request" "/journal"
header('Location: /journal'); see_other('/journal');
http_response_code(303);
exit;
case 'PATCH': case 'PATCH':
$req = Request::byId($_PATCH['requestId']); $req = Request::byId($_PATCH['requestId']);
@ -44,8 +42,5 @@ switch ($_SERVER['REQUEST_METHOD']) {
$patch['history'] = $req->history; $patch['history'] = $req->history;
Patch::byId(Table::REQUEST, $req->id, $patch); Patch::byId(Table::REQUEST, $req->id, $patch);
//Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl //Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl
// TODO: make redirect that filters out non-local URLs see_other($_PATCH['returnTo']);
header('Location: ' . $_PATCH['returnTo']);
http_response_code(303);
exit;
} }

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
use MyPrayerJournal\{Auth, Request, Table, UI};
use BitBadger\PDODocument\Patch;
require '../../start.php';
if ($_SERVER['REQUEST_METHOD'] <> 'PATCH') not_found();
Auth::requireUser(false);
$req = Request::byId($_GET['id']);
if (!$req) not_found();
$until = (new DateTimeImmutable($_PATCH['until'] . 'T00:00:00', new DateTimeZone($_REQUEST['time_zone'])))
->setTimezone(new DateTimeZone('Etc/UTC'));
Patch::byId(Table::REQUEST, $req->id, ['snoozedUntil' => $until->format('c')]);
// TODO: message
hide_modal('snooze');
UI::journal();

View File

@ -18,6 +18,8 @@ if (php_sapi_name() != 'cli') {
if (!is_null($auth0_user)) { if (!is_null($auth0_user)) {
$_SESSION['user_id'] = $auth0_user['sub']; $_SESSION['user_id'] = $auth0_user['sub'];
} }
$_REQUEST['time_zone'] = $_SERVER['HTTP_X_TIME_ZONE'] ?? 'Etc/UTC';
} }
Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']); Configuration::$pdoDSN = 'sqlite:' . implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', 'mpj.db']);
@ -35,3 +37,25 @@ function not_found(): never
http_response_code(404); http_response_code(404);
die('Not found'); die('Not found');
} }
/**
* Return a 303 redirect ("see other" - redirects to a GET)
*
* @param string $url The URL to which the browser should be redirected
*/
function see_other(string $url): never
{
header('Location: ' . (str_starts_with($url, 'http') ? '/' : $url));
http_response_code(303);
exit;
}
/**
* Add a header that instructs the browser to close an open modal dialog
*
* @param string $name The name of the dialog to be closed
*/
function hide_modal(string $name): void
{
header("X-Hide-Modal: $name");
}