Files
shooting-event/phoenix/lib/shooting_event_phx/domain/event_data.ex
2026-04-29 00:45:48 +04:00

354 lines
9.2 KiB
Elixir

defmodule ShootingEventPhx.Domain.EventData do
@moduledoc false
import Ecto.Query
alias ShootingEventPhx.Repo
alias ShootingEventPhx.Domain.{Player, Score, ScoreAttachment, Stages}
def list_players do
Repo.all(from p in Player, order_by: [asc: p.id])
end
def ensure_score_rows(players) do
Repo.transaction(fn ->
for player <- players, stage <- Stages.score_stages() do
Repo.query!(
"""
INSERT OR IGNORE INTO scores(stage, player_id, score)
VALUES(?1, ?2, 0)
""",
[stage, player.id]
)
end
end)
end
def read_scores(players) do
base =
for stage <- Stages.score_stages(), into: %{} do
{stage, Map.new(players, &{&1.id, 0})}
end
rows =
Repo.all(
from s in Score,
select: {s.stage, s.player_id, s.score}
)
Enum.reduce(rows, base, fn {stage, player_id, score}, acc ->
if Map.has_key?(acc, stage) do
Map.update!(acc, stage, &Map.put(&1, player_id, score))
else
acc
end
end)
end
def read_score_proofs(players) do
base =
for stage <- Stages.score_stages(), into: %{} do
{stage, Map.new(players, &{&1.id, ""})}
end
rows =
Repo.all(
from sa in ScoreAttachment,
select: {sa.stage, sa.player_id, sa.image_data}
)
Enum.reduce(rows, base, fn {stage, player_id, image_data}, acc ->
if Map.has_key?(acc, stage) do
Map.update!(acc, stage, &Map.put(&1, player_id, image_data || ""))
else
acc
end
end)
end
def score_map_to_json(scores) do
for {stage, stage_map} <- scores, into: %{} do
{stage,
for({player_id, value} <- stage_map, into: %{}, do: {Integer.to_string(player_id), value})}
end
end
def proof_map_to_json(proofs) do
for {stage, stage_map} <- proofs, into: %{} do
compact =
for {player_id, image_data} <- stage_map,
String.trim(to_string(image_data)) != "",
into: %{} do
{Integer.to_string(player_id), image_data}
end
{stage, compact}
end
end
def create_player(attrs) do
name_ar = attrs["nameAr"] |> to_string() |> String.trim()
name_en = attrs["nameEn"] |> to_string() |> String.trim()
group_code = attrs["groupCode"] |> normalize_group()
cond do
name_ar == "" or name_en == "" ->
{:error, :bad_request, "both Arabic and English names are required"}
true ->
Repo.transaction(fn ->
{:ok, player} =
%Player{name_ar: name_ar, name_en: name_en, group_code: group_code, image_data: ""}
|> Repo.insert()
for stage <- Stages.score_stages() do
Repo.query!(
"""
INSERT OR IGNORE INTO scores(stage, player_id, score) VALUES(?1, ?2, 0)
""",
[stage, player.id]
)
end
end)
|> case do
{:ok, _} -> :ok
{:error, reason} -> {:error, :internal, inspect(reason)}
end
end
end
def update_player(id, attrs) do
with %Player{} = player <- Repo.get(Player, id) do
updates = player_updates(attrs)
cond do
Map.has_key?(updates, :_error) ->
{:error, :bad_request, Map.get(updates, :_error)}
map_size(updates) == 0 ->
{:error, :bad_request, "no fields to update"}
true ->
case Repo.update(Ecto.Changeset.change(player, updates)) do
{:ok, _} -> :ok
{:error, changeset} -> {:error, :bad_request, format_changeset_errors(changeset)}
end
end
else
nil -> {:error, :not_found, "player not found"}
end
end
def delete_player(id) do
case Repo.get(Player, id) do
nil ->
{:error, :not_found, "player not found"}
player ->
case Repo.delete(player) do
{:ok, _} -> :ok
{:error, reason} -> {:error, :internal, inspect(reason)}
end
end
end
def auto_group_players(groups) do
normalized_groups =
groups
|> List.wrap()
|> Enum.map(&normalize_group/1)
|> Enum.reject(&(&1 == ""))
|> Enum.uniq()
if normalized_groups == [] do
{:error, :bad_request, "at least one group is required"}
else
player_ids = Repo.all(from p in Player, select: p.id, order_by: [asc: p.id])
shuffled = Enum.shuffle(player_ids)
case Repo.transaction(fn ->
Enum.with_index(shuffled)
|> Enum.each(fn {player_id, idx} ->
group_code = Enum.at(normalized_groups, rem(idx, length(normalized_groups)))
Repo.query!(
"UPDATE players SET group_code = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
[group_code, player_id]
)
end)
end) do
{:ok, _} -> :ok
{:error, reason} -> {:error, :internal, inspect(reason)}
end
end
end
def update_score(stage, player_id, score) when is_integer(score) do
if score < 0 or score > 9999 do
{:error, :bad_request, "score must be between 0 and 9999"}
else
Repo.query!(
"""
INSERT INTO scores(stage, player_id, score)
VALUES(?1, ?2, ?3)
ON CONFLICT(stage, player_id) DO UPDATE SET score = excluded.score, updated_at = CURRENT_TIMESTAMP
""",
[stage, player_id, score]
)
:ok
end
end
def update_score_proof(stage, player_id, image_data) do
payload = to_string(image_data || "") |> String.trim()
cond do
payload == "" ->
{:error, :bad_request, "imageData is required"}
byte_size(payload) > 5_000_000 ->
{:error, :bad_request, "image payload too large"}
true ->
Repo.query!(
"""
INSERT INTO score_attachments(stage, player_id, image_data)
VALUES(?1, ?2, ?3)
ON CONFLICT(stage, player_id) DO UPDATE SET image_data = excluded.image_data, updated_at = CURRENT_TIMESTAMP
""",
[stage, player_id, payload]
)
:ok
end
end
def delete_score_proof(stage, player_id) do
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1 AND player_id = ?2", [
stage,
player_id
])
:ok
end
def reset_stage_scores(stages, reset_proofs?) do
case Repo.transaction(fn ->
Enum.each(stages, fn stage ->
Repo.query!(
"UPDATE scores SET score = 0, updated_at = CURRENT_TIMESTAMP WHERE stage = ?1",
[stage]
)
if reset_proofs? do
Repo.query!("DELETE FROM score_attachments WHERE stage = ?1", [stage])
end
end)
end) do
{:ok, _} -> :ok
{:error, reason} -> {:error, :internal, inspect(reason)}
end
end
def load_score_proof_image(stage, player_id) do
case Repo.one(
from sa in ScoreAttachment,
where: sa.stage == ^stage and sa.player_id == ^player_id,
select: sa.image_data
) do
nil -> {:error, :not_found}
image -> {:ok, image}
end
end
def load_current_score(stage, player_id) do
score =
Repo.one(
from s in Score, where: s.stage == ^stage and s.player_id == ^player_id, select: s.score
)
{:ok, score || 0}
end
defp player_updates(attrs) do
updates = %{}
updates =
case Map.get(attrs, "nameAr") do
nil ->
updates
name ->
trimmed = to_string(name) |> String.trim()
if trimmed == "",
do: Map.put(updates, :_error, "arabic name cannot be empty"),
else: Map.put(updates, :name_ar, trimmed)
end
updates =
case Map.get(attrs, "nameEn") do
nil ->
updates
name ->
trimmed = to_string(name) |> String.trim()
if trimmed == "",
do: Map.put(updates, :_error, "english name cannot be empty"),
else: Map.put(updates, :name_en, trimmed)
end
updates =
if Map.has_key?(attrs, "groupCode"),
do: Map.put(updates, :group_code, normalize_group(Map.get(attrs, "groupCode"))),
else: updates
updates =
if Map.has_key?(attrs, "imageData") do
image = attrs |> Map.get("imageData") |> to_string() |> String.trim()
Map.put(updates, :image_data, image)
else
updates
end
updates =
if truthy?(Map.get(attrs, "clearImage")) do
Map.put(updates, :image_data, "")
else
updates
end
case Map.pop(updates, :_error) do
{nil, sanitized} ->
if byte_size(Map.get(sanitized, :image_data, "")) > 2_500_000 do
%{_error: "image payload too large"}
else
sanitized
end
{message, _} ->
%{_error: message}
end
end
defp normalize_group(group), do: group |> to_string() |> String.trim()
defp truthy?(value) do
case value do
true -> true
1 -> true
"1" -> true
"true" -> true
"yes" -> true
"on" -> true
_ -> false
end
end
defp format_changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Enum.map(fn {field, msgs} -> "#{field} #{Enum.join(msgs, ",")}" end)
|> Enum.join("; ")
end
end