# P4-club-admin — screen inventory

| | |
| --- | --- |
| Title (ar) | إدارة النادي |
| Title (en) | The club admin surface |
| Owning epic | D4.0 |
| Reviewer | Zayed |
| Timebox | 3 days |
| Opened | 2026-09-18 |
| Reviewed | — |
| Locked | — |
| Status | draft |

## The question

Everything a club owns is here: who it is, who may act for it, how it is
organised, who is on its roster and under what contract, which competitions it
has entered and who it has filed for them. It is the largest surface in the
product and the only one where the club is writing rather than reading, so it is
also the one place where a refusal is a normal Tuesday rather than an accident.

Three facts shape all ten screens:

- **A club under review browses and transacts nothing.** That is not a footnote
  on one screen; it is the condition every screen in this area is read under, so
  it is a switch and not a state, and every screen renders both sides of it.
- **Almost everything here is season-scoped.** The structure, the roster, the
  entries and the squad list all mean nothing until a season is named. S1
  settled that the club is an area with a tab strip and its name and status sit
  at the head of every screen in it; it did not settle whether the *season* sits
  there too. That is the live remainder, and it is this prototype's D1.
- **A refusal here has a rule and usually a date.** A squad refusal is the
  clearest example in the whole product: four rules, three of which refuse
  somebody in this cast today, one of which does not, and a documented exception
  that lifts one of them.

The sporting catalogue — federations, seasons, age categories, tournaments,
windows — is here too, because the screens that consume it are here. It is
reference data of exactly P3's shape and it renders P3's answers rather than
re-asking them; what it settles is what a *season* and a *window* are, since
every date this area computes comes from those two rows.

## The epic's D1 was already answered, and is replaced

The owning epic asks, as its first decision, "the club shell: a dedicated
`/club` area with its own navigation vs club screens inside the main
navigation".

**That is S3, and S1 locked it on 18 September 2026**: the club is *an area with
a tab strip*, with its name and status at the head of every screen inside it —
and not as a preference, but because under the flat answer the structure and
roster screens lost the club's status band completely and a pending club reached
its roster with nothing telling it that it could not transact. That is precisely
the condition this prototype is built around. A locked decision is superseded by
a new prototype, never reopened inside one, so **this prototype renders the
locked answer and does not offer the rejected one as a switch.**

In its place, D1 here is the question the locked answer leaves open and which
every screen in the area runs into: **where the season is chosen.** Four of the
ten screens are season-scoped. If the season belongs to the area, it sits in the
same head as the club's name and status and follows the reader from the
structure to the roster to the entries to the squad list. If it belongs to the
screen, each screen carries its own control and two screens can disagree about
which season is being read. Both are built, both are switchable, and the cost of
each is visible on the screen rather than argued in a document.

## Decisions to settle

| Id | Decision | Options built | Switch |
| --- | --- | --- | --- |
| **D1** | **Where the season is chosen.** The structure, the roster, the entries and the squad list are all season-scoped. Is the season chosen **once for the club area**, in the same head that carries the club's name and status, or **re-chosen on each screen**? | a) once in the area head, carried across the area — `season=area` b) a control on each screen — `season=screen` | `season` |
| D2 | **The structure page.** A club is sections → teams → the people posted to them, and teams exist per season. Is it **one tree** that shows the whole shape at once, or **season-filtered tables**, one per thing? | a) a tree — `structure=tree` b) tables — `structure=tables` | `structure` |
| **D3** | **How a squad refusal is shown, and where a documented exception is entered.** Three of the four squad rules refuse somebody on this club's roster today. Is the refusal **on the row of the person refused**, or in **a rules panel** beside the list that says which rule caught whom? | a) inline on the row — `refusal=row` b) a rules panel — `refusal=panel` | `refusal` |
| D4 | **Attach or create.** Adding a person to the roster is either finding one who already exists or creating one who does not, and the club does not know which before it looks. **One search box that offers creation** when it finds nothing, or **two separate paths** chosen up front? | a) one box — `attach=one` b) two paths — `attach=two` | `attach` |

### What the reviewer must answer

