PhoenixKitManufacturing.Machines (PhoenixKitManufacturing v0.3.2)

Copy Markdown View Source

Context module for managing machines, plus thin read wrappers over the machine_type / operation phoenix_kit_entities records used to tag and describe them.

Machines and types have a many-to-many relationship via a join table, so a machine can be tagged with several types at once (e.g. both "CNC" and "Milling"). Machines use hard-delete (simple reference data); machine type/operation CRUD moved to the generic entities admin UI (/admin/entities/machine_type/data, /admin/entities/operation/data) as part of the entities migration — see dev_docs/ENTITIES_MIGRATION_SPEC.md. This module keeps only the read-side other module code (pickers, the machine form, Web.DashboardLive's stat tile) still needs, resolved through PhoenixKitManufacturing.EntitiesRegistry.

Activity logging

Every mutating function accepts opts \ []. When actor_uuid: is present in opts, the mutation is logged via PhoenixKit.Activity.log/1 under the "manufacturing" module key. Logging failures never crash the primary operation — both PhoenixKit.Activity.log/1 and this module's maybe_log_activity/5 rescue internally, so on a host that has not yet run core's activity migration the mutation still succeeds and the failure degrades to a Logger.warning.

Usage from IEx

alias PhoenixKitManufacturing.Machines

{:ok, mill} = Machines.create_machine(%{name: "CNC-01", code: "M-001"})
[%{uuid: type_uuid} | _] = Machines.list_machine_types(status: "published")
{:ok, _} = Machines.sync_machine_types(mill.uuid, [type_uuid])

Machines.list_machines(type_uuid: type_uuid)
Machines.count_machines()

Summary

Functions

Returns an Ecto.Changeset for tracking machine changes.

Returns the total count of machine types.

Returns the total count of machines.

Returns the number of machines with type_uuid currently assigned.

Hard-deletes a machine. Cascades to type assignments.

Fetches a machine by UUID. Returns nil if not found.

Returns true if the machine has the given operation linked.

Returns true if the machine has the given type assigned.

Returns a %{operation_uuid => time_norm_seconds} map of a machine's current operation links, for initializing the operations section of the machine form.

Returns a list of type UUIDs linked to a machine.

Batch-resolves linked machine-type UUIDs for a list of machines in a single query, e.g. %{machine_uuid => [type_uuid, ...]}. Machines with no linked types are absent from the result map (not present with []).

Lists the operations linked to a machine, each paired with its per-machine time-norm override.

Lists machine types via EntitiesRegistry.

Lists all machines, ordered by name.

Resolves a human-readable location label for a machine, trying (in order)

Logs a module enable/disable toggle. Called from the enable_system / disable_system module lifecycle functions.

Merges the field_template rows of every published machine type in type_uuids into a single ordered list, for rendering the dynamic metadata inputs on the machine form.

Syncs the operation links for a machine (full replace).

Syncs the type assignments for a machine (full replace).

Updates a machine with the given attributes.

Types

list_machine_types_opts()

@type list_machine_types_opts() :: [
  locale: String.t() | nil,
  status: String.t() | nil
]

list_machines_opts()

@type list_machines_opts() :: [status: String.t(), type_uuid: String.t()]

opts()

@type opts() :: keyword()

status_filter()

@type status_filter() :: [{:status, String.t()}]

Functions

change_machine(machine, attrs \\ %{})

Returns an Ecto.Changeset for tracking machine changes.

count_machine_types(opts \\ [])

@spec count_machine_types(status_filter()) :: non_neg_integer()

Returns the total count of machine types.

A thin EntitiesRegistry wrapper kept alongside list_machine_types/1 (rather than removed with the rest of the machine-type CRUD) because Web.DashboardLive's stat tile still calls it — nothing in the entities migration replaces that caller.

Options

count_machines(opts \\ [])

@spec count_machines(status_filter()) :: non_neg_integer()

Returns the total count of machines.

count_machines_with_operation(operation_uuid)

@spec count_machines_with_operation(String.t()) :: non_neg_integer()

Same as count_machines_with_type/1, for the operation entity.

count_machines_with_type(type_uuid)

@spec count_machines_with_type(String.t()) :: non_neg_integer()

Returns the number of machines with type_uuid currently assigned.

Intended as the host app's reverse_references count_fn for the machine_type entity (an advisory "used by N machines" hint on the entities trash UI) — see dev_docs/IMPLEMENTATION_PLAN_E.md's ANDI follow-up task. Not called anywhere in this module itself.

create_machine(attrs, opts \\ [])

@spec create_machine(map(), opts()) ::
  {:ok, PhoenixKitManufacturing.Schemas.Machine.t()}
  | {:error, Ecto.Changeset.t()}

Creates a machine.

Required: :name. Optional: :code, :manufacturer, :serial_number, :description, :location_note, :status, :data, :metadata.

delete_machine(machine, opts \\ [])

Hard-deletes a machine. Cascades to type assignments.

get_machine(uuid)

@spec get_machine(String.t()) :: PhoenixKitManufacturing.Schemas.Machine.t() | nil

Fetches a machine by UUID. Returns nil if not found.

Does not preload linked machine types — see list_machines/1 moduledoc.

has_operation?(machine_uuid, operation_uuid)

@spec has_operation?(String.t(), String.t()) :: boolean()

Returns true if the machine has the given operation linked.

has_type?(machine_uuid, type_uuid)

@spec has_type?(String.t(), String.t()) :: boolean()

Returns true if the machine has the given type assigned.

linked_operation_overrides(machine_uuid)

@spec linked_operation_overrides(String.t()) :: %{
  required(String.t()) => integer() | nil
}

Returns a %{operation_uuid => time_norm_seconds} map of a machine's current operation links, for initializing the operations section of the machine form.

The map's keys are the full linked-operation set — every linked operation appears, whether or not it carries an override — which is exactly the shape sync_machine_operations/3 needs for its "before" side of the diff.

linked_type_uuids(machine_uuid)

@spec linked_type_uuids(String.t()) :: [String.t()]

Returns a list of type UUIDs linked to a machine.

linked_type_uuids_by_machine(machine_uuids)

@spec linked_type_uuids_by_machine([String.t()]) :: %{
  required(String.t()) => [String.t()]
}

Batch-resolves linked machine-type UUIDs for a list of machines in a single query, e.g. %{machine_uuid => [type_uuid, ...]}. Machines with no linked types are absent from the result map (not present with []).

Used in place of the preload: :machine_types removed when machine_type_uuid became a soft reference (see Schemas.MachineTypeAssignment moduledoc) — callers resolve type names from the returned UUIDs themselves (e.g. list_machine_types/1).

list_machine_operations(machine_uuid)

@spec list_machine_operations(String.t()) :: [
  %{
    operation: PhoenixKitManufacturing.EntitiesRegistry.record() | nil,
    time_norm_seconds: integer() | nil
  }
]

Lists the operations linked to a machine, each paired with its per-machine time-norm override.

Returns `%{operation: EntitiesRegistry.record()nil, time_norm_seconds:
integer()nil}` maps, ordered by the linked operation's name (resolved

from EntitiesRegistryoperation_uuid is a soft reference, see Schemas.MachineOperation moduledoc, and carries no name of its own). operation is nil for a dangling link (the linked entity-data record was hard-removed out from under a soft reference — an accepted risk of the entities migration, see dev_docs/ENTITIES_MIGRATION_SPEC.md §5); such rows sort first. time_norm_seconds is the raw MachineOperation override as stored — nil means "no override, use the operation's own base_time_norm_seconds"; resolving that fallback is left to the caller (this function doesn't look at operation.base_time_norm_seconds itself).

list_machine_types(opts \\ [])

Lists machine types via EntitiesRegistry.

Options

  • :locale — resolves each record's :name for this locale (a bare Gettext code or BCP-47 dialect); nil (default) resolves the primary-language title.
  • :status — filter by exact status (e.g. "published"). nil (default, unlike the old "active"-by-convention behavior) returns every cached status — callers that only want published records must pass status: "published" explicitly.

list_machines(opts \\ [])

Lists all machines, ordered by name.

Does not preload linked machine types — machine_type_uuid is a soft reference (see Schemas.MachineTypeAssignment moduledoc), not an Ecto association, so there is nothing for preload: to resolve. Callers that need type names for a batch of machines should use linked_type_uuids_by_machine/1.

Options

  • :status — filter by status.
  • :type_uuid — filter to only machines that have this type assigned.

location_label(machine, opts \\ [])

@spec location_label(PhoenixKitManufacturing.Schemas.Machine.t(), opts()) ::
  String.t() | nil

Resolves a human-readable location label for a machine, trying (in order):

  1. space_uuidPhoenixKitLocations.Spaces.full_path/2, e.g. "Main Warehouse / Floor 2 / Rack 5".
  2. location_uuid — the translated name of the Location itself (no specific space picked).
  3. location_note — legacy freeform text for machines that predate the location_uuid/space_uuid link (see Schemas.Machine).
  4. nil — no location data at all.

phoenix_kit_locations is a soft cross-module reference (no FK — see Schemas.Machine's moduledoc): a uuid pointing at data this call can't reach (record deleted, table not migrated on this host, …) is treated as "no answer" and falls through to the next step rather than raising, hence the rescue around each cross-module read.

Options

  • :locale — forwarded to Spaces.full_path/2 / used to pick the translated Location name, same _name -> name -> primary-name fallback chain as PhoenixKitLocations.Web.Components.PlacePicker. nil (default) always shows the primary-language name.

log_module_toggle(state, opts \\ [])

@spec log_module_toggle(:enabled | :disabled, opts()) :: :ok

Logs a module enable/disable toggle. Called from the enable_system / disable_system module lifecycle functions.

merged_field_template(type_uuids)

@spec merged_field_template([String.t()]) :: [map()]

Merges the field_template rows of every published machine type in type_uuids into a single ordered list, for rendering the dynamic metadata inputs on the machine form.

type_uuids is expected to already be filtered down to "linked to this machine" (e.g. MapSet.to_list/1 of the toggled type badges on the form) — this function does no linking lookup of its own, it only merges.

Types are read via EntitiesRegistry.list(:machine_type, nil, status: "published") — locale isn't threaded through (unlike list_machine_types/1) because this function never reads a record's :name/:titles, only metadata["field_template"], so the registry's locale-dependent title resolution is irrelevant here. Records come back ordered by position (drag-order in the entities admin UI; creation-order immediately after the V5 migration seed, since every migrated record starts at position: 0 — see the E-plan's "Решения по открытым вопросам" #5) — so the merge order (and therefore which type wins a key collision) follows that order, not the order of type_uuids. When two linked types both define a field_template row with the same key, the earlier one in registry order wins and the later row is dropped silently — this is a deliberate "first wins" merge, not an error. Callers rendering the merged template SHOULD hint which type a field came from when a collision is possible (e.g. a "from <type name>" caption next to the label) — this function only resolves the winner, it doesn't surface which types lost.

sync_machine_operations(machine_uuid, overrides_map, opts \\ [])

@spec sync_machine_operations(
  String.t(),
  %{required(String.t()) => integer() | nil},
  opts()
) ::
  {:ok, :synced | :unchanged} | {:error, :operation_assignment_failed}

Syncs the operation links for a machine (full replace).

overrides_map is a %{operation_uuid => time_norm_seconds | nil} map: its key set is the full desired list of linked operations, and each value is that operation's per-machine norm override (nil ⇒ no override, fall back to the operation's own base_time_norm_seconds).

Unlike sync_machine_types/3 (which only needs to compare a set of linked UUIDs), this compares the whole map with Map.equal?/2 against linked_operation_overrides/1 — same key set and same values. An unchanged set of linked operations with a changed override is still a real sync, not a no-op, because the override value is data the caller asked to persist.

Replaces all existing links with the given map, wrapped in a transaction for atomicity. Logs machine.operations_synced only when something actually changed; a no-op sync is silent.

sync_machine_types(machine_uuid, type_uuids, opts \\ [])

@spec sync_machine_types(String.t(), [String.t()], opts()) ::
  {:ok, :synced | :unchanged} | {:error, :type_assignment_failed}

Syncs the type assignments for a machine (full replace).

Replaces all existing assignments with the given list of type UUIDs, wrapped in a transaction for atomicity. Logs machine.types_synced only when the assignment set actually changed; a no-op sync is silent.

update_machine(machine, attrs, opts \\ [])

Updates a machine with the given attributes.