'; } private static function field(string $name,string $label,string $type='text',string $value='',int $max=128): string { $id=$name.'_'.bin2hex(random_bytes(3)); return '
'; } public function help(): void { $this->displayContent('

Using your club workspace

Open Clubs and dues to view clubs assigned to your account. Club rosters and your season dues are available inside each club.

Site administrators create clubs, assign existing accounts, add seasons, and record dues and received payments. Contact your club administrator for membership or account assistance.

'); } public function home(): void { $service=new RosterService();$html='

Club workspace

Club access requires a verified email. Verify your email or request another link.

'; if(!$service->ready()){ $html.=$service->administrator()?'

Club setup is ready to install. Run the database migrations described in PRODUCTION-FIXES.md.

':'

Your club workspace is being prepared. Please contact the site administrator.

'; }else{ $clubs=$service->clubs(); if(!$clubs)$html.='

You are not assigned to an active club yet. A site administrator can create clubs and assign memberships.

'; foreach($clubs as $club)$html.='

'.self::h($club['club_name']).'

'.self::h(ucfirst($club['club_status'])).'

Open club
'; $html.=''.($service->administrator()?'Manage clubs and memberships':'View my clubs and dues').''; } $this->displayContent($html.'
'); } private function financialHistory(int $club,bool $active,bool $admin): string { $history=(new LedgerService())->history($club);$html='

Payment history

'; if($admin)$html.='
'; $html.='
'; foreach($history['payments'] as $p){ $html.=''; } $html.='
PaymentMemberDateAmountMethod / referenceRecorded byStatus
#'.(int)$p['payment_serial'].''.self::h($p['member_name']).''.self::h($p['paid_on']).'$'.number_format($p['amount_cents']/100,2).''.self::h($p['payment_method'].' '.$p['payment_reference']).''.self::h($p['actor']??'Historical record').''; if($p['reversal_serial'])$html.='Reversed: '.self::h($p['reversal_reason'].' by '.$p['reversal_actor'].' on '.$p['reversed_at']); elseif($admin&&$active)$html.='
Reverse incorrect payment

This preserves the original entry and restores the amount owed. It does not send a refund.

'.$this->form('reverse',$club).''.self::field('reason','Reversal reason','text','',500).'
'; else $html.='Received';$html.='

Dues correction history

'; } public function workspace(): void { $service=new RosterService();if(!$service->ready()){$this->home();return;} $clubRaw=$_POST['club_serial']??$_GET['club_serial']??'';$club=($clubRaw===''||$clubRaw==='0')?0:MaintenanceService::id($clubRaw); if(($_SERVER['REQUEST_METHOD']??'GET')==='POST' && ($_POST['step']??'')!==''){ $service->requireAdministrator();$this->requireCsrf(); switch($_POST['step']){ case 'club':$club=$service->saveClub($club,$_POST['club_name']??'',$_POST['club_status']??'active');break; case 'member':$service->saveMember($club,empty($_POST['membership_serial'])?0:MaintenanceService::id($_POST['membership_serial']),$_POST['identifier']??'',$_POST['membership_status']??'active',$_POST['joined_on']??'');break; case 'season':$service->addSeason($club,$_POST['season_name']??'',$_POST['starts_on']??'',$_POST['ends_on']??'');break; case 'charge':$service->charge($club,MaintenanceService::id($_POST['membership_serial']??null),MaintenanceService::id($_POST['season_serial']??null),$_POST['amount']??'');break; case 'payment':$service->payment($club,MaintenanceService::id($_POST['due_serial']??null),$_POST['amount']??'',$_POST['paid_on']??'',$_POST['request_key']??'',$_POST['payment_method']??'unspecified',$_POST['payment_reference']??'');break; case 'adjust':(new LedgerService())->adjust($club,MaintenanceService::id($_POST['due_serial']??null),$_POST['corrected_amount']??'',$_POST['reason']??'',$_POST['request_key']??'');break; case 'reverse':(new LedgerService())->reverse($club,MaintenanceService::id($_POST['payment_serial']??null),$_POST['reason']??'',$_POST['request_key']??'');break; default:throw new AppException('Unknown club action.'); } $_SESSION['roster_notice']='Changes saved.';header('Location: main.php?Xaction=Roster.workspace&club_serial='.$club,true,303);exit; } $html='

Clubs and memberships

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

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

';unset($_SESSION['roster_notice']);} $clubs=$service->clubs();$html.=''; if($service->administrator())$html.='

Create club

'.$this->form('club').self::field('club_name','Club name').'
'; if(!$club && $clubs)$club=(int)$clubs[0]['club_serial']; if(!$club){$this->displayContent($html.'

No clubs are available.

');return;} $selected=$service->club($club);$members=$service->members($club);$seasons=$service->seasons($club);$dues=$service->dues($club); $html.='

'.self::h($selected['club_name']).'

'; if($service->administrator()){ $html.='
Edit or archive club'.$this->form('club',$club).self::field('club_name','Club name','text',$selected['club_name']).'
'; } $html.='

Roster

'.($service->administrator()?'':'').''; foreach($members as $member){ $html.=''; if($service->administrator())$html.=''; $html.=''; } if(!$members)$html.=''; $html.='
NameEmailMembershipJoinedUpdate membership
'.self::h($member['user_first_name'].' '.$member['user_last_name']).''.self::h($member['user_email']).''.self::h($member['membership_status']).''.self::h($member['joined_on']).''.$this->form('member',$club).'
No members assigned.
'; if($service->administrator() && $selected['club_status']==='active')$html.='

Assign an existing account

'.$this->form('member',$club).self::field('identifier','Username or email').self::field('joined_on','Joined on','date',date('Y-m-d')).'

Create or invite accounts through Users before assigning them to a club.

'; $html.='

Seasons

'; if($service->administrator() && $selected['club_status']==='active')$html.=$this->form('season',$club).self::field('season_name','Season name','text','',100).self::field('starts_on','Starts on','date').self::field('ends_on','Ends on','date').'
'; $html.='

'.($service->administrator()?'Season dues':'My season dues').'

Amounts are in USD. Payments below are manually recorded receipts.

'.($service->administrator()?'':'').''; foreach($dues as $due){$balance=(int)$due['amount_cents']-(int)$due['paid_cents'];$html.=''; if($service->administrator()){$html.='';}$html.=''; } if(!$dues)$html.='';$html.='
MemberSeasonDuesPaidBalanceRecord received payment
'.self::h($due['user_first_name'].' '.$due['user_last_name']).''.self::h($due['season_name']).'$'.number_format($due['amount_cents']/100,2).'$'.number_format($due['paid_cents']/100,2).'$'.number_format($balance/100,2).'';if($balance>0 && $selected['club_status']==='active')$html.=$this->form('payment',$club).''.self::field('amount','Payment (USD)','text').self::field('paid_on','Received on','date',date('Y-m-d')).'';if($selected['club_status']==='active')$html.='
Correct dues'.$this->form('adjust',$club).''.self::field('corrected_amount','Corrected total (USD)').self::field('reason','Reason','text','',500).'
';$html.='
No dues recorded.
'; if($service->administrator() && $selected['club_status']==='active' && $members && $seasons){ $html.='

Record season dues

'.$this->form('charge',$club).'
'.self::field('amount','Dues (USD)','text').'
'; } $html.=$this->financialHistory($club,$selected['club_status']==='active',$service->administrator()); $this->displayContent($html.''); } }