'Roster.memberSummary','club_serial'=>$club,'membership_serial'=>$membership,'members_page'=>BoundedList::page('members'),'dues_page'=>BoundedList::page('dues'),'payments_page'=>BoundedList::page('payments'),'adjustments_page'=>BoundedList::page('adjustments'),'seasons_page'=>BoundedList::page('seasons'),'notes_page'=>BoundedList::page('notes')]); return '

Member: '.self::h($this->memberLabel).'

'; } private static function field(string $name,string $label,string $type='text',string $value='',int $max=128): string { $id='member_'.$name.'_'.bin2hex(random_bytes(3)); return '
'; } private function noteForm(int $club,int $membership,?array $note=null): string { $id='club_note_'.($note?(int)$note['note_serial']:'new'); $form=$this->form($note?'note_edit':'note_add',$club,$membership); if($note)$form.=''; return $form.'

Up to 2,000 characters. Only administrators of this club and Super Administrators can see this note.

'; } private function deleteNoteForm(int $club,int $membership,array $note): string { return $this->form('note_delete',$club,$membership).'

Delete this administrative note? This cannot be undone. A record of the deletion will remain in the audit trail.

'.self::h($note['note_text']).'
'; } public function show(): void { $club=MaintenanceService::id($_GET['club_serial']??$_POST['club_serial']??null); $membership=MaintenanceService::id($_GET['membership_serial']??$_POST['membership_serial']??null); $service=new RosterService();$member=$service->member($club,$membership);$selected=$service->club($club); $this->memberLabel=trim($member['user_first_name'].' '.$member['user_last_name']).' ('.$member['user_email'].')'; $manage=$service->manages($club);$super=(new TenantAccess())->isSuper();$active=$selected['club_status']==='active'; if(($_SERVER['REQUEST_METHOD']??'GET')==='POST'){ $this->requireCsrf();(new TenantAccess())->requireManager($club); if(!$active)throw new AppException('Reactivate the club before changing records.',409); if((int)($_POST['membership_serial']??0)!==$membership||(int)($_POST['club_serial']??0)!==$club)throw new AppException('The selected membership changed. Reload its summary.',409); switch($_POST['step']??''){ case 'member':$service->saveMember($club,$membership,'',$_POST['membership_status']??'',$_POST['joined_on']??'',$_POST['membership_role']??'member');break; case 'charge':$service->charge($club,$membership,MaintenanceService::id($_POST['season_serial']??null),$_POST['amount']??'');break; case 'payment':(new LedgerService())->payment($club,MaintenanceService::id($_POST['due_serial']??null),$_POST['amount']??'',$_POST['paid_on']??'',$_POST['request_key']??'',$_POST['payment_method']??'unspecified',$_POST['payment_reference']??'',$membership);break; case 'adjust':(new LedgerService())->adjust($club,MaintenanceService::id($_POST['due_serial']??null),$_POST['corrected_amount']??'',$_POST['reason']??'',$_POST['request_key']??'',$membership);break; case 'reverse':(new LedgerService())->reverse($club,MaintenanceService::id($_POST['payment_serial']??null),$_POST['reason']??'',$_POST['request_key']??'',$membership);break; case 'note_add':(new ClubMemberNotes())->save($club,$membership,0,$_POST['note_text']??'');break; case 'note_edit':(new ClubMemberNotes())->save($club,$membership,MaintenanceService::id($_POST['note_serial']??null),$_POST['note_text']??'',$_POST['note_version']??'');break; case 'note_delete':(new ClubMemberNotes())->delete($club,$membership,MaintenanceService::id($_POST['note_serial']??null),$_POST['note_version']??'');break; default:throw new AppException('Unknown member action.'); } $_SESSION['roster_notice']='Changes saved.'; $redirect='main.php?'.http_build_query(['Xaction'=>'Roster.memberSummary','club_serial'=>$club,'membership_serial'=>$membership,'members_page'=>BoundedList::page('members'),'dues_page'=>BoundedList::page('dues'),'payments_page'=>BoundedList::page('payments'),'adjustments_page'=>BoundedList::page('adjustments'),'seasons_page'=>BoundedList::page('seasons'),'notes_page'=>(in_array($_POST['step']??'',['note_add','note_edit','note_delete'],true)?1:BoundedList::page('notes'))]); if(in_array($_POST['step']??'',['note_add','note_edit','note_delete'],true))$redirect.='#admin-notes'; if(($_SESSION[self::AJAX]??false)&&($_SERVER['HTTP_X_HCR_MODAL']??'')==='1'){header('Content-Type: application/json');echo json_encode(['ok'=>true,'redirect'=>$redirect]);return;} header('Location: '.$redirect,true,303);return; } $dues=$service->dues($club,BoundedList::page('dues'),$membership); $history=(new LedgerService())->history($club,BoundedList::page('payments'),BoundedList::page('adjustments'),$membership); // Totals include every season, independently of the visible history pages. $stmt=$this->connect()->prepare('SELECT COALESCE(SUM(d.amount_cents+COALESCE((SELECT SUM(a.amount_cents) FROM club_due_adjustments a WHERE a.due_serial=d.due_serial),0)),0) AS dues_cents,COALESCE(SUM(COALESCE((SELECT SUM(p.amount_cents) FROM club_payments p WHERE p.due_serial=d.due_serial AND NOT EXISTS(SELECT 1 FROM club_payment_reversals r WHERE r.payment_serial=p.payment_serial)),0)),0) AS paid_cents FROM club_dues d WHERE d.club_serial=? AND d.membership_serial=?'); $stmt->execute([$club,$membership]);$totals=$stmt->fetch(); $back='main.php?'.http_build_query(['Xaction'=>'Roster.workspace','club_serial'=>$club,'members_page'=>BoundedList::page('members')]).'#roster'; $name=trim($member['user_first_name'].' '.$member['user_last_name']); $html='

