Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram:pain
> ./lib/pain_web/live/book_live.ex
Lenses
(coming soon!)
defmodule PainWeb.BookLive do
use Surface.LiveView
import PainWeb.Gettext
import Pain.Schedule, only: [check_blocks_on_calendars: 3]
alias PainWeb.Components.Accion
alias PainWeb.Components.BodyMap
alias PainWeb.Components.Page
alias PainWeb.Components.Choices
alias PainWeb.Components.Class
alias PainWeb.Components.Conditions
alias PainWeb.Components.Employee
alias PainWeb.Components.Schedule
alias PainWeb.Components.ServiceMap
alias Surface.Components.Form
import PainWeb.CoreComponents, only: [modal: 1, show_modal: 1, hide_modal: 1]
data number, :integer, default: 1
data open_class, :string, default: ""
data schedule, :string, default: nil
data display_bios, :boolean, default: true
data services, :map, default: %{}
data employed, :map, default: %{}
data calendars, :map, default: %{}
data limbs, :map, default: %{}
data addons, :map, default: %{}
data all_addons, :list, default: []
data customer, :form, default: %{
"name" => "",
"phone" => "",
"email" => "",
"reference" => "",
"conditions" => false,
}
data booked, :list, default: nil
def mount _, _, socket do
{ :ok, socket |> assign(:all_addons, Pain.Schedule.addons) }
end
def handle_event("bypass", _, socket) do
{:noreply, socket |> assign(%{
number: 4,
services: %{
1 => "90Min Reflexology with Chinese Medicine",
2 => "90min Massage",
3 => "Cupping",
4 => "Wet Cupping"
},
schedule: "2023-09-30T19:00",
employed: %{1 => "_fem", 2 => "_masc", 3 => "Andy Ji", 4 => "Bin Wang"}
})}
end
def handle_event("number", params, socket),
do: {:noreply, assign(socket, :number, String.to_integer params["num"])}
def handle_event("open_class", params, socket),
do: {:noreply, assign(socket, :open_class, params["name"])}
def handle_event("choose_service", params, socket) do
num = String.to_integer(params["num"])
service = all_services() |> Enum.filter(& &1["name"] == params["name"]) |> hd |> IO.inspect
{:noreply, socket
|> update(:services, &(Map.put(&1, num, params["name"])))
|> update(:limbs, fn limbs ->
case service["class"] do
"Body Massage" ->
Map.update(limbs, num, ["_choose"], & &1 ++ ["_choose"] |> Enum.uniq)
_ -> limbs
end
end)
}
end
def handle_event("choose_limb", params, socket) do
num = String.to_integer(params["num"])
{:noreply, socket
|> update(:limbs, fn limbs ->
Map.update(limbs, num, [params["limb"]], &
case Enum.member?(&1, params["limb"]) do
false -> &1 ++ [params["limb"]] |> Enum.uniq
true -> &1 -- [params["limb"]]
end
)
end)
}
end
def handle_event("begin_choosing_limbs", params, socket) do
num = String.to_integer(params["num"])
{:noreply, socket
|> update(:limbs, fn limbs ->
Map.update(limbs, num, ["_choose"], & &1 ++ ["_choose"] |> Enum.uniq)
end)
}
end
def handle_event("clear_limbs", params, socket) do
num = String.to_integer(params["num"])
{:noreply, socket
|> update(:limbs, fn limbs -> Map.put(limbs, num, ["_choose"]) end)
}
end
def handle_event("done_choosing_limbs", params, socket) do
num = String.to_integer(params["num"])
{:noreply, socket
|> update(:limbs, fn limbs -> Map.update(limbs, num, [], & &1 -- ["_choose"]) end)
}
end
def handle_event("clear_services", _, socket) do
{:noreply, socket
|> assign(:services, %{})
|> assign(:limbs, %{})
|> assign(:schedule, nil)
|> assign(:employed, %{})
}
end
def handle_event("employ", params, socket) do
{:noreply, update(socket, :employed, fn employed ->
Map.update(employed, String.to_integer(params["num"]), params["name"],
&(if &1 == params["name"], do: nil, else: params["name"]))
end)}
end
def handle_event("clear_employees", _, socket) do
{:noreply, socket |> assign(:employed, %{}) }
end
def handle_event("clear_schedule", _, socket) do
{:noreply, socket
|> assign(:schedule, nil)
|> assign(:employed, %{})
}
end
def handle_event("schedule", params, socket) do
{:noreply, socket
|> assign(:schedule, params["shape"])
|> assign(:calendars, params["shape"] |> schedule_calendars(socket.assigns))
}
end
def handle_event("render_bios", params, socket), do:
{:noreply, socket |> assign(:display_bios, params["shape"]) }
def handle_event("customer", params, socket) do
{:noreply, socket |> assign(:customer, params
|> Map.take(~w[name phone email reference conditions])) }
end
def handle_event("addon", params, socket) do
num = String.to_integer(params["num"])
addon = String.to_integer(params["addon"])
{:noreply, socket |> update(:addons, fn addons ->
if Enum.member?(addons[num] || [], addon),
do: Map.update(addons, num, [], & &1 -- [addon]),
else: Map.update(addons, num, [addon], & &1 ++ [addon])
end) }
end
def handle_event("book", _, socket) do
links = (
socket.assigns
|> Map.take(~w[ employed schedule customer limbs ]a)
|> Pain.Order.book(
chosen_services(socket.assigns[:services]),
socket.assigns[:addons])
) |> IO.inspect()
{:noreply, socket |> assign(:booked, links) }
end
def handle_info {process, response}, socket do
Process.demonitor(process, [:flush])
send_update Schedule, id: "schedule", possible: response, process: nil
{:noreply, socket}
end
def classed_services do
{:ok, s} = (
:pain
|> Application.app_dir("priv")
|> Path.join("services.yml")
|> YamlElixir.read_from_file
); s
end
def all_services do
classed_services()["classes"] |> Enum.map(fn c ->
c["services"] |> Enum.map(&(Map.put(&1, "class", c["name"])))
end) |> List.flatten
end
def all_employees do
{:ok, e} = (
:pain
|> Application.app_dir("priv")
|> Path.join("employees.yml")
|> YamlElixir.read_from_file
);
e["employees"] |> Enum.filter(fn e -> e["enabled"] != false end)
end
def chosen_services(services) do
all = classed_services()["classes"] |> Enum.map(fn class ->
class["services"]
|> Enum.map(&(if &1["hanyu"], do: &1, else: Map.put(&1, "hanyu", class["hanyu"])))
|> Enum.map(&(Map.put(&1, "class", class["name"])))
end) |> List.flatten
services
|> Enum.reduce(%{}, fn { n, name }, chosen ->
Map.put(chosen, n, all |> Enum.find(&(&1["name"] == name)))
end)
end
def service_keys(services) do
all = all_services()
services |> Map.values() |> Enum.map(fn name ->
Enum.find(all, &(&1["name"] == name))["schedule_key"]
end)
end
def scheduled_block(schedule) do
(schedule <> ":00Z-04:00")
|> String.slice(0, 20)
|> NaiveDateTime.from_iso8601()
|> elem(1)
end
def employee_keys, do: all_employees() |> Enum.map(&(&1["schedule_key"]))
def schedule_calendars(schedule, assigns) do
(schedule <> Pain.Schedule.ending)
|> check_blocks_on_calendars(assigns[:services] |> service_keys, employee_keys())
end
def employee_bookable?(calendars, employee, services, employed) do
ss = all_services()
case (employed |> Enum.find(fn {_, name} -> name == employee["name"] end)) do
# remember: you can only book an employee once per block.
{n, _employee} ->
Map.put %{ 1 => false, 2 => false, 3 => false, 4 => false }, n, true
# usually, check each employee's schedule.
_ ->
services |> Enum.reduce(%{}, fn { n, s }, map ->
service = (ss |> Enum.filter(&(&1["name"] == s)) |> hd)["schedule_key"]
Map.put(map, n, calendars |> employee_can_do?(employee, service))
end)
end
end
@doc """
PainWeb.BookLive.bookable_by_gender(cals, %{
1 => "90Min Reflexology with Chinese Medicine",
2 => "90min Massage",
3 => "Cupping",
4 => "Wet Cupping" },
%{ 1 => "Bin Wang"})
"""
def bookable_by_gender(calendars, services, employed) do
calendars
|> bookable_employees_by_service(services)
|> Enum.reduce(%{}, fn {n, employees}, remaining ->
Map.put(remaining, n, employees
|> Enum.filter(&(!Enum.member?(Map.values(employed), &1["name"]))))
end)
|> Enum.reduce(%{}, fn {n, employees}, by_service ->
Map.put(by_service, n, employees |> Enum.reduce(%{}, fn employee, by_gender ->
Map.update(by_gender, employee["gender"], 1, &(&1+1))
end))
end)
end
def bookable_any(calendars, services, employed, decide) do
calendars
|> bookable_by_gender(services, employed)
|> Enum.reduce(%{}, fn {n, g}, by_service ->
Map.put(by_service, n, decide.(Map.values(g) |> Enum.reduce(0, &(&1 + &2)))) end)
end
def bookable_as_gender(calendars, services, employed, gender, decide) do
calendars
|> bookable_by_gender(services, employed)
|> Enum.reduce(%{}, fn {n, g}, by_service ->
Map.put(by_service, n, decide.(g[gender] || 0)) end)
end
@doc """
PainWeb.BookLive.bookable_employees_by_service(cals, %{
1 => "90Min Reflexology with Chinese Medicine",
2 => "90min Massage",
3 => "Cupping",
4 => "Wet Cupping" })
"""
def bookable_employees_by_service calendars, services do
ss = all_services()
es = all_employees()
services |> Enum.reduce(%{}, fn { n, s }, by_service ->
service_key = (ss |> Enum.filter(&(&1["name"] == s)) |> hd)["schedule_key"]
Map.put(by_service, n, es
|> Enum.filter(fn employee -> calendars |> employee_can_do?(employee, service_key) end)
|> Enum.map(&(Map.take(&1, ~w[name gender])))
) end)
end
def employee_can_do?(calendars, employee, service_key) do
(calendars
|> Enum.filter(&(&1["calendarID"] == employee["schedule_key"]))
|> Enum.filter(&(&1["appointmentTypeID"] == service_key))
|> Enum.filter(&(&1["valid"]))
|> length()
) > 0
end
def needing_choice(limbs) do
limbs
|> Enum.filter(fn {_n, limbs} -> Enum.member?(limbs, "_choose") end)
|> Map.new
end
def body_areas(chosen) do
(chosen || [])
|> Enum.filter(& !Enum.member?([nil, "_choose"], &1))
end
def sum_services chosen_services do
chosen_services
|> Enum.reduce(0, fn {_, service}, sum ->
sum + (service["duracion"]
|> String.split("$")
|> Enum.at(1)
|> String.to_float)
end)
end
def sum_addons addons, all_addons do
addons
|> Map.values()
|> List.flatten()
|> Enum.map(fn key -> all_addons |> Enum.find(& &1["id"] == key) end)
|> Enum.map(& &1["price"] |> String.to_float)
|> Enum.sum()
end
def explain_services assigns do
~F"""
<style>
ul { margin-bottom: 1rem; padding-left: 1rem; list-style: disc; }
li.service { margin-bottom: 1rem; }
a { text-decoration: underline; }
</style>
<ul class="services">
{#for {n, service} <- chosen_services(@services)}
<li class="service">
{service["name"]}
{#if service["hanyu"]} / {service["hanyu"]}{/if}
<br/>{service["duracion"]}<br/>
{#if (@booked || []) |> Enum.at(n-1) == "/error"}
<h3>An error occurred booking this appointment.</h3>
{#elseif (@booked || []) |> Enum.at(n-1)}
<a target="_blank" href={@booked |> Enum.at(n-1)}>
See or cancel this booking.
</a><br/>
{/if}
{#if length(body_areas @limbs[n]) == 0}
on no specific location on body.
{#if !@booked}
(<a href="#" :on-click="begin_choosing_limbs" phx-value-num={n}>
change</a>){/if}
{#else}
on:
{#if !@booked}
(<a href="#" :on-click="begin_choosing_limbs" phx-value-num={n}>
change</a>){/if}
<ul>{#for area <- body_areas(@limbs[n])}
<li>{area}</li>
{/for}</ul>
{/if}
<br/>Add-ons:<br/>
{#if !@booked}
{#for addon <- @all_addons}
<button :on-click="addon" phx-value-num={n} phx-value-addon={addon["id"]}
class={"btn", "join-item", "btn-sm",
"btn-active": Enum.member?(@addons[n] || [], addon["id"]) } >
<span>${addon["price"]}</span> /
<span>{addon["name"]}</span>
</button>
{/for}
{#else}
<ul>{#for addon <- ((@addons[n] || [])
|> Enum.map(fn key -> @all_addons |> Enum.find(& &1["id"] == key) end)) }
<li><span>${addon["price"]}</span> / <span>{addon["name"]}</span></li>
{#else}(none){/for}
</ul>
{/if}
</li>
{/for}
</ul>
{#if (@services |> chosen_services |> map_size) > 0}
Once your appointments have ended, you'll be charged a sum of: <br/>
${sum_services(chosen_services(@services)) + sum_addons(@addons, @all_addons)}
(plus taxes)
{/if}
"""
end
end