diff --git a/lib/localspot/businesses/import.ex b/lib/localspot/businesses/import.ex index d28c5d9..82884a7 100644 --- a/lib/localspot/businesses/import.ex +++ b/lib/localspot/businesses/import.ex @@ -38,6 +38,8 @@ defmodule Localspot.Businesses.Import do """ alias Localspot.Businesses + alias Localspot.Businesses.Category + alias Localspot.Repo require Logger @@ -45,6 +47,89 @@ defmodule Localspot.Businesses.Import do {:ok, %{imported: non_neg_integer(), skipped: non_neg_integer(), errors: list()}} | {:error, term()} + @type preview_result :: + {:ok, %{total: non_neg_integer(), missing_categories: list(String.t())}} + | {:error, term()} + + @doc """ + Previews a JSON file and returns information about missing categories. + """ + @spec preview_file(String.t()) :: preview_result() + def preview_file(path) do + case File.read(path) do + {:ok, content} -> preview_json(content) + {:error, reason} -> {:error, {:file_error, reason}} + end + end + + @doc """ + Previews a JSON string and returns information about missing categories. + """ + @spec preview_json(String.t()) :: preview_result() + def preview_json(json_string) do + case Jason.decode(json_string) do + {:ok, %{"businesses" => businesses}} when is_list(businesses) -> + analyze_businesses(businesses) + + {:ok, _} -> + {:error, {:invalid_format, "JSON must contain a 'businesses' array"}} + + {:error, reason} -> + {:error, {:json_error, reason}} + end + end + + defp analyze_businesses(businesses) do + categories = load_categories() + + # Extract all unique category keys from businesses + category_keys = + businesses + |> Enum.map(&(&1["category"] || "")) + |> Enum.reject(&(&1 == "")) + |> Enum.uniq() + + # Find which ones are missing + missing_categories = + category_keys + |> Enum.reject(fn key -> Map.has_key?(categories, String.downcase(key)) end) + |> Enum.sort() + + {:ok, %{total: length(businesses), missing_categories: missing_categories}} + end + + @doc """ + Creates categories from a list of category names/slugs. + Returns {:ok, created_count} or {:error, reason}. + """ + @spec create_categories(list(String.t())) :: {:ok, non_neg_integer()} | {:error, term()} + def create_categories(category_names) do + results = + Enum.map(category_names, fn name -> + slug = Businesses.generate_slug(name) + + display_name = + name + |> String.replace("-", " ") + |> String.split() + |> Enum.map(&String.capitalize/1) + |> Enum.join(" ") + + attrs = %{ + name: display_name, + slug: slug, + description: "#{display_name} category" + } + + %Category{} + |> Category.changeset(attrs) + |> Repo.insert() + end) + + created = Enum.count(results, &match?({:ok, _}, &1)) + {:ok, created} + end + @doc """ Imports businesses from a JSON file path. """ diff --git a/lib/localspot_web/live/admin_live/import.ex b/lib/localspot_web/live/admin_live/import.ex index 63d53d6..2892f05 100644 --- a/lib/localspot_web/live/admin_live/import.ex +++ b/lib/localspot_web/live/admin_live/import.ex @@ -27,7 +27,11 @@ defmodule LocalspotWeb.AdminLive.Import do |> assign(:page_title, "Import Businesses") |> assign(:uploaded_files, []) |> assign(:import_result, nil) + |> assign(:preview_result, nil) |> assign(:importing, false) + |> assign(:previewing, false) + |> assign(:creating_categories, false) + |> assign(:pending_file_content, nil) |> assign(:json_example, @json_example) |> allow_upload(:import_file, accept: ~w(.json), @@ -41,31 +45,97 @@ defmodule LocalspotWeb.AdminLive.Import do {:noreply, socket} end + @impl true + def handle_event("preview", _params, socket) do + socket = assign(socket, :previewing, true) + + # Read and store file content for later import + {file_content, preview_result} = + consume_uploaded_entries(socket, :import_file, fn %{path: path}, _entry -> + content = File.read!(path) + {:ok, {content, Import.preview_json(content)}} + end) + |> case do + [{content, result}] -> {content, result} + [] -> {nil, {:error, :no_file}} + end + + {:noreply, + socket + |> assign(:preview_result, preview_result) + |> assign(:pending_file_content, file_content) + |> assign(:previewing, false)} + end + + @impl true + def handle_event("create_categories", _params, socket) do + socket = assign(socket, :creating_categories, true) + + case socket.assigns.preview_result do + {:ok, %{missing_categories: missing_categories}} when missing_categories != [] -> + {:ok, created} = Import.create_categories(missing_categories) + + # Re-preview to confirm categories were created + new_preview = + case socket.assigns.pending_file_content do + nil -> {:error, :no_file} + content -> Import.preview_json(content) + end + + {:noreply, + socket + |> assign(:preview_result, new_preview) + |> assign(:creating_categories, false) + |> put_flash(:info, "Created #{created} category(ies)")} + + _ -> + {:noreply, assign(socket, :creating_categories, false)} + end + end + @impl true def handle_event("import", _params, socket) do socket = assign(socket, :importing, true) - result = - consume_uploaded_entries(socket, :import_file, fn %{path: path}, _entry -> - {:ok, Import.from_file(path)} - end) - import_result = - case result do - [{:ok, result}] -> result - [{:error, reason}] -> {:error, reason} - [] -> {:error, :no_file} + case socket.assigns.pending_file_content do + nil -> {:error, :no_file} + content -> Import.from_json(content) end {:noreply, socket |> assign(:import_result, import_result) + |> assign(:preview_result, nil) + |> assign(:pending_file_content, nil) |> assign(:importing, false)} end @impl true def handle_event("clear_result", _params, socket) do - {:noreply, assign(socket, :import_result, nil)} + {:noreply, + socket + |> assign(:import_result, nil) + |> assign(:preview_result, nil) + |> assign(:pending_file_content, nil) + |> allow_upload(:import_file, + accept: ~w(.json), + max_entries: 1, + max_file_size: 10_000_000 + )} + end + + @impl true + def handle_event("cancel_preview", _params, socket) do + {:noreply, + socket + |> assign(:preview_result, nil) + |> assign(:pending_file_content, nil) + |> allow_upload(:import_file, + accept: ~w(.json), + max_entries: 1, + max_file_size: 10_000_000 + )} end @impl true @@ -86,63 +156,140 @@ defmodule LocalspotWeb.AdminLive.Import do - <.form for={%{}} phx-change="validate" phx-submit="import" class="space-y-4"> -
- Drag and drop a JSON file here, or - -
-Max file size: 10MB
++ Drag and drop a JSON file here, or + +
+Max file size: 10MB
+Missing Categories Detected
++ The following categories don't exist and need to be created: +
+0} class="text-sm text-base-content/60 mt-2"> + You must create the missing categories before importing. +