- **D1** — Not "does the club have an area"; that is locked and both variants
  render it. The question is whether the **season** joins the club's name and
  status in the area's head. In the area's head it is one choice, stated once,
  and every season-scoped screen inherits it — at the cost of a control at the
  top of screens that are not season-scoped at all (the club profile, the
  members). On each screen it is local and unmistakable — at the cost of a
  reader who reads the 2025/26 structure, moves to the roster and is silently
  back in 2026/27. Answer: which, and if it is the area, whether a screen that
  is *not* season-scoped hides the control or leaves it standing.
- **D2** — A tree shows that فريق تحت 17 has no head coach in the same glance
  that shows it exists, because the gap is a hole in the shape. Tables show one
  thing at a time and are the only variant where "staff" can be a list with
  columns — post, team, whether the person is public — which is what the club
  actually edits. Answer: which, and whether the answer changes when a club has
  one section rather than two.
- **D3** — This is the review pattern arriving where the decision is taken by a
  rule and not by a person, and **it reuses P3's pattern verbatim**: a queue of
  candidates, the evidence on each, a decision that names its reason. Inline
  refusals put the reason where the name is, and a club with three refusals sees
  three separate sentences that each repeat the rule. A rules panel states each
  rule once and names whom it caught, and costs the reader a look away from the
  row. Answer: which, and **where the documented exception is entered** — the
  prototype puts it on the refusal itself, because an exception with no refusal
  in front of it is a form nobody can fill in correctly.
- **D4** — One box is one journey and it is honest about the order things happen
  in: you look, and only then do you know. It also makes creating a person the
  *bottom* of a result list, which is where an accidental duplicate comes from.
  Two paths make creation deliberate and make the search results the only thing
  on the search screen. Answer: which, and whether a club may create a person at
  all without at least one identifying document — because every person a club
  creates is a profile somebody may later claim, and P3 reviews those claims.

## Screens

`p4.js` renders the chrome only, and is `p3.js` with this prototype's switches
and its own `HERE`. The navigation list, the grouping, the two badges and the
absent-not-disabled rule are identical, because they are S1's and not a
prototype's to vary. The one thing this file adds is the club area's head: the
club's name and its status, and — under D1's first answer — the season beside
them.

| # | Screen | File | Purpose | Actor | Settles |
| --- | --- | --- | --- | --- | --- |
| 1 | Club profile and members | `index.html` | The club's registration record and the people who may act for it, including an invitation nobody has answered. The area's entry screen, and where "one club at a time per person" is stated. | ناصر العمري / فهد الزهراني | D1 |
| 2 | Club registration | `register.html` | What is asked of a club that is registering, what happens next, and the two refusals the form can return — a registration number already held, and an account that is already in a club. | فهد الزهراني | — (the pending state) |
| 3 | Club structure | `structure.html` | Sections, teams for the chosen season, and the staff posted to them. The gap — a team with no head coach — is the point. | ناصر العمري | **D2**, D1 |
| 4 | Roster and contracts | `roster.html` | Twenty-four people, three contract queues, one injury, and three rows with no contract recorded. | ناصر العمري | D1 |
| 5 | Attach or create a person | `attach.html` | Adding somebody to the roster, both ways round, with the refusal that a person another club holds under contract is not attached but approached. | ناصر العمري | **D4** |
| 6 | Contract and injury status | `contract.html` | One person's record at the club: the contract change as a new record rather than an edit, the history that is never deleted, and the injury nobody outside the club reads. | ناصر العمري | — (the contract-change shape) |
| 7 | Tournament entries | `entries.html` | Which team is entered in what, for the chosen season, and why an entry is never deleted. | ناصر العمري | D1 |
| 8 | The squad list | `squad.html` | Eighteen entered of a maximum twenty-five, three candidates each refused by a different rule, and the documented exception that lifts one of them. | ناصر العمري | **D3**, D1 |
| 9 | The sporting catalogue | `catalogue.html` | Federations, seasons, age categories, tournament editions and registration windows — the rows every date on the other nine screens is computed from. | هند الشمري | — (what a season and a window are) |
| 10 | A club's page, signed in | `club.html` | The screen the S1 review found missing: the club directory in the signed-in navigation opened the anonymous visitor page. This is the signed-in twin. | ناصر العمري reading نادي النهضة | — (the missing screen) |

### Why ten, and not fewer

Each of the ten either carries a decision or carries a state no other screen can
reach. `register.html` exists because the pending club's whole journey starts
there and because "one club at a time per person" has no other home.
`contract.html` exists because a contract change is the one write in this area
that must not be an edit-in-place, and because injury status has to be shown
somewhere that proves it is club-internal. `club.html` exists because the S1
review listed it as missing and its phase is this one. `catalogue.html` exists
because the navigation already points at it and because a season and a window
are the two rows the rest of the prototype computes from.

### The review pattern, reused

P3 settled the review pattern and wrote it down in five points. The squad list is
this prototype's queue of decisions and **is built from those five points without
changing them**:

1. The candidate list is a queue of people waiting on a decision, each row
   carrying who they are and what stands in their way. No count that is not the
   length of the list or the size of the squad.
2. Each row carries its evidence — the contract, the date of birth, the entry
   another club already filed — beside what the entry would become.
3. A decision is two buttons and a reason. Entering needs none. **An exception
   requires a documented reason**, in free text with a document named, and the
   federation and the club both read it verbatim.
4. A decided row stays readable with its decision, its reason, its author and
   its date. An entry is never deleted.
5. The decision is recorded against a person, and that name stays in the
   "last edit by, and when" column for the rest of the record's life.

The one thing P4 adds, and hands back to P3 as a question: here the refusal comes
from **a rule, not a reviewer**, and the reason is therefore already written. D3
is about where that pre-written reason goes.

## Switches carried in the URL

| Parameter | Values | Screens | What it changes |
| --- | --- | --- | --- |
| `locale` | `ar` (default), `en` | all | handled by `shared/proto.js` |
| `theme` | `light` (default), `dark` | all | handled by `shared/proto.js` |
| `state` | see below | all | handled by `shared/proto.js` |
| `season` | `area` (default), `screen` | the season-scoped screens, and the area head everywhere | **D1**: the season chosen once for the area, or once per screen |
| `structure` | `tree` (default), `tables` | `structure.html` | **D2**: a tree, or season-filtered tables |
| `refusal` | `row` (default), `panel` | `squad.html` | **D3**: the refusal on the row, or a rules panel |
| `attach` | `one` (default), `two` | `attach.html` | **D4**: one search box, or two paths |
| `club` | `approved` (default), `pending` | all club screens | **Not a decision.** Which club is signed in — نادي الصحراء under ناصر العمري, or نادي واحة الخرج under فهد الزهراني. It is a switch and not a state because a club's standing is the condition every screen is read under, not something one request returns. |
| `seat` | `owner` (default), `member` | `index.html`, and the club's tab strip on every club screen | **Not a decision.** نادي الصحراء's two seats: ناصر العمري, who owns the account, and **عمر الدخيل**, who is a member and does not. It composes with `club`: the pending club has no second seat, because فهد الزهراني registered it alone. The rule it renders is the owner-only entry being **absent**, which no fixture could show before. |
| `mobile` | `drawer` (default), `bar` | all | **Inherited from P2's D5.** Carried so P4's screens can be shot at 390 under whatever P2 settles. |
| `role` | `club` (default here), `pro`, `agent`, `admin` | all | Who is signed in, and therefore which entries exist at all. It changes the navigation, never the screen: a refused route is the `no-permission` **state**. |

`?drawer=open` opens the mobile drawer, as in P2 and P3, so a shot of it is
reproducible from an address.

## States per screen