'.self::h($name).'

'.self::h($member['user_email']).'

Club: '.self::h($selected['club_name']).' · '.self::h(ucfirst($selected['club_status'])).'

Membership: '.self::h(ucfirst($member['membership_status'])).' · '.self::h(ucfirst($member['membership_role'])).'
Joined: '.self::h($member['joined_on']).'

Back to club roster
'; if(!empty($_SESSION['roster_notice'])){$html.='

'.self::h($_SESSION['roster_notice']).'

';unset($_SESSION['roster_notice']);} if($manage&&$active){ $html.=''; } if($manage){ $notes=(new ClubMemberNotes())->listing($club,$membership,BoundedList::page('notes')); $html.='

Administrative notes

Visible only to this club’s administrators and Super Administrators.

'; if($active)$html.=$this->noteForm($club,$membership); $html.='
'.($active?'':'').''; foreach($notes as $note){ $html.=''; if($active)$html.=''; $html.=''; } if(!$notes)$html.=''; $html.='
NoteUpdatedByActions
'.self::h($note['note_text']).''.self::h($note['note_timestamp']).''.self::h($note['note_origin']).''.$this->noteForm($club,$membership,$note).$this->deleteNoteForm($club,$membership,$note).'
No administrative notes yet.
'.BoundedList::navigation('notes','Roster.memberSummary').'
'; } $html.='
'; foreach(['Total dues'=>(int)$totals['dues_cents'],'Payments received'=>(int)$totals['paid_cents'],'Balance due'=>(int)$totals['dues_cents']-(int)$totals['paid_cents']] as $label=>$amount) $html.='

'.$label.'

'.self::money($amount).'

'; $html.='

Amounts are in USD. Totals include all seasons and exclude reversed payments.

Season dues

'.($manage&&$active?'':'').''; foreach($dues as $due){ $balance=(int)$due['amount_cents']-(int)$due['paid_cents']; $html.=''; if($manage&&$active){ $html.=''; } $html.=''; } if(!$dues)$html.=''; $html.='
SeasonDuesPaidBalanceActions
'.self::h($due['season_name']).''.self::money((int)$due['amount_cents']).''.self::money((int)$due['paid_cents']).''.self::money($balance).''; if($balance>0)$html.=$this->form('payment',$club,$membership).''.self::field('amount','Payment (USD)').self::field('paid_on','Received on','date',date('Y-m-d')).''; $html.=$this->form('adjust',$club,$membership).''.self::field('corrected_amount','Corrected total (USD)','text',number_format((int)$due['amount_cents']/100,2,'.','')).self::field('reason','Reason','text','',500).'

This records an adjustment and preserves the original dues and payment history.

'; $html.='
No dues recorded for this membership.
'.BoundedList::navigation('dues','Roster.memberSummary').'

Payment history

'.($manage&&$active?'':'').''; foreach($history['payments'] as $payment){ $status=$payment['reversal_serial']?'Reversed: '.$payment['reversal_reason'].' ('.$payment['reversed_at'].' UTC)':'Received'; $html.=''; if($manage&&$active){ $html.=''; } $html.=''; } if(!$history['payments'])$html.=''; $html.='
PaymentDateAmountMethod / referenceStatusActions
#'.(int)$payment['payment_serial'].' · Dues #'.(int)$payment['due_serial'].''.self::h($payment['paid_on']).''.self::money((int)$payment['amount_cents']).''.self::h($payment['payment_method'].' / '.$payment['payment_reference']).''.self::h($status).''; if(!$payment['reversal_serial'])$html.=$this->form('reverse',$club,$membership).''.self::field('reason','Reversal reason','text','',500).'

This preserves the original payment and restores the amount owed. It does not issue a refund.

'; $html.='
No payments recorded for this membership.
'.BoundedList::navigation('payments','Roster.memberSummary').'

Dues correction history

'; foreach($history['adjustments'] as $adjustment)$html.=''; if(!$history['adjustments'])$html.=''; $html.='
Date (UTC)AdjustmentCorrected totalReason
'.self::h($adjustment['created_at']).''.self::money((int)$adjustment['amount_cents']).''.self::money((int)$adjustment['corrected_amount_cents']).''.self::h($adjustment['reason']).'
No dues corrections recorded for this membership.
'.BoundedList::navigation('adjustments','Roster.memberSummary').'
'; $this->displayContent($manage&&$active?ClubFormModals::render($html):$html); } }