Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting multiple filter conditions per field #155

Closed
masiamj opened this issue Feb 8, 2023 · 15 comments
Closed

Supporting multiple filter conditions per field #155

masiamj opened this issue Feb 8, 2023 · 15 comments
Labels
bug Something isn't working

Comments

@masiamj
Copy link

masiamj commented Feb 8, 2023

Is your feature request related to a problem? Please describe.
Hi @woylie, first off, thanks so much for maintaining this project. It's fantastic and I've really enjoyed the API you expose.

The problem: I'm building a relatively static form where I'd like to allow users to enter a min and max range for a specific value. The interface for this would provide 2 separate inputs per field.

EDIT: I believe this problem is well-described here: #88. There's a mention to PR #89, but I'm not sure if this issue ever got resolved?

As an example, let's say we'd like to enter a minimum and maximum age, I believe the field definitions would look like:

[
  age: [label: "Minimum age", op: :>=, type: "number"],
  age: [label: "Maximum age", op: :<=, type: "number"]
]

From my implementation efforts and research, it appears this structure with the same field and different operators isn't yet supported by the library.

In researching this issue, I found the work you did to use separate labels for multiple fields, and stumbled across this conversation as well which I believe is pertinent.

Right now, I believe the age fields in the example would appear separately in the UI; however, upon collating the filter values, they would be merged, taking the value of the first age filter, the second one being discarded, and values from the UI being assigned to different inputs via the zip-based implementation here.

I could be totally wrong here, if you have any advice to support the use case above, it would be extremely helpful!

Describe the solution you'd like
My ideal solution would be that this library supports an arbitrary number of independently configured inputs per field just based on the fields configuration described above.

Describe alternatives you've considered
As an alternative, I tried creating a more manual implementation of the filter_fields component as described here, but was unsuccessful.

Additional context
As I mentioned, I could be totally off base here, but I've been struggling with this for a few hours and would love any advice.

Thanks so much!

@woylie woylie added bug Something isn't working and removed feature request labels Feb 8, 2023
@woylie
Copy link
Owner

woylie commented Feb 8, 2023

Hi @masiamj, thanks for the issue. I think you're right, and you already found the place where it goes wrong. I'll try to look into this on the weekend, unless you're up for opening a PR.

@masiamj
Copy link
Author

masiamj commented Feb 8, 2023

Hey @woylie! I'm a little swamped this week with some personal things so I doubt I'll have the time to make the update before the weekend, but will link a PR here if I do.

Otherwise, I'm happy to discuss any ideas if you start looking into it. Thanks a ton for your efforts maintaining this – great work!

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

I was able to look a bit further into this, and it appears that the problem actually lies in the to_form/4 implementation, specifically:

defp filter_reducer({field, opts}, acc, filters)

I didn't find the time to fix this yet, but I should be able to get back to it in the next couple of days.

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

Scratch that. I updated the tests with assertions on the value inputs, and they pass. The function I linked would only be problematic if two filters for the same field with the same operator would be used. I don't think that's a use case (if it is, let me know).

The zip you linked should be fine for static forms, since the number of filters returned in the meta struct should always match the number of fields in the options passed to the filter_fields function.

I'll need to test this in an actual application.

@masiamj
Copy link
Author

masiamj commented Feb 12, 2023

Hey @woylie thanks for looking into this!

First off, in my specific case, you're correct to say the same field with the same operator is not a desired use case.

Taking a deeper look at the code, I also think you're right that the zip is fine for static forms (not sure for the dynamic use case).

My new hypothesis is that the issue actually lies in the hidden_inputs_for_filter and potentially the use of inputs_for from Phoenix. My idea is that if 2 filters have the same name, age in the example above, then there would be 2 inputs named age, and one would get overridden leading to a mismatched/unexpected number of fields being submitted by the form, causing a mismatch where number of filters != number of form values.

If you think this is a valid idea I'm happy to help explore solutions.

Any thoughts here? Thanks!

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

I just tested it in a real-world application, and it works fine. Are you actually having min/max age filters in your form? Or do you by any chance have something like a min/max date or date time?

