добавил возможность администраторам создавать игроков, тренеров и судей вручную
This commit is contained in:
186
app.py
186
app.py
@@ -75,6 +75,7 @@ from repositories.player_repository import (
|
||||
update_player_admin,
|
||||
update_player_photo_enabled,
|
||||
ensure_player_photo_enabled_column,
|
||||
create_player_admin,
|
||||
)
|
||||
|
||||
from repositories.referee_repository import (
|
||||
@@ -82,6 +83,7 @@ from repositories.referee_repository import (
|
||||
get_referee_by_id,
|
||||
update_referee_admin,
|
||||
get_all_referees,
|
||||
create_referee_admin,
|
||||
# replace_match_referees,
|
||||
)
|
||||
|
||||
@@ -91,6 +93,7 @@ from repositories.coach_repository import (
|
||||
search_coaches_for_admin,
|
||||
get_coach_by_id,
|
||||
update_coach_admin,
|
||||
create_coach_admin,
|
||||
)
|
||||
|
||||
from repositories.stadium_repository import (
|
||||
@@ -103,6 +106,7 @@ from repositories.team_repository import (
|
||||
search_teams_for_admin,
|
||||
get_team_by_id,
|
||||
update_team_admin,
|
||||
list_teams_for_admin_select,
|
||||
)
|
||||
|
||||
from repositories.match_event_repository import (
|
||||
@@ -894,6 +898,188 @@ def close_session(session_token: str):
|
||||
|
||||
|
||||
|
||||
|
||||
def _admin_create_optional_int(value, field_title: str, minimum: int = 0) -> int | None:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
number = int(text)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Поле «{field_title}» должно быть целым числом.") from exc
|
||||
if number < minimum:
|
||||
raise ValueError(f"Поле «{field_title}» не может быть меньше {minimum}.")
|
||||
return number
|
||||
|
||||
|
||||
def _admin_create_required_int(value, field_title: str, minimum: int = 0) -> int:
|
||||
number = _admin_create_optional_int(value, field_title, minimum)
|
||||
if number is None:
|
||||
raise ValueError(f"Заполните поле «{field_title}».")
|
||||
return number
|
||||
|
||||
|
||||
def _render_admin_entity_create(
|
||||
request: Request,
|
||||
entity_type: str = "player",
|
||||
error: str | None = None,
|
||||
created_id: int | None = None,
|
||||
form_values: dict | None = None,
|
||||
status_code: int = 200,
|
||||
):
|
||||
allowed_entities = {"player", "coach", "referee"}
|
||||
entity_type = entity_type if entity_type in allowed_entities else "player"
|
||||
|
||||
try:
|
||||
teams = list_teams_for_admin_select()
|
||||
except Exception:
|
||||
teams = []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
name="admin_db_create.html",
|
||||
request=request,
|
||||
context={
|
||||
"entity_type": entity_type,
|
||||
"teams": teams,
|
||||
"error": error,
|
||||
"created_id": created_id,
|
||||
"form_values": form_values or {},
|
||||
},
|
||||
status_code=status_code,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/admin/db/create", response_class=HTMLResponse)
|
||||
def admin_db_create_page(
|
||||
request: Request,
|
||||
entity: str = Query(default="player"),
|
||||
created_id: int | None = Query(default=None),
|
||||
):
|
||||
denied = require_role(request, {"admin"})
|
||||
if denied:
|
||||
return denied
|
||||
return _render_admin_entity_create(
|
||||
request=request,
|
||||
entity_type=entity,
|
||||
created_id=created_id,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/db/create", response_class=HTMLResponse)
|
||||
async def admin_db_create_submit(request: Request):
|
||||
denied = require_role(request, {"admin"})
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
form = await request.form()
|
||||
values = {key: str(value) for key, value in form.items()}
|
||||
entity_type = values.get("entity_type", "player").strip().lower()
|
||||
if entity_type not in {"player", "coach", "referee"}:
|
||||
entity_type = "player"
|
||||
|
||||
try:
|
||||
full_name = values.get("full_name", "").strip()
|
||||
if not full_name:
|
||||
raise ValueError("Заполните поле «Полное имя / ФИО».")
|
||||
|
||||
external_id = values.get("external_id", "").strip()
|
||||
first_name = values.get("first_name", "").strip()
|
||||
last_name = values.get("last_name", "").strip()
|
||||
is_active = "is_active" in form
|
||||
|
||||
if entity_type == "player":
|
||||
team_id = _admin_create_required_int(values.get("team_id"), "Команда", 1)
|
||||
position = values.get("position", "").strip()
|
||||
if not position:
|
||||
raise ValueError("Выберите или укажите амплуа игрока.")
|
||||
|
||||
created_id = create_player_admin(
|
||||
team_id=team_id,
|
||||
full_name=full_name,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
external_id=external_id,
|
||||
number=values.get("number", ""),
|
||||
position=position,
|
||||
birth_date=values.get("birth_date", ""),
|
||||
photo=values.get("photo", ""),
|
||||
video=values.get("video", ""),
|
||||
height_cm=_admin_create_optional_int(values.get("height_cm"), "Рост", 1),
|
||||
weight_kg=_admin_create_optional_int(values.get("weight_kg"), "Вес", 1),
|
||||
games=_admin_create_optional_int(values.get("games"), "Игры", 0) or 0,
|
||||
goals=_admin_create_optional_int(values.get("goals"), "Голы", 0) or 0,
|
||||
penaltys=_admin_create_optional_int(values.get("penaltys"), "Голы с пенальти", 0) or 0,
|
||||
assists=_admin_create_optional_int(values.get("assists"), "Передачи", 0) or 0,
|
||||
yellows=_admin_create_optional_int(values.get("yellows"), "Жёлтые карточки", 0) or 0,
|
||||
reds=_admin_create_optional_int(values.get("reds"), "Красные карточки", 0) or 0,
|
||||
is_active=is_active,
|
||||
photo_enabled="photo_enabled" in form,
|
||||
)
|
||||
redirect_url = f"/admin/db/create?entity=player&created_id={created_id}"
|
||||
|
||||
elif entity_type == "coach":
|
||||
team_id = _admin_create_required_int(values.get("team_id"), "Команда", 1)
|
||||
role = values.get("role", "").strip()
|
||||
if not role:
|
||||
raise ValueError("Заполните должность / амплуа тренера.")
|
||||
|
||||
created_id = create_coach_admin(
|
||||
team_id=team_id,
|
||||
full_name=full_name,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
external_id=external_id,
|
||||
birth_date=values.get("birth_date", ""),
|
||||
role=role,
|
||||
is_active=is_active,
|
||||
)
|
||||
redirect_url = f"/admin/db/create?entity=coach&created_id={created_id}"
|
||||
|
||||
else:
|
||||
created_id = create_referee_admin(
|
||||
full_name=full_name,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
middle_name=values.get("middle_name", ""),
|
||||
city=values.get("city", ""),
|
||||
external_id=external_id,
|
||||
is_active=is_active,
|
||||
)
|
||||
redirect_url = f"/admin/db/create?entity=referee&created_id={created_id}"
|
||||
|
||||
log_action(
|
||||
request,
|
||||
action="admin_db_entity_created",
|
||||
entity_type=entity_type,
|
||||
entity_id=created_id,
|
||||
details={"full_name": full_name, "external_id": external_id},
|
||||
)
|
||||
return RedirectResponse(url=redirect_url, status_code=303)
|
||||
|
||||
except ValueError as exc:
|
||||
return _render_admin_entity_create(
|
||||
request=request,
|
||||
entity_type=entity_type,
|
||||
error=str(exc),
|
||||
form_values=values,
|
||||
status_code=400,
|
||||
)
|
||||
except Exception as exc:
|
||||
traceback.print_exc()
|
||||
error_text = "Не удалось создать запись."
|
||||
if "duplicate key" in str(exc).lower() or "unique constraint" in str(exc).lower():
|
||||
error_text += " Запись с таким External ID уже существует."
|
||||
else:
|
||||
error_text += f" Ошибка базы данных: {str(exc)}"
|
||||
return _render_admin_entity_create(
|
||||
request=request,
|
||||
entity_type=entity_type,
|
||||
error=error_text,
|
||||
form_values=values,
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/admin/db/players", response_class=HTMLResponse)
|
||||
def admin_db_players(
|
||||
request: Request,
|
||||
|
||||
@@ -360,3 +360,58 @@ def update_coach_admin(
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def create_coach_admin(
|
||||
team_id: int,
|
||||
full_name: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
external_id: str = "",
|
||||
birth_date: str = "",
|
||||
role: str = "",
|
||||
is_active: bool = True,
|
||||
) -> int:
|
||||
"""Создаёт тренера вручную из административного раздела."""
|
||||
query = """
|
||||
INSERT INTO coaches (
|
||||
external_id,
|
||||
team_id,
|
||||
player,
|
||||
lastname,
|
||||
name,
|
||||
born,
|
||||
amplua,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, %s, NOW(), NOW()
|
||||
)
|
||||
RETURNING id;
|
||||
"""
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
query,
|
||||
(
|
||||
external_id.strip(),
|
||||
int(team_id),
|
||||
full_name.strip(),
|
||||
last_name.strip(),
|
||||
first_name.strip(),
|
||||
birth_date.strip(),
|
||||
role.strip(),
|
||||
bool(is_active),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return int(row[0])
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -544,3 +544,101 @@ def update_player_photo_enabled(player_id: int, photo_enabled: bool = False) ->
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_player_admin(
|
||||
team_id: int,
|
||||
full_name: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
external_id: str = "",
|
||||
number: str = "",
|
||||
position: str = "",
|
||||
birth_date: str = "",
|
||||
photo: str = "",
|
||||
video: str = "",
|
||||
height_cm: int | None = None,
|
||||
weight_kg: int | None = None,
|
||||
games: int = 0,
|
||||
goals: int = 0,
|
||||
penaltys: int = 0,
|
||||
assists: int = 0,
|
||||
yellows: int = 0,
|
||||
reds: int = 0,
|
||||
is_active: bool = True,
|
||||
photo_enabled: bool = False,
|
||||
) -> int:
|
||||
"""Создаёт игрока вручную из административного раздела."""
|
||||
query = """
|
||||
INSERT INTO players (
|
||||
external_id,
|
||||
team_id,
|
||||
full_name,
|
||||
first_name,
|
||||
last_name,
|
||||
number,
|
||||
position,
|
||||
pos,
|
||||
amplua,
|
||||
born,
|
||||
photo,
|
||||
video,
|
||||
height_cm,
|
||||
weight_kg,
|
||||
games,
|
||||
goals,
|
||||
penaltys,
|
||||
assists,
|
||||
yellows,
|
||||
reds,
|
||||
is_active,
|
||||
photo_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, %s, %s,
|
||||
NULLIF(%s, '')::date, NULLIF(%s, ''), NULLIF(%s, ''), %s, %s,
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
|
||||
)
|
||||
RETURNING id;
|
||||
"""
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
query,
|
||||
(
|
||||
external_id.strip(),
|
||||
int(team_id),
|
||||
full_name.strip(),
|
||||
first_name.strip(),
|
||||
last_name.strip(),
|
||||
number.strip(),
|
||||
position.strip(),
|
||||
position.strip(),
|
||||
position.strip(),
|
||||
birth_date.strip(),
|
||||
photo.strip(),
|
||||
video.strip(),
|
||||
height_cm,
|
||||
weight_kg,
|
||||
int(games),
|
||||
int(goals),
|
||||
int(penaltys),
|
||||
int(assists),
|
||||
int(yellows),
|
||||
int(reds),
|
||||
bool(is_active),
|
||||
bool(photo_enabled),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return int(row[0])
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -266,3 +266,55 @@ def replace_match_referees(match_id: int, rows: list[dict]) -> None:
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def create_referee_admin(
|
||||
full_name: str,
|
||||
first_name: str = "",
|
||||
last_name: str = "",
|
||||
middle_name: str = "",
|
||||
city: str = "",
|
||||
external_id: str = "",
|
||||
is_active: bool = True,
|
||||
) -> int:
|
||||
"""Создаёт судью вручную из административного раздела."""
|
||||
query = """
|
||||
INSERT INTO referees (
|
||||
external_id,
|
||||
full_name,
|
||||
lastname,
|
||||
name,
|
||||
middle_name,
|
||||
city,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
NULLIF(%s, ''), %s, %s, %s, %s, NULLIF(%s, ''), %s, NOW(), NOW()
|
||||
)
|
||||
RETURNING id;
|
||||
"""
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
query,
|
||||
(
|
||||
external_id.strip(),
|
||||
full_name.strip(),
|
||||
last_name.strip(),
|
||||
first_name.strip(),
|
||||
middle_name.strip(),
|
||||
city.strip(),
|
||||
bool(is_active),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return int(row[0])
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -239,3 +239,29 @@ def update_team_admin(
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_teams_for_admin_select() -> list[dict]:
|
||||
"""Возвращает полный компактный список команд для выпадающих списков админки."""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, short_name_3, city
|
||||
FROM teams
|
||||
ORDER BY name ASC, id ASC
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1] or "",
|
||||
"short_name_3": row[2] or "",
|
||||
"city": row[3] or "",
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -149,6 +149,9 @@
|
||||
<form method="get" action="/admin/db/coaches" class="search-form">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Поиск по ФИО или external_id">
|
||||
<button type="submit" class="btn btn-primary">Найти</button>
|
||||
{% if request.state.current_user and request.state.current_user.role == "admin" %}
|
||||
<a href="/admin/db/create?entity=coach" class="btn btn-primary">➕ Создать тренера</a>
|
||||
{% endif %}
|
||||
<a href="/admin/db" class="btn btn-secondary">Назад</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
459
templates/admin_db_create.html
Normal file
459
templates/admin_db_create.html
Normal file
@@ -0,0 +1,459 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Создание записи</title>
|
||||
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1115;
|
||||
--panel: #171a21;
|
||||
--panel-2: #1d222b;
|
||||
--border: #2b3240;
|
||||
--text: #e8ecf3;
|
||||
--muted: #9aa4b2;
|
||||
--accent: #4f8cff;
|
||||
--accent-hover: #3e78e6;
|
||||
--success: #44cf88;
|
||||
--danger: #ff6b78;
|
||||
--shadow: 0 10px 30px rgba(0, 0, 0, .35);
|
||||
--radius: 16px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.page {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 22px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
max-width: 760px;
|
||||
}
|
||||
.entity-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.entity-tab {
|
||||
min-height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-2);
|
||||
border-radius: 12px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.entity-tab.active {
|
||||
border-color: var(--accent);
|
||||
background: rgba(79, 140, 255, .14);
|
||||
color: #cfe0ff;
|
||||
}
|
||||
.notice {
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 18px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.notice.error {
|
||||
border: 1px solid rgba(255, 107, 120, .5);
|
||||
background: rgba(255, 107, 120, .1);
|
||||
color: #ffd2d6;
|
||||
}
|
||||
.notice.success {
|
||||
border: 1px solid rgba(68, 207, 136, .5);
|
||||
background: rgba(68, 207, 136, .1);
|
||||
color: #caffdf;
|
||||
}
|
||||
.notice a { color: inherit; font-weight: 700; }
|
||||
.section {
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.section:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.section-hint {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: -7px;
|
||||
margin-bottom: 14px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
.form-grid.stats {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
.form-group.full { grid-column: 1 / -1; }
|
||||
label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.required::after {
|
||||
content: " *";
|
||||
color: var(--danger);
|
||||
}
|
||||
input[type="text"], input[type="date"], input[type="number"], select {
|
||||
width: 100%;
|
||||
min-height: 43px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); }
|
||||
.checkbox-grid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.checkbox-card {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
}
|
||||
.checkbox-card input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.btn {
|
||||
min-height: 43px;
|
||||
padding: 0 15px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-secondary { background: transparent; color: var(--text); border: 1px solid var(--border); }
|
||||
.empty-teams {
|
||||
padding: 14px;
|
||||
border: 1px dashed var(--danger);
|
||||
border-radius: 10px;
|
||||
color: #ffd2d6;
|
||||
background: rgba(255, 107, 120, .08);
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.page { padding: 12px; }
|
||||
.panel { padding: 16px; }
|
||||
.entity-tabs, .form-grid, .form-grid.stats { grid-template-columns: 1fr; }
|
||||
.form-group.full { grid-column: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="panel">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">Создание записи</div>
|
||||
<div class="subtitle">Выберите тип человека. Форма покажет только те поля, которые используются для этой роли в базе WFL.</div>
|
||||
</div>
|
||||
<a href="/admin/db" class="btn btn-secondary">Назад к разделам</a>
|
||||
</div>
|
||||
|
||||
<div class="entity-tabs">
|
||||
<a href="/admin/db/create?entity=player" class="entity-tab {% if entity_type == 'player' %}active{% endif %}">⚽ Игрок</a>
|
||||
<a href="/admin/db/create?entity=coach" class="entity-tab {% if entity_type == 'coach' %}active{% endif %}">📋 Тренер</a>
|
||||
<a href="/admin/db/create?entity=referee" class="entity-tab {% if entity_type == 'referee' %}active{% endif %}">🟨 Судья</a>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="notice error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if created_id %}
|
||||
<div class="notice success">
|
||||
Запись успешно создана. ID: <b>{{ created_id }}</b>.
|
||||
{% if entity_type == 'player' %}
|
||||
<a href="/admin/db/players/{{ created_id }}/edit">Открыть карточку игрока</a>
|
||||
{% elif entity_type == 'coach' %}
|
||||
<a href="/admin/db/coaches/{{ created_id }}/edit">Открыть карточку тренера</a>
|
||||
{% else %}
|
||||
<a href="/admin/db/referees/{{ created_id }}/edit">Открыть карточку судьи</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/db/create">
|
||||
<input type="hidden" name="entity_type" value="{{ entity_type }}">
|
||||
|
||||
{% if entity_type == 'player' %}
|
||||
<div class="section">
|
||||
<div class="section-title">Основные данные игрока</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="required">Команда</label>
|
||||
{% if teams %}
|
||||
<select name="team_id" required>
|
||||
<option value="">Выберите команду</option>
|
||||
{% for team in teams %}
|
||||
<option value="{{ team.id }}" {% if form_values.get('team_id') == team.id|string %}selected{% endif %}>
|
||||
{{ team.name }}{% if team.city %} — {{ team.city }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<div class="empty-teams">В базе нет команд. Сначала добавьте или загрузите команды.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>External ID</label>
|
||||
<input type="text" name="external_id" value="{{ form_values.get('external_id', '') }}" placeholder="ID игрока на сайте РФС">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label class="required">Полное имя / ФИО</label>
|
||||
<input type="text" name="full_name" value="{{ form_values.get('full_name', '') }}" required placeholder="Фамилия Имя">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Имя</label>
|
||||
<input type="text" name="first_name" value="{{ form_values.get('first_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Фамилия</label>
|
||||
<input type="text" name="last_name" value="{{ form_values.get('last_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Игровой номер</label>
|
||||
<input type="text" name="number" value="{{ form_values.get('number', '') }}" placeholder="Например, 10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="required">Амплуа</label>
|
||||
<input type="text" name="position" list="playerPositions" value="{{ form_values.get('position', '') }}" required placeholder="Выберите или введите">
|
||||
<datalist id="playerPositions">
|
||||
<option value="Вратарь">
|
||||
<option value="Защитник">
|
||||
<option value="Полузащитник">
|
||||
<option value="Нападающий">
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Дата рождения</label>
|
||||
<input type="date" name="birth_date" value="{{ form_values.get('birth_date', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Рост, см</label>
|
||||
<input type="number" name="height_cm" min="1" value="{{ form_values.get('height_cm', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Вес, кг</label>
|
||||
<input type="number" name="weight_kg" min="1" value="{{ form_values.get('weight_kg', '') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Медиа</div>
|
||||
<div class="section-hint">Можно указать имя файла или полный путь — формат остаётся таким же, как в существующих карточках.</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Фото</label>
|
||||
<input type="text" name="photo" value="{{ form_values.get('photo', '') }}" placeholder="Путь или имя файла фотографии">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Видео</label>
|
||||
<input type="text" name="video" value="{{ form_values.get('video', '') }}" placeholder="Путь или имя видео">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Статистика</div>
|
||||
<div class="section-hint">Для нового игрока можно оставить нули. При следующем парсинге статистика обновится по External ID.</div>
|
||||
<div class="form-grid stats">
|
||||
<div class="form-group"><label>Игры</label><input type="number" name="games" min="0" value="{{ form_values.get('games', '0') }}"></div>
|
||||
<div class="form-group"><label>Голы</label><input type="number" name="goals" min="0" value="{{ form_values.get('goals', '0') }}"></div>
|
||||
<div class="form-group"><label>Голы с пенальти</label><input type="number" name="penaltys" min="0" value="{{ form_values.get('penaltys', '0') }}"></div>
|
||||
<div class="form-group"><label>Передачи</label><input type="number" name="assists" min="0" value="{{ form_values.get('assists', '0') }}"></div>
|
||||
<div class="form-group"><label>Жёлтые карточки</label><input type="number" name="yellows" min="0" value="{{ form_values.get('yellows', '0') }}"></div>
|
||||
<div class="form-group"><label>Красные карточки</label><input type="number" name="reds" min="0" value="{{ form_values.get('reds', '0') }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="checkbox-grid">
|
||||
<label class="checkbox-card"><input type="checkbox" name="is_active" {% if not form_values or form_values.get('is_active') %}checked{% endif %}> Активный игрок</label>
|
||||
<label class="checkbox-card"><input type="checkbox" name="photo_enabled" {% if form_values.get('photo_enabled') %}checked{% endif %}> Использовать фотографию в титрах</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif entity_type == 'coach' %}
|
||||
<div class="section">
|
||||
<div class="section-title">Данные тренера</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="required">Команда</label>
|
||||
{% if teams %}
|
||||
<select name="team_id" required>
|
||||
<option value="">Выберите команду</option>
|
||||
{% for team in teams %}
|
||||
<option value="{{ team.id }}" {% if form_values.get('team_id') == team.id|string %}selected{% endif %}>
|
||||
{{ team.name }}{% if team.city %} — {{ team.city }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<div class="empty-teams">В базе нет команд. Сначала добавьте или загрузите команды.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>External ID</label>
|
||||
<input type="text" name="external_id" value="{{ form_values.get('external_id', '') }}" placeholder="ID тренера на сайте РФС">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label class="required">Полное имя / ФИО</label>
|
||||
<input type="text" name="full_name" value="{{ form_values.get('full_name', '') }}" required placeholder="Фамилия Имя">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Имя</label>
|
||||
<input type="text" name="first_name" value="{{ form_values.get('first_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Фамилия</label>
|
||||
<input type="text" name="last_name" value="{{ form_values.get('last_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="required">Должность / амплуа</label>
|
||||
<input type="text" name="role" list="coachRoles" value="{{ form_values.get('role', '') }}" required placeholder="Например, главный тренер">
|
||||
<datalist id="coachRoles">
|
||||
<option value="Главный тренер">
|
||||
<option value="Старший тренер">
|
||||
<option value="Тренер">
|
||||
<option value="Тренер вратарей">
|
||||
<option value="Тренер по физической подготовке">
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Дата рождения</label>
|
||||
<input type="date" name="birth_date" value="{{ form_values.get('birth_date', '') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="checkbox-grid">
|
||||
<label class="checkbox-card"><input type="checkbox" name="is_active" {% if not form_values or form_values.get('is_active') %}checked{% endif %}> Активный тренер</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="section">
|
||||
<div class="section-title">Данные судьи</div>
|
||||
<div class="section-hint">Конкретная роль — главный судья, помощник, резервный — назначается отдельно в карточке матча.</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group full">
|
||||
<label class="required">Полное имя / ФИО</label>
|
||||
<input type="text" name="full_name" value="{{ form_values.get('full_name', '') }}" required placeholder="Фамилия Имя Отчество">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Фамилия</label>
|
||||
<input type="text" name="last_name" value="{{ form_values.get('last_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Имя</label>
|
||||
<input type="text" name="first_name" value="{{ form_values.get('first_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Отчество</label>
|
||||
<input type="text" name="middle_name" value="{{ form_values.get('middle_name', '') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Город</label>
|
||||
<input type="text" name="city" value="{{ form_values.get('city', '') }}">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>External ID</label>
|
||||
<input type="text" name="external_id" value="{{ form_values.get('external_id', '') }}" placeholder="ID судьи во внешнем источнике">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="checkbox-grid">
|
||||
<label class="checkbox-card"><input type="checkbox" name="is_active" {% if not form_values or form_values.get('is_active') %}checked{% endif %}> Активный судья</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn btn-primary" {% if entity_type in ['player', 'coach'] and not teams %}disabled{% endif %}>Создать запись</button>
|
||||
{% if entity_type == 'player' %}
|
||||
<a href="/admin/db/players" class="btn btn-secondary">Список игроков</a>
|
||||
{% elif entity_type == 'coach' %}
|
||||
<a href="/admin/db/coaches" class="btn btn-secondary">Список тренеров</a>
|
||||
{% else %}
|
||||
<a href="/admin/db/referees" class="btn btn-secondary">Список судей</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -520,6 +520,9 @@
|
||||
<div class="title">Редактирование базы</div>
|
||||
|
||||
<div class="tabs">
|
||||
{% if current_user and current_user.role == "admin" %}
|
||||
<a class="tab-link" href="/admin/db/create?entity=player">➕ Создать запись</a>
|
||||
{% endif %}
|
||||
<a class="tab-link" href="/admin/db/players">Игроки</a>
|
||||
<a class="tab-link" href="/admin/db/referees">Судьи</a>
|
||||
<a class="tab-link" href="/admin/db/teams">Команды</a>
|
||||
|
||||
@@ -190,6 +190,9 @@
|
||||
<input type="hidden" name="direction" value="{{ direction }}">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Поиск по ФИО, external_id или команде">
|
||||
<button type="submit" class="btn btn-primary">Найти</button>
|
||||
{% if request.state.current_user and request.state.current_user.role == "admin" %}
|
||||
<a href="/admin/db/create?entity=player" class="btn btn-primary">➕ Создать игрока</a>
|
||||
{% endif %}
|
||||
<a href="/admin/db" class="btn btn-secondary">Назад</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<form method="get" action="/admin/db/referees" class="search-form">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="Поиск по ФИО или external_id">
|
||||
<button type="submit" class="btn btn-primary">Найти</button>
|
||||
{% if request.state.current_user and request.state.current_user.role == "admin" %}
|
||||
<a href="/admin/db/create?entity=referee" class="btn btn-primary">➕ Создать судью</a>
|
||||
{% endif %}
|
||||
<a href="/admin/db" class="btn btn-secondary">Назад</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user