| Screen | default | loading | empty | error | partial | no-permission | What the non-obvious ones are, and what is deliberately absent |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `index.html` | ✓ | ✓ | | ✓ | ✓ | ✓ | `partial`: an invitation sent to سارة الحمد on 13 سبتمبر 2026 and unanswered, so the club believes it has three members and has two. **`empty` is absent and cannot be reached**: a club always has at least its account owner, so an empty members list is not a state this screen has. |
| `register.html` | ✓ | ✓ | | ✓ | ✓ | ✓ | `partial`: the form saved with the licence number missing, which cannot be submitted; `error`: **the registration number 1010-778-2019 is already held by نادي الصحراء** — a refusal, not a failure; `no-permission`: ناصر العمري opening this screen, because a person belongs to one club at a time. **`empty` is absent**: a form is never empty of its fields. |
| `structure.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: a club with no section yet — **and it is role-aware**: an approved club is told to create one, a club under review is told the truth, which is that it cannot; `partial`: فريق تحت 17 exists with no head coach, so the structure is real and incomplete at once. |
| `roster.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: a roster with nobody on it, **role-aware** in the same way; `partial`: the three with no contract recorded, which is S1's queue and the line it put on the landing. |
| `attach.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: the search matched nobody, which is the exact moment creation is offered and therefore where D4 is decided; `error`: **attaching فيصل الدوسري is refused** because نادي النهضة holds him to 31 ديسمبر 2026 — the rule, the date it changes, and the one thing to do instead; `partial`: a person the club created rather than found, whose record is a stub until somebody claims it — **and is not on the roster**, which is a stated decision and not an omission: creating a record and attaching a person are two acts, so the roster stays at twenty-four and the no-contract rows stay at three until the club attaches him. |
| `contract.html` | ✓ | ✓ | | ✓ | ✓ | ✓ | `error`: a contract change refused because its start date overlaps the contract it replaces; `partial`: an injury recorded with no expected return date. **`empty` is absent**: a person's record always holds the person. |
| `entries.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: the chosen season has no entries — which is what 2027/28 looks like today, and the clearest thing the season control does; `error`: entering فريق كرة السلة in a football competition — **a refusal with no date, because this rule never changes**; `partial`: an entry accepted whose squad list has not been filed. |
| `squad.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: the كأس الاتحاد entry, which has no list yet; `error`: **the amendment is refused because the window closed on 31 أغسطس 2026 and the next opens 1 يناير 2027**; `partial`: the same list read after a documented exception was recorded for راكان العتيبي — eighteen entered, one ready under a documented exception, and two still refused, and the exception readable with its author and its date. |
| `catalogue.html` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `empty`: an age-category catalogue with nothing in it, so no team can be created anywhere on the platform; `error`: deleting نسخة 2026/27 of دوري المحترفين is refused because sixteen clubs hold entries in it and an entry is never deleted; `partial`: a tournament edition created with no registration window, so no club can enter it and nothing says why. |
| `club.html` | ✓ | ✓ | ✓ | ✓ | ✓ | | `empty`: نادي صقور الرياض لكرة السلة, which publishes no team, so a reader opens onto the club and nothing else; `partial`: a club whose public profile module is not active, so a signed-in reader gets the registry facts and no squad. **`no-permission` is absent and that is the finding**: reading a club's page needs no entitlement, and a club under review reads it like anyone else — browsing is exactly what it may do. |

Every state named in a footnote on a screen is a state that renders on that
screen. Where a state is absent the table says so and says why; none of the ten
carries a state invented to fill a column.

## Fixture world

The Stage 2 cast in [`../FIXTURES.md`](../FIXTURES.md). What this prototype
added to it, before using it, is marked **new**:

| | |
| --- | --- |
| Signed in, `club=approved` | **ناصر العمري** — account owner of **نادي الصحراء**, معتمد |
| Signed in, `club=pending` | **فهد الزهراني** — account owner of **نادي واحة الخرج**, قيد المراجعة since 14 سبتمبر 2026 |
| The season | **2026/27 — 1 أغسطس 2026 إلى 30 يونيو 2027**; 2025/26 is the season past; the winter window opens **1 يناير 2027** |
| Sections and teams | two sections; four teams in 2026/27 and three in 2025/26 — **new**, and it is what the season control actually changes |
| Staff | the five in the cast; **فريق تحت 17 has no head coach** — **new** |
| The roster | 24, of whom nine were already named; **راكان العتيبي (16) and بسام الزامل (entered by نادي صقور الحجاز on 10 أغسطس 2026)** are **new**, and exist only to give two squad rules a person to refuse |
| The injury | **عمر الشهراني** — رباط الكاحل، 12 سبتمبر 2026، العودة المتوقعة 20 أكتوبر 2026 — **new**, and the only injury in the cast |
| The squad list | **18 of a maximum 25**, submitted 20 أغسطس 2026 — **a stated correction to the cast**, which read 22 and could not be squared with a roster of 24 |
| Today | 18 سبتمبر 2026 |

### Creating a record does not put the person on the roster

`attach.html`'s `partial` state used to announce that the record had been created
**and added to the roster**, give عبدالرحمن الخييل no contract, and then link on as
«بلا عقد مسجّل — ثلاثة» — which asserts a roster of twenty-five with four
no-contract rows and denies it in the same breath, while `roster.html`,
`squad.html` and `entries.html` all say three.

Settled the way the cast already reads: `FIXTURES.md` says of both
attach-or-create records that **neither is on anybody's roster**, so creation
makes a profile on the platform and nothing more. Attaching is the other act the
screen offers, and the screen now says so on the record — «في قائمة النادي: لا،
لم يُضَمّ بعد».

Note what this deliberately does **not** claim: a roster row is not conditional on
a contract. Three people sit on this roster with no contract recorded, so the
contract is not what admits anyone, and the copy says two acts rather than a
sequence.

### The one contradiction inside the shared cast, and what was done about it

The cast said the football squad list held **22 of a maximum 25** and that the
roster held **24**, of whom two have no contract recorded, two are basketball
players, one is sixteen and one was already entered elsewhere. Twenty-two plus
six is twenty-eight. The number that had to give was the squad list, because the
roster's twenty-four is S1's and is quoted on its landing; the squad list is
quoted nowhere but here. It is **18**, the correction is written into
`FIXTURES.md` in the open rather than hidden, and the arithmetic now closes
exactly.

The consequence is worth stating because it is a design finding rather than a
bookkeeping one: **the cap of twenty-five is not the rule that binds this club.**
It has seven seats free and still cannot fill three of them, because every
remaining candidate is refused by a different rule. The screen therefore draws
the cap as remaining capacity and the other three rules as refusals, rather than
inventing seven more people so that a fourth refusal could be drawn.

## Out of scope

- **The club shell.** S1 settled it — an area with a tab strip, the name and the
  status at the head of every screen inside it. It is rendered here and it is not
  re-asked; see "The epic's D1 was already answered" above.
- **Approving a club.** P3 owns the club approval queue and built it from its own
  review pattern. This prototype owns the two sides the club sees: the form it
  sends and the state it waits in.
- **The anonymous club page.** P6's. This prototype owns the signed-in twin only.
- **Needs and applications**, and **approaches**. P5's, and the roster links to
  them rather than restating them.
- **Advertising spaces, modules and the plan.** P6's, and "الوحدات والاشتراك"
  stays the account owner's alone in this prototype's navigation as in every
  other.
- **The list shape of a reference catalogue.** P3's D2. `catalogue.html` renders
  P3's table answer and does not offer the alternative; if P3's review chooses
  the other, this screen follows it.
- **Anything with a price.** Version one is free for everyone.

## Open questions

- **A contract change is drawn as a new record with the old one kept**, because
  the prototype had to pick one and because "entries are never deleted" points
  that way. Whether a correction — a typo in a date — is also a new record, or an
  edit with an audit line, is not settled and changes what the contract endpoint
  accepts.
- **May a club create a person with no document at all?** The prototype lets it,
  and marks the result a stub that reads as unverified in the market until
  somebody claims it. Every person a club creates is a profile P3 may later have
  to adjudicate, so the cost of the easy answer lands on somebody else's queue.
- **Who may act for a club?** Membership is still one owner flag and nothing
  else, carried from S1. The roster, the squad list and the catalogue are three
  very different writes to hand to the same undifferentiated "member".
- **An injury is club-internal here.** Whether it reaches the market at the
  private tier — a club reading a profile it is about to approach — is not
  settled, and S1 settled only that it is never public.
- **Does a squad entry follow the person or the club?** بسام الزامل is entered
  for نادي صقور الحجاز in an edition he no longer plays in. The rule refuses his
  entry for نادي الصحراء; nothing here says whether صقور الحجاز's entry is amended,
  and the window means it cannot be until January anyway.

## Not built

Empty at the time of writing.