One thing to look out for is that the Flop filter value will be the string value as produced by the HTML input, and it will be passed back as it is to your input component.

I had a case with a start date and an inclusive end date, both represented by date inputs. The DB column was a date time, not a date, and before passing the end date to the query function, I added T23:59:59 to the end date value. On the way back, the full date time string was passed to the HTML input, but since the input only expected a date, it saw the string as invalid, which caused the input to be reset as soon as another field was changed. I had to make sure to turn the date/time string back into a date string that the date input could understand. Maybe something similar is going on in your form?

@masiamj
Copy link
Author

masiamj commented Feb 12, 2023

Interesting, thanks for checking this out.

Maybe this is just an issue with my implementation. I use string, select, and date filters, maybe my form fields below will provide better context:

[
    name: [label: "Name", op: :ilike_or, placeholder: "Search by name...", type: "search"],
    daily_trends_symbol: [
      label: "Symbol",
      op: :ilike,
      placeholder: "Search by ticker...",
      type: "search"
    ],
    price: [label: "Minimum Price ($)", op: :>=, type: "number"],
    price: [label: "Maximum Price ($)", op: :<=, type: "number"],
    change_percent: [label: "Minimum change today (%)", op: :>=, type: "number"],
    change_percent: [label: "Maximum change today (%)", op: :<=, type: "number"],
    market_cap: [label: "Minimum market cap ($)", op: :>=, type: "number"],
    market_cap: [label: "Maximum market cap ($)", op: :<=, type: "number"],
    trend: [
      label: "Trend",
      options: [{"Uptrend", "up"}, {"Neutral", "neutral"}, {"Downtrend", "down"}],
      prompt: "",
      type: "select"
    ],
    trend_change: [
      label: "Trend change today",
      options: [
        {"Neutral to Uptrend", "neutral_to_up"},
        {"Neutral to Downtrend", "neutral_to_down"},
        {"Uptrend to neutral", "up_to_neutral"},
        {"Uptrend to downtrend", "up_to_down"},
        {"Downtrend to Uptrend", "down_to_up"},
        {"Downtrend to neutral", "down_to_neutral"}
      ],
      prompt: "",
      type: "select"
    ],
    close_above_day_200_sma: [
      label: "Close > 200D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    close_above_day_50_sma: [
      label: "Close > 50D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    day_50_sma_above_day_200_sma: [
      label: "50D SMA > 200D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    sector: [
      label: "Sector",
      options: [
        {"Communications", "Communication Services"},
        {"Consumer Discretionary", "Consumer Cyclical"},
        {"Consumer Staples", "Consumer Defensive"},
        {"Energy", "Energy"},
        {"Financials", "Financial Services"},
        {"Healthcare", "Healthcare"},
        {"Industrials", "Industrials"},
        {"Materials", "Basic Materials"},
        {"Real Estate", "Real Estate"},
        {"Technology", "Technology"},
        {"Utilities", "Utilities"}
      ],
      prompt: "",
      type: "select"
    ],
    pe: [label: "Minimum PE Ratio ($)", op: :>=, type: "number"],
    pe: [label: "Maximum PE Ratio ($)", op: :<=, type: "number"],
    eps: [label: "Minimum EPS ($)", op: :>=, type: "number"],
    eps: [label: "Maximum EPS ($)", op: :<=, type: "number"],
    pe_ratio_ttm: [label: "Minimum PE Ratio (TTM)", op: :>=, type: "number"],
    pe_ratio_ttm: [label: "Maximum PE Ratio (TTM)", op: :<=, type: "number"],
    peg_ratio_ttm: [label: "Minimum PEG Ratio (TTM)", op: :>=, type: "number"],
    peg_ratio_ttm: [label: "Maximum PEG Ratio (TTM)", op: :<=, type: "number"],
    cash_ratio_ttm: [label: "Minimum Cash Ratio (TTM)", op: :>=, type: "number"],
    cash_ratio_ttm: [label: "Maximum Cash Ratio (TTM)", op: :<=, type: "number"],
    current_ratio_ttm: [label: "Minimum Current Ratio (TTM)", op: :>=, type: "number"],
    current_ratio_ttm: [label: "Maximum Current Ratio (TTM)", op: :<=, type: "number"],
    dividend_yield_ttm: [label: "Minimum Dividend Yield (TTM)", op: :>=, type: "number"],
    dividend_yield_ttm: [label: "Maximum Dividend Yield (TTM)", op: :<=, type: "number"],
    earnings_yield_ttm: [label: "Minimum Earnings Yield (TTM)", op: :>=, type: "number"],
    earnings_yield_ttm: [label: "Maximum Earnings Yield (TTM)", op: :<=, type: "number"],
    price_to_book_ratio_ttm: [
      label: "Minimum Price to Book Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    price_to_book_ratio_ttm: [
      label: "Maximum Price to Book Ratio (TTM)",
      op: :<=,
      type: "number"
    ],
    price_to_sales_ratio_ttm: [
      label: "Minimum Price to Sales Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    price_to_sales_ratio_ttm: [
      label: "Maximum Price to Sales Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    quick_ratio_ttm: [label: "Minimum Quick Ratio (TTM)", op: :>=, type: "number"],
    quick_ratio_ttm: [label: "Maximum Quick Ratio (TTM)", op: :<=, type: "number"],
    return_on_assets_ttm: [
      label: "Minimum Return on Assets (TTM)",
      op: :>=,
      type: "number"
    ],
    return_on_assets_ttm: [
      label: "Maximum Return on Assets (TTM)",
      op: :<=,
      type: "number"
    ],
    return_on_equity_ttm: [
      label: "Minimum Return on Equity (TTM)",
      op: :>=,
      type: "number"
    ],
    return_on_equity_ttm: [
      label: "Maximum Return on Equity (TTM)",
      op: :<=,
      type: "number"
    ],
    ipo_date: [label: "Minimum IPO date", op: :>=, type: "date"],
    ipo_date: [label: "Maximum IPO date", op: :<=, type: "date"]
  ]

I don't believe I'm encountering the issue you mentioned as what I'm seeing on submit is a field mismatch in that the price form value is actually being assigned to the change_percent filter. Any thoughts?

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

price_min: [label: "Minimum Price ($)", op: :>=, type: "number"],
price_max: [label: "Maximum Price ($)", op: :<=, type: "number"],

Are these two different columns? Or should this both be price?

@masiamj
Copy link
Author

masiamj commented Feb 12, 2023

^Sorry yes edited to both be price, that was a bad config. My mistake.

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

Does it work after changing it?

@masiamj
Copy link
Author

masiamj commented Feb 12, 2023

Unfortunately not (sorry I copied that from an experiment I was trying, the initial implementation is still having an issue). In an effort to provide as much helpful debugging information as possible, I'm copying the LiveView implementation and resulting filters, hopefully this will help.

LiveView:

defmodule LoinWeb.ScreenerLive do
  use LoinWeb, :live_view

  alias Loin.{FMP, Intl}

  @fields [
    name: [label: "Name", op: :ilike_or, placeholder: "Search by name...", type: "search"],
    daily_trends_symbol: [
      label: "Symbol",
      op: :ilike,
      placeholder: "Search by ticker...",
      type: "search"
    ],
    price_min: [label: "Minimum Price ($)", op: :>=, type: "number"],
    price_max: [label: "Maximum Price ($)", op: :<=, type: "number"],
    change_percent: [label: "Minimum change today (%)", op: :>=, type: "number"],
    change_percent: [label: "Maximum change today (%)", op: :<=, type: "number"],
    market_cap: [label: "Minimum market cap ($)", op: :>=, type: "number"],
    market_cap: [label: "Maximum market cap ($)", op: :<=, type: "number"],
    trend: [
      label: "Trend",
      options: [{"Uptrend", "up"}, {"Neutral", "neutral"}, {"Downtrend", "down"}],
      prompt: "",
      type: "select"
    ],
    trend_change: [
      label: "Trend change today",
      options: [
        {"Neutral to Uptrend", "neutral_to_up"},
        {"Neutral to Downtrend", "neutral_to_down"},
        {"Uptrend to neutral", "up_to_neutral"},
        {"Uptrend to downtrend", "up_to_down"},
        {"Downtrend to Uptrend", "down_to_up"},
        {"Downtrend to neutral", "down_to_neutral"}
      ],
      prompt: "",
      type: "select"
    ],
    close_above_day_200_sma: [
      label: "Close > 200D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    close_above_day_50_sma: [
      label: "Close > 50D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    day_50_sma_above_day_200_sma: [
      label: "50D SMA > 200D SMA",
      options: [{"Yes", true}, {"No", false}],
      prompt: "",
      type: "select"
    ],
    sector: [
      label: "Sector",
      options: [
        {"Communications", "Communication Services"},
        {"Consumer Discretionary", "Consumer Cyclical"},
        {"Consumer Staples", "Consumer Defensive"},
        {"Energy", "Energy"},
        {"Financials", "Financial Services"},
        {"Healthcare", "Healthcare"},
        {"Industrials", "Industrials"},
        {"Materials", "Basic Materials"},
        {"Real Estate", "Real Estate"},
        {"Technology", "Technology"},
        {"Utilities", "Utilities"}
      ],
      prompt: "",
      type: "select"
    ],
    pe: [label: "Minimum PE Ratio ($)", op: :>=, type: "number"],
    pe: [label: "Maximum PE Ratio ($)", op: :<=, type: "number"],
    eps: [label: "Minimum EPS ($)", op: :>=, type: "number"],
    eps: [label: "Maximum EPS ($)", op: :<=, type: "number"],
    pe_ratio_ttm: [label: "Minimum PE Ratio (TTM)", op: :>=, type: "number"],
    pe_ratio_ttm: [label: "Maximum PE Ratio (TTM)", op: :<=, type: "number"],
    peg_ratio_ttm: [label: "Minimum PEG Ratio (TTM)", op: :>=, type: "number"],
    peg_ratio_ttm: [label: "Maximum PEG Ratio (TTM)", op: :<=, type: "number"],
    cash_ratio_ttm: [label: "Minimum Cash Ratio (TTM)", op: :>=, type: "number"],
    cash_ratio_ttm: [label: "Maximum Cash Ratio (TTM)", op: :<=, type: "number"],
    current_ratio_ttm: [label: "Minimum Current Ratio (TTM)", op: :>=, type: "number"],
    current_ratio_ttm: [label: "Maximum Current Ratio (TTM)", op: :<=, type: "number"],
    dividend_yield_ttm: [label: "Minimum Dividend Yield (TTM)", op: :>=, type: "number"],
    dividend_yield_ttm: [label: "Maximum Dividend Yield (TTM)", op: :<=, type: "number"],
    earnings_yield_ttm: [label: "Minimum Earnings Yield (TTM)", op: :>=, type: "number"],
    earnings_yield_ttm: [label: "Maximum Earnings Yield (TTM)", op: :<=, type: "number"],
    price_to_book_ratio_ttm: [
      label: "Minimum Price to Book Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    price_to_book_ratio_ttm: [
      label: "Maximum Price to Book Ratio (TTM)",
      op: :<=,
      type: "number"
    ],
    price_to_sales_ratio_ttm: [
      label: "Minimum Price to Sales Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    price_to_sales_ratio_ttm: [
      label: "Maximum Price to Sales Ratio (TTM)",
      op: :>=,
      type: "number"
    ],
    quick_ratio_ttm: [label: "Minimum Quick Ratio (TTM)", op: :>=, type: "number"],
    quick_ratio_ttm: [label: "Maximum Quick Ratio (TTM)", op: :<=, type: "number"],
    return_on_assets_ttm: [
      label: "Minimum Return on Assets (TTM)",
      op: :>=,
      type: "number"
    ],
    return_on_assets_ttm: [
      label: "Maximum Return on Assets (TTM)",
      op: :<=,
      type: "number"
    ],
    return_on_equity_ttm: [
      label: "Minimum Return on Equity (TTM)",
      op: :>=,
      type: "number"
    ],
    return_on_equity_ttm: [
      label: "Maximum Return on Equity (TTM)",
      op: :<=,
      type: "number"
    ],
    ipo_date: [label: "Minimum IPO date", op: :>=, type: "date"],
    ipo_date: [label: "Maximum IPO date", op: :<=, type: "date"]
  ]

  @impl true
  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:filtered_data, [])
      |> assign(:meta, %{})
      |> assign(:form_fields, @fields)

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <div class="flex flex-row flex-wrap">
        <div class="lg:h-[94vh] w-full lg:w-1/5 lg:overflow-y-scroll">
          <div class="flex flex-row items-center justify-between bg-gray-50 sticky top-0 p-4">
            <p class="text-gray-500 text-xs">Screener filters</p>
            <button
              class="text-blue-500 text-xs px-2 py-1 bg-white hover:bg-blue-50 rounded"
              phx-click="reset-filter"
            >
              Reset filters
            </button>
          </div>
          <.form
            :let={f}
            class="grid grid-cols-2 lg:grid-cols-1 gap-4 p-4"
            for={@meta}
            phx-change="update-filter"
          >
            <Flop.Phoenix.filter_fields :let={i} form={f} fields={@form_fields}>
              <.input
                id={i.id}
                name={i.name}
                label={i.label}
                type={i.type}
                value={i.value}
                field={{i.form, i.field}}
                {i.rest}
              />
            </Flop.Phoenix.filter_fields>
          </.form>
        </div>
        <div class="lg:h-[94vh] w-full lg:w-4/5">
          <Flop.Phoenix.table
            items={@filtered_data}
            meta={@meta}
            opts={[
              container: true,
              container_attrs: [class: "w-full screener-table-container lg:h-[88vh]"],
              table_attrs: [class: "min-w-full border-separate", style: "border-spacing: 0"],
              tbody_td_attrs: [class: "px-2 py-0.5 bg-white border-b border-gray-200 text-xs"],
              thead_th_attrs: [
                class: "bg-gray-100 sticky top-0 px-2 py-2 text-left text-xs font-medium"
              ]
            ]}
            path={~p"/screener"}
          >
            <:col :let={item} col_style="min-width:200px;" label="Name" field={:name}>
              <.link class="flex flex-col" patch={~p"/s/#{item.fmp_securities_symbol}"}>
                <span class="text-gray-500 line-clamp-1" style="font-size:10px;">
                  <%= item.name %>
                </span>
                <span class="font-medium line-clamp-1"><%= item.fmp_securities_symbol %></span>
              </.link>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="Price/share" field={:price}>
              <%= Intl.format_money_decimal(item.price) %>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="Change" field={:change_value}>
              <span class={class_for_value(item.change_value)}>
                <%= Intl.format_money_decimal(item.change_value) %>
              </span>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="Change %" field={:change_percent}>
              <span class={class_for_value(item.change_percent)}>
                <%= Intl.format_percent(item.change_percent) %>
              </span>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="Market cap" field={:market_cap}>
              <%= Intl.format_money_decimal(item.market_cap) %>
            </:col>
            <:col :let={item} col_style="min-width:120px;" label="Trend" field={:trend}>
              <.trend_badge trend={item.trend} />
            </:col>
            <:col :let={item} col_style="min-width:120px;" label="Trend at" field={:daily_trends_date}>
              <%= item.daily_trends_date %>
            </:col>
            <:col :let={item} col_style="min-width:120px;" label="Prev trend" field={:previous_trend}>
              <.trend_badge trend={item.previous_trend} />
            </:col>
            <:col :let={item} col_style="min-width:120px;" label="Trend change" field={:trend_change}>
              <.trend_change_badge trend_change={item.trend_change} />
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Price > 200D"
              field={:close_above_day_200_sma}
            >
              <%= boolean_content(item.close_above_day_200_sma) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Price > 50D"
              field={:close_above_day_50_sma}
            >
              <%= boolean_content(item.close_above_day_50_sma) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="50D SMA > 200D SMA"
              field={:day_50_sma_above_day_200_sma}
            >
              <%= boolean_content(item.day_50_sma_above_day_200_sma) %>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="200D SMA" field={:fmp_securities_day_200_sma}>
              <%= Intl.format_money_decimal(item.fmp_securities_day_200_sma) %>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="50D SMA" field={:fmp_securities_day_50_sma}>
              <%= Intl.format_money_decimal(item.fmp_securities_day_50_sma) %>
            </:col>

            <:col :let={item} col_style="min-width:120px;" label="Sector" field={:sector}>
              <%= item.sector %>
            </:col>
            <:col :let={item} col_style="min-width:120px;" label="Industry" field={:industry}>
              <span class="line-clamp-2">
                <%= item.industry %>
              </span>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="PE Ratio" field={:pe}>
              <%= Intl.format_decimal(item.pe) %>
            </:col>
            <:col :let={item} col_style="min-width:100px;" label="EPS" field={:eps}>
              <%= Intl.format_money_decimal(item.eps) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="PE Ratio (TTM)"
              field={:pe_ratio_ttm}
            >
              <%= Intl.format_decimal(item.pe_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="PE Growth Ratio (TTM)"
              field={:peg_ratio_ttm}
            >
              <%= Intl.format_decimal(item.peg_ratio_ttm) %>
            </:col>

            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Cash Ratio (TTM)"
              field={:cash_ratio_ttm}
            >
              <%= Intl.format_decimal(item.cash_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Current Ratio (TTM)"
              field={:current_ratio_ttm}
            >
              <%= Intl.format_decimal(item.current_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Dividend Yield (TTM)"
              field={:dividend_yield_ttm}
            >
              <%= Intl.format_percent(item.dividend_yield_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Earnings Yield (TTM)"
              field={:earnings_yield_ttm}
            >
              <%= Intl.format_percent(item.earnings_yield_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Net Profit Margin (TTM)"
              field={:net_profit_margin_ttm}
            >
              <%= Intl.format_percent(item.net_profit_margin_ttm) %>
            </:col>

            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Price/Book Ratio (TTM)"
              field={:price_to_book_ratio_ttm}
            >
              <%= Intl.format_decimal(item.price_to_book_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Price/Sales Ratio (TTM)"
              field={:price_to_sales_ratio_ttm}
            >
              <%= Intl.format_decimal(item.price_to_sales_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Quick Ratio (TTM)"
              field={:quick_ratio_ttm}
            >
              <%= Intl.format_decimal(item.quick_ratio_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Return on Assets (TTM)"
              field={:return_on_assets_ttm}
            >
              <%= Intl.format_percent(item.return_on_assets_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Return on Equity (TTM)"
              field={:return_on_equity_ttm}
            >
              <%= Intl.format_percent(item.return_on_equity_ttm) %>
            </:col>
            <:col
              :let={item}
              col_style="min-width:100px;"
              label="Employees"
              field={:full_time_employees}
            >
              <%= Intl.format_decimal(item.full_time_employees, :short) %>
            </:col>

            <:col :let={item} col_style="min-width:100px;" label="IPO Date" field={:ipo_date}>
              <%= Intl.format_date(item.ipo_date) %>
            </:col>
          </Flop.Phoenix.table>

          <Flop.Phoenix.pagination
            meta={@meta}
            path={~p"/screener"}
            opts={[
              current_link_attrs: [class: "text-white bg-blue-500 p-1 rounded text-xs"],
              next_link_attrs: [
                class:
                  "bg-gray-50 hover:bg-gray-100 p-1 rounded text-xs text-gray-500 hover:text-gray-800 order-last"
              ],
              next_link_content: "Next",
              page_links: {:ellipsis, 3},
              pagination_link_attrs: [
                class:
                  "bg-gray-50 hover:bg-gray-100 p-1 rounded text-xs text-gray-500 hover:text-gray-800"
              ],
              pagination_list_attrs: [class: "flex flex-row items-center gap-2"],
              previous_link_attrs: [
                class:
                  "order-first bg-gray-50 hover:bg-gray-100 p-1 rounded text-xs text-gray-500 hover:text-gray-800"
              ],
              previous_link_content: "Previous",
              wrapper_attrs: [
                class: "flex flex-row items-center gap-2 w-full justify-center min-h-[6vh] py-2"
              ]
            ]}
          />
        </div>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("update-filter", params, socket) do
    {:noreply, push_patch(socket, to: ~p"/screener?#{params}")}
  end

  @impl true
  def handle_event("reset-filter", _, %{assigns: assigns} = socket) do
    flop = %Flop{} |> Flop.set_page(1) |> Flop.reset_filters()
    path = Flop.Phoenix.build_path(~p"/screener", flop, backend: assigns.meta.backend)
    {:noreply, push_patch(socket, to: path)}
  end

  @impl Phoenix.LiveView
  def handle_params(params, _, socket) do
    case FMP.filter_screener(params) do
      {:ok, {results, meta}} ->
        {:noreply, assign(socket, %{filtered_data: results, meta: meta})}

      _ ->
        {:noreply, push_navigate(socket, to: ~p"/screener")}
    end
  end

  # Private

  defp boolean_content(value) do
    case value do
      true -> "✅"
      false -> "❌"
      _ -> "-"
    end
  end

  defp class_for_value(value) do
    case value do
      x when x > 0 -> "text-green-600"
      x when x < 0 -> "text-red-600"
      _ -> "text-gray-500"
    end
  end

  defp trend_badge(%{trend: nil} = assigns) do
    ~H"""

    """
  end

  defp trend_badge(%{trend: "up"} = assigns) do
    ~H"""
    <div class="text-green-500 text-xs font-medium flex items-center justify-center bg-green-100 px-2 py-0.5 rounded">
      <span>Uptrend</span>
    </div>
    """
  end

  defp trend_badge(%{trend: "down"} = assigns) do
    ~H"""
    <div class="text-red-500 text-xs font-medium flex items-center justify-center bg-red-100 px-2 py-0.5 rounded">
      <span>Downtrend</span>
    </div>
    """
  end

  defp trend_badge(%{trend: "neutral"} = assigns) do
    ~H"""
    <div class="text-gray-500 text-xs font-medium flex items-center justify-center bg-gray-100 px-2 py-0.5 rounded">
      <span>Neutral</span>
    </div>
    """
  end

  defp trend_change_badge(%{trend_change: nil} = assigns) do
    ~H"""

    """
  end

  defp trend_change_badge(%{trend_change: trend_change} = assigns)
       when trend_change in ["down_to_up", "neutral_to_up"] do
    ~H"""
    <div class="text-green-500 text-xs font-medium flex items-center justify-center bg-green-100 px-2 py-0.5 rounded">
      <span>To Uptrend</span>
    </div>
    """
  end

  defp trend_change_badge(%{trend_change: trend_change} = assigns)
       when trend_change in ["up_to_down", "neutral_to_down"] do
    ~H"""
    <div class="text-red-500 text-xs font-medium flex items-center justify-center bg-red-100 px-2 py-0.5 rounded">
      <span>To Downtrend</span>
    </div>
    """
  end

  defp trend_change_badge(%{trend_change: trend_change} = assigns)
       when trend_change in ["up_to_neutral", "down_to_neutral"] do
    ~H"""
    <div class="text-gray-500 text-xs font-medium flex items-center justify-center bg-gray-100 px-2 py-0.5 rounded">
      <span>To Neutral</span>
    </div>
    """
  end
end

Params on update-filter event:

%{
  "_target" => ["filters", "8", "value"],
  "filters" => %{
    "7" => %{"field" => "trend_change", "value" => ""},
    "27" => %{"field" => "earnings_yield_ttm", "op" => "<=", "value" => ""},
    "25" => %{"field" => "dividend_yield_ttm", "op" => "<=", "value" => ""},
    "14" => %{"field" => "eps", "op" => ">=", "value" => ""},
    "8" => %{"field" => "close_above_day_200_sma", "value" => "up"},
    "36" => %{"field" => "return_on_equity_ttm", "op" => ">=", "value" => ""},
    "20" => %{"field" => "cash_ratio_ttm", "op" => ">=", "value" => ""},
    "31" => %{
      "field" => "price_to_sales_ratio_ttm",
      "op" => ">=",
      "value" => ""
    },
    "5" => %{"field" => "market_cap", "op" => "<=", "value" => ""},
    "6" => %{"field" => "trend", "value" => ""},
    "3" => %{"field" => "change_percent", "op" => "<=", "value" => ""},
    "33" => %{"field" => "quick_ratio_ttm", "op" => "<=", "value" => ""},
    "34" => %{"field" => "return_on_assets_ttm", "op" => ">=", "value" => ""},
    "26" => %{"field" => "earnings_yield_ttm", "op" => ">=", "value" => ""},
    "21" => %{"field" => "cash_ratio_ttm", "op" => "<=", "value" => ""},
    "15" => %{"field" => "eps", "op" => "<=", "value" => ""},
    "23" => %{"field" => "current_ratio_ttm", "op" => "<=", "value" => ""},
    "12" => %{"field" => "pe", "op" => ">=", "value" => ""},
    "0" => %{"field" => "name", "op" => "ilike_or", "value" => ""},
    "11" => %{"field" => "sector", "value" => ""},
    "17" => %{"field" => "pe_ratio_ttm", "op" => "<=", "value" => ""},
    "10" => %{"field" => "day_50_sma_above_day_200_sma", "value" => ""},
    "18" => %{"field" => "peg_ratio_ttm", "op" => ">=", "value" => ""},
    "39" => %{"field" => "ipo_date", "op" => "<=", "value" => ""},
    "38" => %{"field" => "ipo_date", "op" => ">=", "value" => ""},
    "35" => %{"field" => "return_on_assets_ttm", "op" => "<=", "value" => ""},
    "32" => %{"field" => "quick_ratio_ttm", "op" => ">=", "value" => ""},
    "28" => %{"field" => "price_to_book_ratio_ttm", "op" => ">=", "value" => ""},
    "29" => %{"field" => "price_to_book_ratio_ttm", "op" => "<=", "value" => ""},
    "30" => %{
      "field" => "price_to_sales_ratio_ttm",
      "op" => ">=",
      "value" => ""
    },
    "2" => %{"field" => "change_percent", "op" => ">=", "value" => ""},
    "22" => %{"field" => "current_ratio_ttm", "op" => ">=", "value" => ""},
    "24" => %{"field" => "dividend_yield_ttm", "op" => ">=", "value" => ""},
    "37" => %{"field" => "return_on_equity_ttm", "op" => "<=", "value" => ""},
    "4" => %{"field" => "market_cap", "op" => ">=", "value" => ""},
    "13" => %{"field" => "pe", "op" => "<=", "value" => ""},
    "16" => %{"field" => "pe_ratio_ttm", "op" => ">=", "value" => ""},
    "9" => %{"field" => "close_above_day_50_sma", "value" => ""},
    "1" => %{"field" => "daily_trends_symbol", "op" => "ilike", "value" => ""},
    "19" => %{"field" => "peg_ratio_ttm", "op" => "<=", "value" => ""}
  }
}

This is the output with a single filter on the trend column for value: up.

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

In the code you posted, it still says price_min and price_max. Can you confirm that this was changed?

@woylie
Copy link
Owner

woylie commented Feb 12, 2023

Flop will not render inputs for fields that are not filterable.

@masiamj
Copy link
Author

masiamj commented Feb 12, 2023

You are completely correct, I don't know how I missed it (think this was a bad merge), but with the change to price the code is functioning as expected.

I'm so sorry for any stress/confusion I caused here, thank you so much for your amazing library and help.

@woylie woylie closed this as completed Feb 12, 2023
@woylie
Copy link
Owner

woylie commented Feb 12, 2023

No worries!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants