NutritionManualEntryForm.tsx 8.85 KB
import React from "react";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import type { LabelElement } from "../../types/labelTemplate";
import { NUTRITION_FACTS_LAYOUT_ROWS } from "../../lib/nutritionFactsLayout";
import { listNutritionManualFieldSpecs } from "../../lib/nutritionManualEntry";

function pick(values: Record<string, string>, key: string): string {
  return String(values[key] ?? "");
}

type NutrientRowDef = {
  key: string;
  label: string;
  indent?: boolean;
};

function nutrientRowsForElement(el: LabelElement): NutrientRowDef[] {
  const cfg = (el.config ?? {}) as Record<string, unknown>;
  const fixed = Array.isArray(cfg.fixedNutrients)
    ? (cfg.fixedNutrients as Record<string, unknown>[])
    : [];
  const layoutByKey = new Map(NUTRITION_FACTS_LAYOUT_ROWS.map((r) => [r.key, r]));
  const rows: NutrientRowDef[] = NUTRITION_FACTS_LAYOUT_ROWS.map((r) => ({
    key: r.key,
    label: r.label,
    indent: r.indent,
  }));
  for (const row of fixed) {
    const key = String(row.key ?? "").trim();
    if (!key || layoutByKey.has(key)) {
      if (key && layoutByKey.has(key) && row.label) {
        const idx = rows.findIndex((x) => x.key === key);
        if (idx >= 0) rows[idx] = { ...rows[idx], label: String(row.label) };
      }
      continue;
    }
    rows.push({
      key,
      label: String(row.label ?? key),
      indent: false,
    });
  }
  return rows;
}

function extraNutrientIds(el: LabelElement): Array<{ id: string; name: string }> {
  const specs = listNutritionManualFieldSpecs(el);
  const out: Array<{ id: string; name: string }> = [];
  for (const s of specs) {
    if (!s.subKey.startsWith("extra:") || !s.subKey.endsWith(":value")) continue;
    const id = s.subKey.slice("extra:".length, -":value".length);
    if (out.some((x) => x.id === id)) continue;
    out.push({ id, name: s.columnLabel.replace(/ \(amount\)$/, "") });
  }
  return out;
}

/** 分组录入:顶部 Servings / Serve size / Calories + 每行「含量 + %DV」(< 前缀在模板编辑器配置) */
export function NutritionManualEntryForm({
  element,
  values,
  onFieldChange,
  className,
}: {
  element: LabelElement;
  values: Record<string, string>;
  onFieldChange: (subKey: string, next: string) => void;
  className?: string;
}) {
  const nutrientRows = nutrientRowsForElement(element);
  const extras = extraNutrientIds(element);

  return (
    <div className={className ?? "space-y-4"}>
      <div className="space-y-2">
        <div className="text-xs font-semibold text-gray-700">Top metrics</div>
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
          <FieldPair
            label="Servings"
            value={pick(values, "servingsPerContainer")}
            onChange={(v) => onFieldChange("servingsPerContainer", v)}
            placeholder="e.g. 6"
          />
          <FieldPair
            label="Serve size"
            value={pick(values, "servingSize")}
            onChange={(v) => onFieldChange("servingSize", v)}
            placeholder="e.g. 500g"
          />
        </div>
        <FieldPair
          label="Calories"
          value={pick(values, "calories")}
          onChange={(v) => onFieldChange("calories", v)}
          placeholder="e.g. 368"
        />
      </div>

      <div className="space-y-2">
        <div className="text-xs font-semibold text-gray-700">Nutrients</div>
        {nutrientRows.map((row) => (
          <NutrientInputRow
            key={row.key}
            label={row.label}
            indent={row.indent}
            amount={pick(values, row.key)}
            percent={pick(values, `${row.key}Percent`)}
            onAmount={(v) => onFieldChange(row.key, v)}
            onPercent={(v) => onFieldChange(`${row.key}Percent`, v)}
          />
        ))}
        {extras.map((ex) => (
          <NutrientInputRow
            key={ex.id}
            label={ex.name}
            amount={pick(values, `extra:${ex.id}:value`)}
            percent={pick(values, `extra:${ex.id}:percent`)}
            onAmount={(v) => onFieldChange(`extra:${ex.id}:value`, v)}
            onPercent={(v) => onFieldChange(`extra:${ex.id}:percent`, v)}
          />
        ))}
      </div>
    </div>
  );
}

/** 从 element config 读取当前预览/录入值(模板编辑器画布同步用) */
export function nutritionValuesFromElementConfig(cfg: Record<string, unknown>): Record<string, string> {
  const out: Record<string, string> = {
    servingsPerContainer: String(cfg.servingsPerContainer ?? cfg.ServingsPerContainer ?? ""),
    servingSize: String(cfg.servingSize ?? cfg.ServingSize ?? ""),
    calories: String(cfg.calories ?? cfg.Calories ?? ""),
  };
  const fixed = Array.isArray(cfg.fixedNutrients)
    ? (cfg.fixedNutrients as Record<string, unknown>[])
    : [];
  for (const row of fixed) {
    const key = String(row.key ?? "").trim();
    if (!key) continue;
    out[key] = String(row.value ?? cfg[key] ?? "");
    out[`${key}Percent`] = String(row.dailyValuePercent ?? row.percent ?? cfg[`${key}Percent`] ?? "");
  }
  for (const item of NUTRITION_FACTS_LAYOUT_ROWS) {
    if (out[item.key] === undefined) out[item.key] = String(cfg[item.key] ?? "");
    if (out[`${item.key}Percent`] === undefined) {
      out[`${item.key}Percent`] = String(cfg[`${item.key}Percent`] ?? "");
    }
  }
  const extras = Array.isArray(cfg.extraNutrients) ? cfg.extraNutrients : [];
  extras.forEach((item, idx) => {
    const row = item as Record<string, unknown>;
    const id = String(row.id ?? `extra-${idx}`);
    out[`extra:${id}:value`] = String(row.value ?? "");
    out[`extra:${id}:percent`] = String(cfg[`extra:${id}:percent`] ?? "");
  });
  return out;
}

/** 将分组录入写回 element config(模板编辑器预览;保存模板时仍会清空数值) */
export function applyNutritionValuesToElementConfig(
  baseCfg: Record<string, unknown>,
  values: Record<string, string>,
): Record<string, unknown> {
  const cfg: Record<string, unknown> = { ...baseCfg };
  cfg.servingsPerContainer = pick(values, "servingsPerContainer");
  cfg.servingSize = pick(values, "servingSize");
  cfg.calories = pick(values, "calories");

  const baseFixed = Array.isArray(baseCfg.fixedNutrients)
    ? (baseCfg.fixedNutrients as Record<string, unknown>[])
    : [];
  const keys =
    baseFixed.length > 0
      ? baseFixed.map((r) => String(r.key ?? "").trim()).filter(Boolean)
      : NUTRITION_FACTS_LAYOUT_ROWS.map((r) => r.key);

  const fixedArr = keys.map((key) => {
    const baseRow = baseFixed.find((r) => String(r.key ?? "").trim() === key);
    const layout = NUTRITION_FACTS_LAYOUT_ROWS.find((r) => r.key === key);
    const unit = String(baseRow?.unit ?? layout?.defaultUnit ?? "");
    const label = String(baseRow?.label ?? layout?.label ?? key);
    const value = pick(values, key);
    const dailyValuePercent = pick(values, `${key}Percent`);
    const lessThan = Boolean(baseRow?.lessThan ?? baseCfg[`${key}LessThan`]);
    if (value) cfg[key] = value;
    else delete cfg[key];
    delete cfg[`${key}Unit`];
    cfg[`${key}Percent`] = dailyValuePercent;
    cfg[`${key}LessThan`] = lessThan;
    return { key, label, value, unit, dailyValuePercent, lessThan };
  });
  cfg.fixedNutrients = fixedArr;

  const extras = Array.isArray(baseCfg.extraNutrients) ? [...(baseCfg.extraNutrients as object[])] : [];
  for (const ex of extras) {
    const row = ex as Record<string, unknown>;
    const id = String(row.id ?? "");
    if (!id) continue;
    row.value = pick(values, `extra:${id}:value`);
    cfg[`extra:${id}:percent`] = pick(values, `extra:${id}:percent`);
    cfg[`extra:${id}:lessThan`] = Boolean(baseCfg[`extra:${id}:lessThan`]);
  }
  cfg.extraNutrients = extras;
  return cfg;
}

function FieldPair({
  label,
  value,
  onChange,
  placeholder,
}: {
  label: string;
  value: string;
  onChange: (v: string) => void;
  placeholder?: string;
}) {
  return (
    <div className="space-y-1">
      <Label className="text-xs text-gray-600">{label}</Label>
      <Input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        className="h-8 text-sm"
      />
    </div>
  );
}

function NutrientInputRow({
  label,
  indent,
  amount,
  percent,
  onAmount,
  onPercent,
}: {
  label: string;
  indent?: boolean;
  amount: string;
  percent: string;
  onAmount: (v: string) => void;
  onPercent: (v: string) => void;
}) {
  return (
    <div className="grid grid-cols-[1fr_88px_72px] gap-1.5 items-center">
      <span className={`text-xs text-gray-700 truncate ${indent ? "pl-3" : ""}`}>{label}</span>
      <Input
        value={amount}
        onChange={(e) => onAmount(e.target.value)}
        placeholder="e.g. 11g"
        className="h-8 text-sm text-right"
      />
      <Input
        value={percent}
        onChange={(e) => onPercent(e.target.value)}
        placeholder="0%"
        className="h-8 text-sm text-right"
      />
    </div>
  );
}

export type { NutritionManualFieldSpec } from "../../lib/nutritionManualEntry";