app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 2a804f4a65313a42a4628db19f7387621d492d05
parent 5d9a0e503af7b59455ad6ec572825f72caf1a804
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Thu, 14 Nov 2024 23:37:02 +0000

Edit `/models/trade-product` add list card and line entry components. Edit `/models/trade-product/add` handlers. Add/edit types, utils.

Diffstat:
Asrc/lib/components/line_entries_between.svelte | 23+++++++++++++++++++++++
Asrc/lib/components/line_entry_data.svelte | 17+++++++++++++++++
Asrc/lib/components/line_entry_label.svelte | 17+++++++++++++++++
Asrc/lib/components/trade_product_list_card.svelte | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib/types.ts | 7+++++++
Msrc/lib/utils/trade_product.ts | 42+++++++++++++-----------------------------
Msrc/routes/(app)/models/trade-product/+page.svelte | 35+++++++++--------------------------
Msrc/routes/(app)/models/trade-product/add/+page.svelte | 22+++++++++++-----------
8 files changed, 342 insertions(+), 66 deletions(-)

diff --git a/src/lib/components/line_entries_between.svelte b/src/lib/components/line_entries_between.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import { fmt_cl, type IClOpt } from "@radroots/svelte-lib"; + import LineEntryData from "./line_entry_data.svelte"; + import LineEntryLabel from "./line_entry_label.svelte"; + + export let basis: IClOpt & { + label: IClOpt & { + value: string; + value_f?: string; + }; + data: IClOpt & { + value: string; + value_f?: string; + }; + }; +</script> + +<div + class={`${fmt_cl(basis.classes)} flex flex-row h-6 w-full justify-between items-center`} +> + <LineEntryLabel basis={basis.label} /> + <LineEntryData basis={basis.data} /> +</div> diff --git a/src/lib/components/line_entry_data.svelte b/src/lib/components/line_entry_data.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import { fmt_cl, type IClOpt, type IClWrapOpt } from "@radroots/svelte-lib"; + + export let basis: IClOpt & + IClWrapOpt & { + value: string; + value_f?: string; + }; +</script> + +<div class={`${fmt_cl(basis.classes_wrap)} flex flex-row h-6`}> + <p + class={`${fmt_cl(basis.classes)} font-sans font-[500] text-[1.05rem] text-justify truncate text-layer-1-glyph-shade`} + > + {basis.value || basis.value_f || ``} + </p> +</div> diff --git a/src/lib/components/line_entry_label.svelte b/src/lib/components/line_entry_label.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import { fmt_cl, type IClOpt, type IClWrapOpt } from "@radroots/svelte-lib"; + + export let basis: IClOpt & + IClWrapOpt & { + value: string; + value_f?: string; + }; +</script> + +<div class={`${fmt_cl(basis.classes_wrap)} flex flex-row h-6`}> + <p + class={`${fmt_cl(basis.classes)} font-sans font-[400] text-layer-1-glyph_d text-[1.05rem] capitalize`} + > + {basis.value || basis.value_f || ``} + </p> +</div> diff --git a/src/lib/components/trade_product_list_card.svelte b/src/lib/components/trade_product_list_card.svelte @@ -0,0 +1,245 @@ +<script lang="ts"> + import type { TradeProductBundle } from "$lib/types"; + import { fmt_location_gcs } from "@radroots/models"; + import { + app_layout, + fmt_geol_latitude, + fmt_geol_longitude, + Glyph, + locale, + t, + } from "@radroots/svelte-lib"; + import { + fmt_currency_price, + fmt_plural_agreement, + mass_tf_str, + num_min, + parse_currency_price, + } from "@radroots/utils"; + import LineEntriesBetween from "./line_entries_between.svelte"; + import LineEntryData from "./line_entry_data.svelte"; + import LineEntryLabel from "./line_entry_label.svelte"; + + export let basis: { + result: TradeProductBundle; + }; + $: ({ + result: { trade_product, location_gcs }, + } = basis); + + $: tradeproduct_qty_sold = 0; + $: tradeproduct_qty_avail = + num_min(trade_product.qty_avail, 1) - tradeproduct_qty_sold; +</script> + +<div + class={`flex flex-col min-h-[22rem] w-${$app_layout} justify-start items-start bg-layer-1-surface touch-layer-1 touch-layer-1-raise-less round-44`} +> + <div + class={`flex flex-row h-[10rem] w-full justify-center items-center border-b-line border-b-layer-1-surface-edge`} + > + <button + class={`group flex flex-row w-full justify-center items-center`} + on:click={async () => {}} + > + <div + class={`relative flex flex-col w-full justify-start items-center`} + > + <div + class={`relative flex flex-row py-2 px-[0.8rem] justify-center items-center`} + > + <Glyph + basis={{ + classes: `text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`, + dim: `xl`, + weight: `bold`, + key: `camera`, + }} + /> + <div + class={`absolute top-0 right-0 flex flex-row justify-center items-center`} + > + <Glyph + basis={{ + classes: `text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`, + dim: `xs`, + weight: `bold`, + key: `plus`, + }} + /> + </div> + </div> + <div + class={`absolute -bottom-4 left-0 flex flex-row w-full justify-center items-center`} + > + <p + class={`font-sans font-[500] text-[1rem] text-layer-0-glyph group-active:text-layer-0-glyph_a el-re`} + > + {`${$t(`icu.no_*`, { value: `${$t(`common.photos`)}`.toLowerCase() })}`} + </p> + </div> + </div> + </button> + </div> + {#if location_gcs} + <div + class={`flex flex-col min-h-[11rem] w-full pt-8 pb-12 justify-start items-start`} + > + <div + class={`flex flex-col w-full px-5 gap-6 justify-center items-center`} + > + <div class={`grid grid-cols-12 w-full`}> + <LineEntryLabel + basis={{ + classes_wrap: `col-span-7`, + value: `"${trade_product.title}"`, + }} + /> + <LineEntryData + basis={{ + classes_wrap: `col-span-5 justify-end capitalize`, + value: `${trade_product.key}`, + }} + /> + </div> + <div class={`grid grid-cols-12 w-full gap-y-[2px]`}> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.origin`)}`, + }, + data: { + value: `${fmt_geol_latitude( + location_gcs.lat, + `d`, + 4, + )}, ${fmt_geol_longitude( + location_gcs.lng, + `d`, + 4, + )}`, + }, + }} + /> + <LineEntryData + basis={{ + classes_wrap: `col-span-12 justify-end capitalize`, + value: `${fmt_location_gcs(location_gcs, `city`)}`, + }} + /> + </div> + <div class={`grid grid-cols-12 w-full gap-y-[2px]`}> + {#await parse_currency_price($locale, trade_product.price_currency, trade_product.price_amt) then price} + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.price`)}`, + }, + data: { + value: `${price ? fmt_currency_price(price) : ``} / ${`${$t(`measurement.mass.unit.${trade_product.price_qty_unit}_ab`)}`}`, + }, + }} + /> + {/await} + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`icu.*_order`, { value: `${$t(`common.quantity`)}` })}`, + }, + data: { + value: `${trade_product.qty_amt} / ${`${$t(`measurement.mass.unit.${trade_product.qty_unit}_ab`)}`}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`icu.*_available`, { value: `${$t(`common.quantity`)}` })}`, + }, + data: { + value: `${tradeproduct_qty_avail} ${trade_product.qty_label || fmt_plural_agreement(tradeproduct_qty_avail, `${$t(`common.bag`)}`, `${$t(`common.bags`)}`)}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`icu.*_sold`, { value: `${$t(`common.quantity`)}` })}`, + }, + data: { + value: `${tradeproduct_qty_sold} ${trade_product.qty_label || fmt_plural_agreement(tradeproduct_qty_sold, `${$t(`common.bag`)}`, `${$t(`common.bags`)}`)}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.lot`)}`, + }, + data: { + classes: `capitalize`, + value: `${trade_product.lot}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.process`)}`, + }, + data: { + classes: `capitalize`, + value: `${trade_product.process.replaceAll(`_`, ` `)}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.profile`)}`, + }, + data: { + classes: `capitalize`, + value: `${trade_product.profile.replaceAll(`_`, ` `)}`, + }, + }} + /> + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`common.year`)}`, + }, + data: { + value: `${trade_product.year}`, + }, + }} + /> + </div> + <div class={`grid grid-cols-12 w-full gap-y-[2px]`}> + {#await parse_currency_price($locale, trade_product.price_currency, trade_product.price_amt * Math.floor(Math.max(trade_product.price_qty_amt, 1)) * mass_tf_str(trade_product.price_qty_unit, trade_product.qty_unit, trade_product.qty_amt)) then price} + <LineEntriesBetween + basis={{ + classes: `col-span-12`, + label: { + value: `${$t(`icu.total_*`, { value: `${$t(`common.value`)}` })}`, + }, + data: { + value: `${price ? fmt_currency_price(price) : ``} / ${`${$t(`measurement.mass.unit.${trade_product.price_qty_unit}_ab`)}`}`, + }, + }} + /> + {/await} + </div> + </div> + </div> + {/if} +</div> diff --git a/src/lib/types.ts b/src/lib/types.ts @@ -0,0 +1,6 @@ +import type { LocationGcs, TradeProduct } from "@radroots/models"; + +export type TradeProductBundle = { + trade_product: TradeProduct; + location_gcs?: LocationGcs; +}; +\ No newline at end of file diff --git a/src/lib/utils/trade_product.ts b/src/lib/utils/trade_product.ts @@ -1,13 +1,13 @@ import { parse_trade_product_form_keys, trade_product_form_fields, trade_product_form_vals, type IModelsForm, type TradeProductFormFields } from "@radroots/models"; import { fmt_id, kv } from "@radroots/svelte-lib"; -import { err_msg, type ErrorMessage, type ResultPass } from "@radroots/utils"; +import { err_msg, obj_en, type ErrorMessage, type ResultPass } from "@radroots/utils"; -const trade_products_field_validate = (field: IModelsForm, field_val: string): boolean => { +const trade_products_field_validate = (field_basis: IModelsForm, field_val: string): boolean => { if ( - (!field.optional && !field.validation.test(field_val)) || - (field.optional && + (!field_basis.optional && !field_basis.validation.test(field_val)) || + (field_basis.optional && field_val && - !field.validation.test(field_val)) + !field_basis.validation.test(field_val)) ) return false; return true; }; @@ -21,10 +21,7 @@ export const trade_product_fields_assign = async (opts?: { const fields = { ...trade_product_form_vals }; - for (const [field_ks, field] of Object.entries( - trade_product_form_fields, - )) { - const field_k = parse_trade_product_form_keys(field_ks); + for (const [field_k, _] of obj_en(trade_product_form_fields, parse_trade_product_form_keys)) { if (!field_k) continue; const field_val = await kv.get(`${opts?.kv_pref || fmt_id()}-${field_k}`); if (field_val) fields[field_k] = field_val; @@ -46,21 +43,15 @@ export const trade_product_fields_validate = async (opts: { const fields = { ...trade_product_form_vals }; - for (const [field_ks, field] of Object.entries( - trade_product_form_fields, - )) { - const field_k = parse_trade_product_form_keys(field_ks); + for (const [field_k, _] of obj_en(trade_product_form_fields, parse_trade_product_form_keys)) { if (!field_k) continue; const field_val = await kv.get(`${opts?.kv_pref || fmt_id()}-${field_k}`); if (field_val) fields[field_k] = field_val; } if (opts?.field_defaults && opts?.field_defaults?.length > 0) for (const [field_k, field_v] of opts?.field_defaults) if (!fields[field_k]) fields[field_k] = field_v; - - for (const [field_ks, field_val] of Object.entries(fields)) { - const field_k = parse_trade_product_form_keys(field_ks); + for (const [field_k, field] of obj_en(fields, parse_trade_product_form_keys)) { if (!field_k) continue; - const field = trade_product_form_fields[field_k] - if (!trade_products_field_validate(field, field_val)) { + if (!trade_products_field_validate(trade_product_form_fields[field_k], field)) { if (opts.fields_pass?.includes(field_k)) continue; return err_msg(field_k); } @@ -80,10 +71,7 @@ export const tradeproduct_validate_kv = async (opts?: { const vals = { ...trade_product_form_vals }; - for (const [k, field] of Object.entries( - trade_product_form_fields, - )) { - const field_k = parse_trade_product_form_keys(k); + for (const [field_k, field] of obj_en(trade_product_form_fields, parse_trade_product_form_keys)) { if (!field_k) continue; const field_id = `${opts?.kv_pref || fmt_id()}-${field_k}`; const field_val = await kv.get(field_id); @@ -104,10 +92,7 @@ export const tradeproduct_validate_kv = async (opts?: { export const tradeproduct_init_kv = async (kv_pref: string): Promise<void> => { try { - for (const k of Object.keys( - trade_product_form_fields, - )) { - const field_k = parse_trade_product_form_keys(k); + for (const [field_k, _] of obj_en(trade_product_form_fields, parse_trade_product_form_keys)) { if (!field_k) continue; const field_id = `${kv_pref}-${field_k}` await kv.delete(field_id); @@ -122,9 +107,8 @@ export const tradeproduct_validate_fields = async (opts: { fields: string[]; }): Promise<ResultPass | ErrorMessage<string>> => { try { - for (const field of opts.fields) { - const field_k = parse_trade_product_form_keys(field); - if (!field_k) return err_msg(field); + for (const field_k of opts.fields.map(parse_trade_product_form_keys)) { + if (!field_k) return err_msg(field_k); const field_id = `${opts.kv_pref}-${field_k}`; const field_val = await kv.get(field_id); if (!trade_product_form_fields[field_k].validation.test(field_val)) return err_msg(field_k); diff --git a/src/routes/(app)/models/trade-product/+page.svelte b/src/routes/(app)/models/trade-product/+page.svelte @@ -1,8 +1,8 @@ <script lang="ts"> import { db } from "$lib/client"; - import { type LocationGcs, type TradeProduct } from "@radroots/models"; + import TradeProductListCard from "$lib/components/trade_product_list_card.svelte"; + import type { TradeProductBundle } from "$lib/types"; import { - app_layout, app_notify, LayoutTrellis, LayoutView, @@ -12,12 +12,8 @@ } from "@radroots/svelte-lib"; import { onMount } from "svelte"; - type LoadDataResult = { - trade_product: TradeProduct; - location_gcs?: LocationGcs; - }; type LoadData = { - results: LoadDataResult[]; + results: TradeProductBundle[]; }; let ld: LoadData | undefined = undefined; @@ -41,7 +37,7 @@ return; } - const results: LoadDataResult[] = []; + const results: TradeProductBundle[] = []; for (const trade_product of trade_products.results) { const location_gcs = await db.location_gcs_get({ list: [`on_trade_product`, { id: trade_product.id }], @@ -73,24 +69,11 @@ <LayoutView> <LayoutTrellis> {#each ld.results as li, li_i} - <div - class={`flex flex-col h-[22rem] w-${$app_layout} justify-start items-start bg-layer-1-surface round-44`} - > - <div - class={`flex flex-row h-[11rem] w-${$app_layout} justify-center items-center border-b-line border-b-layer-1-surface-edge`} - > - <p class={`font-sans font-[400] text-layer-0-glyph`}> - photos - </p> - </div> - <div - class={`flex flex-row h-[11rem] w-full justify-center items-center`} - > - <p class={`font-sans font-[400] text-layer-0-glyph`}> - body - </p> - </div> - </div> + <TradeProductListCard + basis={{ + result: li, + }} + /> {/each} </LayoutTrellis> </LayoutView> diff --git a/src/routes/(app)/models/trade-product/add/+page.svelte b/src/routes/(app)/models/trade-product/add/+page.svelte @@ -189,17 +189,14 @@ try { tradepr_key_sel = page_param.default.tradepr_key; tradepr_process_sel = `washed`; - - tradepr_price_amt_val = `1200.07`; - + tradepr_price_amt_val = `4.30`; tradepr_qty_tup_sel.set(`60-kg-bag`); - await kv_sync([ [fmt_id(`title`), `Green Coffee Beans`], - [fmt_id(`lot`), `mountain #1`], + [fmt_id(`lot`), `Ancestor slope`], [ fmt_id(`summary`), - `Good coffee, an amazing batch from our secret hillside with world leading terroir and tasting notes of honey.`, + `Good coffee, an amazing year, wonderful flavour, we love it!`, ], ]); } catch (e) { @@ -249,7 +246,6 @@ num_str(tradepr_parsed_quantity.mass), ); await kv.set(fmt_id(`qty_unit`), tradepr_parsed_quantity.mass_unit); - await kv.set(fmt_id(`qty_label`), tradepr_parsed_quantity.label); } }); @@ -435,10 +431,10 @@ if (!tradepr_photo_paths.length) { const confirm = await dialog.confirm({ message: `${`${$t(`icu.the_listing_will_be_created_without_a_*`, { value: `${$t(`common.photo`)}`.toLowerCase() })}`}. ${$t(`common.do_you_want_to_continue_q`)}`, - ok_label: `${$t(`icu.add_*`, { value: `${$t(`common.photo`)}` })}`, - cancel_label: `${$t(`common.continue`)}`, + cancel_label: `${$t(`icu.add_*`, { value: `${$t(`common.photo`)}` })}`, + ok_label: `${$t(`common.continue`)}`, }); - if (confirm) { + if (!confirm) { await el_focus( fmt_id(`image-upload-control`), async () => await handle_back(2), @@ -497,6 +493,10 @@ [`year`, year_curr()], ], }); + console.log( + JSON.stringify(trade_product_fields, null, 4), + `trade_product_fields`, + ); if (`err` in trade_product_fields) { await dialog.alert( `${$t(`icu.the_*_is_incomplete`, { value: `${$t(`models.trade_product.fields.${trade_product_fields.err}.label`)}`.toLowerCase() })}`, @@ -523,7 +523,7 @@ }, }); if (`pass` in trade_product_location_set) { - route(`/models/nostr-relay/view`); + route(`/models/trade-product`); return; } await dialog.alert(