Files
huntingclubroster/classes/ClubFormModals.php
jric11 b851712968
Security and workflow regressions / regression (push) Has been cancelled
updates from chat
2026-10-21 20:41:57 -04:00

61 lines
5.7 KiB
PHP

<?php
/** Move already-authorized club forms out of summary tables into labelled dialogs. */
final class ClubFormModals {
public static function render(string $html): string {
$titles=['club'=>'Edit or archive club','member'=>'Add member','invite'=>'Invite members',
'season'=>'Add season','charge'=>'Record season dues','payment'=>'Record payment',
'adjust'=>'Correct dues','reverse'=>'Reverse incorrect payment','revokeInvitation'=>'Revoke invitation',
'note_add'=>'Add note','note_edit'=>'Edit note','note_delete'=>'Delete note'];
$doc=new DOMDocument('1.0','UTF-8');$previous=libxml_use_internal_errors(true);
try{$loaded=$doc->loadHTML('<?xml encoding="UTF-8">'.$html,LIBXML_NONET);}
finally{libxml_clear_errors();libxml_use_internal_errors($previous);}
if(!$loaded)throw new RuntimeException('Cannot render club dialogs.');
$main=$doc->getElementsByTagName('main')->item(0);
if(!$main)throw new LogicException('Club summary container is missing.');
$dialogs=[];$index=0;
foreach(iterator_to_array($main->getElementsByTagName('form')) as $form){
$values=[];foreach($form->getElementsByTagName('input') as $input)$values[$input->getAttribute('name')]=$input->getAttribute('value');
$step=$values['step']??'';
if(!in_array($values['action']??'',['Roster.workspace','Roster.memberSummary'],true)||!isset($titles[$step]))continue;
$title=$step==='member'&&!empty($values['membership_serial'])?'Edit membership':$titles[$step];
$id='club-action-'.(++$index);$parent=$form->parentNode;
$trigger=$doc->createElement('button');$trigger->appendChild($doc->createTextNode($title));
foreach(['type'=>'button','class'=>'btn '.($step==='note_delete'?'btn-outline-danger':'btn-outline-primary').' me-2 mb-2','data-bs-toggle'=>'modal','data-bs-target'=>'#'.$id,'data-club-step'=>$step] as $key=>$value)$trigger->setAttribute($key,$value);
$dialog=$doc->createElement('div');
foreach(['class'=>'modal fade club-action-modal','id'=>$id,'tabindex'=>'-1','aria-labelledby'=>$id.'-title','aria-hidden'=>'true'] as $key=>$value)$dialog->setAttribute($key,$value);
$layout=$dialog->appendChild($doc->createElement('div'));$layout->setAttribute('class','modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable');
$content=$layout->appendChild($doc->createElement('div'));$content->setAttribute('class','modal-content');
$header=$content->appendChild($doc->createElement('div'));$header->setAttribute('class','modal-header');
$heading=$header->appendChild($doc->createElement('h2'));$heading->setAttribute('class','modal-title fs-5');$heading->setAttribute('id',$id.'-title');$heading->appendChild($doc->createTextNode($title));
$close=$header->appendChild($doc->createElement('button'));$close->setAttribute('type','button');$close->setAttribute('class','btn-close');$close->setAttribute('data-bs-dismiss','modal');$close->setAttribute('aria-label','Close');
$body=$content->appendChild($doc->createElement('div'));$body->setAttribute('class','modal-body');
// Keep the selected row visible in the dialog so financial/member actions are unambiguous.
$row=$parent;while($row&&$row!==$main&&!in_array($row->nodeName,['tr','li'],true))$row=$row->parentNode;
if($row&&$row!==$main&&!in_array($step,['note_edit','note_delete'],true)){
$text=$row->textContent;
if($row->nodeName==='tr')$text=implode(' · ',array_map(static fn($cell)=>trim($cell->textContent),array_slice(iterator_to_array($row->getElementsByTagName('td')),0,-1)));
$context=$body->appendChild($doc->createElement('p'));$context->setAttribute('class','text-secondary');$context->appendChild($doc->createTextNode(trim(preg_replace('/\s+/u',' ',$text))));
}
if($parent->nodeName==='details'){
$parent->parentNode->replaceChild($trigger,$parent);
foreach(iterator_to_array($parent->childNodes) as $child)if($child->nodeName!=='summary')$body->appendChild($child);
}else{
$previous=$form->previousSibling;
if($previous&&$previous->nodeName==='h4')$previous->parentNode->removeChild($previous);
$following=$form->nextSibling;
$parent->replaceChild($trigger,$form);$body->appendChild($form);
if($following&&$following->nodeName==='p')$body->appendChild($following);
}
$error=$doc->createElement('p');$error->setAttribute('class','alert alert-danger club-modal-error');$error->setAttribute('role','alert');$error->setAttribute('tabindex','-1');$error->setAttribute('hidden','hidden');$body->insertBefore($error,$body->firstChild);
$footer=$content->appendChild($doc->createElement('div'));$footer->setAttribute('class','modal-footer');
$cancel=$footer->appendChild($doc->createElement('button'));$cancel->setAttribute('type','button');$cancel->setAttribute('class','btn btn-outline-secondary');$cancel->setAttribute('data-bs-dismiss','modal');$cancel->appendChild($doc->createTextNode('Cancel'));
$dialogs[]=$dialog;
if($step==='invite')foreach(iterator_to_array($main->getElementsByTagName('a')) as $link)if($link->getAttribute('href')==='#club-invitations'){$link->parentNode->replaceChild($trigger,$link);break;}
}
foreach($dialogs as $dialog)$main->appendChild($dialog);
$script=$main->appendChild($doc->createElement('script'));$script->setAttribute('src','js/club-modals.js?version='.Base::VERSION);
return $doc->saveHTML($main);
}
}