mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
258 lines
6.7 KiB
Markdown
258 lines
6.7 KiB
Markdown
# Manual FACR Mode (Manual Club Data)
|
||
|
||
This document describes the **manual FACR mode** that replaces the automatic FACR scraping when enabled. In manual mode, all club competitions, matches, and tables are stored in the local database and managed via the admin UI or import APIs.
|
||
|
||
## 1. Enabling manual mode
|
||
|
||
Environment variable:
|
||
|
||
```env
|
||
CLUB_DATA_MODE=manual
|
||
```
|
||
|
||
Values:
|
||
|
||
- `auto` – default, uses external FACR integration.
|
||
- `manual` – uses local manual data for club info, matches, and tables.
|
||
|
||
The public endpoints and JSON shapes stay the same in both modes so the frontend and widgets work unchanged.
|
||
|
||
## 2. Data model (manual tables)
|
||
|
||
Manual mode uses dedicated tables:
|
||
|
||
- `ManualCompetition` – competition metadata for the **primary club**.
|
||
- `ManualMatch` – individual matches (fixtures + results).
|
||
- `ManualTableRow` – standings rows for each competition.
|
||
|
||
These are tied to the primary club configured in `Settings` (club ID + type), same as the automatic mode.
|
||
|
||
## 3. Admin UI
|
||
|
||
Manual data is managed under:
|
||
|
||
- `/admin/manual-data` (requires admin role)
|
||
|
||
Features:
|
||
|
||
- Create, update, delete `ManualCompetition` records.
|
||
- Download CSV templates for matches and tables.
|
||
- Import matches and tables from **CSV or Excel (XLSX)** files.
|
||
- Import matches and tables from **raw JSON payloads**.
|
||
|
||
The page shows a live overview of all manual competitions for the primary club.
|
||
|
||
## 4. Backend admin API
|
||
|
||
All admin routes are under `/api/v1/admin/manual` (behind auth + CSRF + admin role).
|
||
|
||
### 4.1 Competitions CRUD
|
||
|
||
- `GET /api/v1/admin/manual/competitions`
|
||
- `POST /api/v1/admin/manual/competitions`
|
||
- `PUT /api/v1/admin/manual/competitions/:id`
|
||
- `DELETE /api/v1/admin/manual/competitions/:id`
|
||
|
||
Payload example:
|
||
|
||
```json
|
||
{
|
||
"code": "A1A",
|
||
"name": "SATUM 5. liga mužů",
|
||
"external_id": "<competition-uuid>",
|
||
"matches_link": "https://www.fotbal.cz/souteze/turnaje/hlavni/...",
|
||
"table_link": "https://www.fotbal.cz/souteze/turnaje/table/...",
|
||
"team_count": "14"
|
||
}
|
||
```
|
||
|
||
### 4.2 CSV/XLSX templates
|
||
|
||
- `GET /api/v1/admin/manual/matches/template`
|
||
- `GET /api/v1/admin/manual/tables/template`
|
||
|
||
These return simple CSV header rows you can open in Excel / Google Sheets and then export back to CSV or XLSX.
|
||
|
||
### 4.3 File imports (CSV or XLSX)
|
||
|
||
Both endpoints accept **multipart/form-data** with a `file` field containing either:
|
||
|
||
- `.csv` text file, or
|
||
- `.xlsx` Excel workbook (first sheet is read).
|
||
|
||
**Matches import**
|
||
|
||
- `POST /api/v1/admin/manual/matches/import`
|
||
- Form field: `file`
|
||
|
||
Expected headers (columns):
|
||
|
||
- `competition_code`
|
||
- `competition_external_id`
|
||
- `round`
|
||
- `is_home`
|
||
- `opponent_name`
|
||
- `opponent_club_link`
|
||
- `external_match_id`
|
||
- `kickoff_date` (YYYY-MM-DD)
|
||
- `kickoff_time` (HH:MM)
|
||
- `score_fulltime`
|
||
- `score_halftime`
|
||
- `match_link`
|
||
- `venue`
|
||
- `note`
|
||
|
||
Behavior:
|
||
|
||
- Competition is resolved by `competition_external_id` (if present) or `competition_code`.
|
||
- `opponent_club_link` is parsed for a UUID and stored as `OpponentExternalID`.
|
||
- `external_match_id` or a UUID from `match_link` is required and used as the match key.
|
||
- If a match with the same `(competition_id, external_match_id)` exists, it is **updated**, otherwise **created**.
|
||
|
||
Response JSON:
|
||
|
||
```json
|
||
{
|
||
"imported": 10,
|
||
"updated": 5,
|
||
"errors": [
|
||
"row 3: competition not found for code='A1A' external_id=''"
|
||
]
|
||
}
|
||
```
|
||
|
||
**Tables import**
|
||
|
||
- `POST /api/v1/admin/manual/tables/import`
|
||
- Form field: `file`
|
||
|
||
Expected headers:
|
||
|
||
- `competition_code`
|
||
- `competition_external_id`
|
||
- `rank`
|
||
- `team_name`
|
||
- `team_club_link`
|
||
- `played`
|
||
- `wins`
|
||
- `draws`
|
||
- `losses`
|
||
- `score`
|
||
- `points`
|
||
|
||
Behavior:
|
||
|
||
- Competition resolution same as matches.
|
||
- `team_club_link` is parsed for a UUID and stored as `ExternalTeamID`.
|
||
- For each competition, existing rows are **deleted once** on the first row, then new `ManualTableRow` records are inserted.
|
||
|
||
Response JSON:
|
||
|
||
```json
|
||
{
|
||
"imported": 42,
|
||
"errors": [
|
||
"row 5: competition not found for code='B3A' external_id=''"
|
||
]
|
||
}
|
||
```
|
||
|
||
## 5. JSON import endpoints
|
||
|
||
These are useful for syncing from external systems or scripts.
|
||
|
||
### 5.1 Matches JSON import
|
||
|
||
- `POST /api/v1/admin/manual/matches/import-json`
|
||
|
||
Body shape:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"competition_code": "A1A",
|
||
"competition_external_id": "<competition-uuid>",
|
||
"round": "2. kolo",
|
||
"is_home": "home",
|
||
"opponent_name": "FC Opponent",
|
||
"opponent_club_link": "https://www.fotbal.cz/kluby/<uuid>",
|
||
"external_match_id": "<match-uuid>",
|
||
"kickoff_date": "2025-03-15",
|
||
"kickoff_time": "17:00",
|
||
"kickoff": "", // optional RFC3339 alternative
|
||
"score_fulltime": "2:1",
|
||
"score_halftime": "1:0",
|
||
"match_link": "https://is.fotbal.cz/public/zapasy/<uuid>",
|
||
"venue": "Kravaře - tráva",
|
||
"note": "Dohrávka"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Semantics:
|
||
|
||
- Competition resolution, opponent UUID parsing, and match keying behave exactly like the CSV/XLSX import.
|
||
- `kickoff` (RFC3339) wins over `kickoff_date` + `kickoff_time` if supplied.
|
||
- Existing matches are updated; new ones are inserted.
|
||
|
||
### 5.2 Tables JSON import
|
||
|
||
- `POST /api/v1/admin/manual/tables/import-json`
|
||
|
||
Body shape:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"competition_code": "A1A",
|
||
"competition_external_id": "<competition-uuid>",
|
||
"rank": "1.",
|
||
"team_name": "FC Example",
|
||
"team_club_link": "https://www.fotbal.cz/kluby/<uuid>",
|
||
"played": "13",
|
||
"wins": "9",
|
||
"draws": "2",
|
||
"losses": "2",
|
||
"score": "45:17",
|
||
"points": "29"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Semantics:
|
||
|
||
- Competition and team UUIDs resolved the same as in file imports.
|
||
- For each competition, existing rows are cleared once, then replaced by the new `items` list.
|
||
|
||
## 6. How public data is built
|
||
|
||
When `CLUB_DATA_MODE=manual`:
|
||
|
||
- `buildManualClubPayload` constructs a FACR-like `ClubInfo` JSON using `ManualCompetition`, `ManualMatch`, and `ManualTableRow`.
|
||
- The prefetch service writes:
|
||
- `facr_club_info.json`
|
||
- `facr_tables.json`
|
||
- `matches.json`
|
||
- Frontend pages (`/matches`, `/standings`, widgets, scoreboard, etc.) consume these JSON files in the **same shape** as automatic mode.
|
||
|
||
## 7. Logos and team matching
|
||
|
||
Logo resolution in manual mode uses the existing chain:
|
||
|
||
1. Logo API / overrides and local cache.
|
||
2. Manual club overrides.
|
||
3. FACR placeholder URLs based on external IDs.
|
||
|
||
Because opponent and team rows store external UUIDs (from `opponent_club_link` and `team_club_link`), manual data can still benefit from logo lookups and alias matching.
|
||
|
||
## 8. Summary
|
||
|
||
- Toggle `CLUB_DATA_MODE` to switch between automatic and manual data without changing the frontend.
|
||
- Use `/admin/manual-data` to manage competitions and import data.
|
||
- Import from **CSV**, **XLSX**, or **JSON** using the endpoints above.
|
||
- Manual mode is designed to mirror automatic FACR data structures so all existing widgets and pages keep working.
|