import { useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { Cascader, FormField } from "@godxjp/ui/data-entry"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import type { TreeOptionProp } from "@godxjp/ui/props"; /** * Cascader — 階層パスピッカー(カラム式)。 * value は常に string[] (単一) または string[][] (複数)。 * 絶対に flat な string を渡さない。 * Composed only from real @godxjp/ui components. */ const EXPENSE_CATEGORIES = [ { value: "operating", label: "営業費用", children: [ { value: "selling", label: "販売費", children: [ { value: "advertising", label: "広告宣伝費" }, { value: "travel", label: "旅費交通費" }, { value: "entertainment", label: "交際費" }, ], }, { value: "admin", label: "一般管理費", children: [ { value: "salary", label: "給料手当" }, { value: "rent", label: "地代家賃" }, { value: "utilities", label: "水道光熱費" }, { value: "supplies", label: "消耗品費" }, ], }, ], }, { value: "non-operating", label: "営業外費用", children: [ { value: "interest", label: "支払利息" }, { value: "fx-loss", label: "為替差損" }, ], }, ]; const REGIONS = [ { value: "kanto", label: "関東", children: [ { value: "tokyo", label: "東京都", children: [ { value: "shinjuku", label: "新宿区" }, { value: "shibuya", label: "渋谷区" }, { value: "chiyoda", label: "千代田区" }, ], }, { value: "kanagawa", label: "神奈川県", children: [ { value: "yokohama", label: "横浜市" }, { value: "kawasaki", label: "川崎市" }, ], }, ], }, { value: "kansai", label: "関西", children: [ { value: "osaka", label: "大阪府", children: [ { value: "osaka-city", label: "大阪市" }, { value: "sakai", label: "堺市" }, ], }, { value: "kyoto", label: "京都府", children: [{ value: "kyoto-city", label: "京都市" }], }, ], }, ]; /** * 配送区分ツリー — ノード単位のフラグを実演: * - disabled: そのノード(および子)を選択不可(休止中の区分) * - isLeaf: 子を持たない葉として扱い、カスケード矢印を出さない * - disableCheckbox: TreeOption 共通フラグ(チェックボックスのみ無効化の意図を表す) */ const SHIPPING_TYPES: TreeOptionProp[] = [ { value: "domestic", label: "国内配送", children: [ { value: "standard", label: "通常便" }, { value: "express", label: "速達便", disableCheckbox: true }, { value: "cool", label: "クール便(休止中)", disabled: true }, ], }, { value: "international", label: "国際配送(準備中)", disabled: true, children: [{ value: "ems", label: "EMS" }], }, // isLeaf: 子なしの確定ノード(カスケードしない) { value: "pickup", label: "店頭受取", isLeaf: true }, ]; /** * fieldNames で実 API 形状(id / name / items)を value/label/children へ写像。 * 実データは value/label/children 以外のキーを使うことが多い。 */ const ORG_UNITS = [ { id: "sales", name: "営業本部", items: [ { id: "sales-east", name: "東日本営業部" }, { id: "sales-west", name: "西日本営業部" }, ], }, { id: "dev", name: "開発本部", items: [ { id: "dev-frontend", name: "フロントエンド部" }, { id: "dev-backend", name: "バックエンド部" }, ], }, ]; /** * Resolve a value-path (string[]) to its human labels by walking the option tree. * `value` carries machine codes; a readout must never show those — map to labels. * `keys` adapts to fieldNames-mapped data (e.g. id/name/items). */ function pathToLabels( options: readonly Record[], path: string[], keys = { value: "value", label: "label", children: "children" }, ): string[] { const labels: string[] = []; let level: readonly Record[] | undefined = options; for (const seg of path) { const node = level?.find((n) => n[keys.value] === seg); if (!node) break; labels.push(String(node[keys.label])); level = node[keys.children] as readonly Record[] | undefined; } return labels; } const ORG_KEYS = { value: "id", label: "name", children: "items" }; export default function Demo() { const [categoryPath, setCategoryPath] = useState([]); const [regionPath, setRegionPath] = useState([]); const [multiPaths, setMultiPaths] = useState([]); const [shippingPaths, setShippingPaths] = useState([]); const [orgPath, setOrgPath] = useState([]); const [hoverPath, setHoverPath] = useState([]); return ( 経費カテゴリ選択(単一・検索付き) 費用科目の階層パスを選択。showSearch でリーフまで全文検索できる。 選択パス:{" "} {categoryPath.length > 0 ? pathToLabels(EXPENSE_CATEGORIES, categoryPath).join(" / ") : "未選択"} setCategoryPath(v as string[])} showSearch placeholder="カテゴリを選択..." /> 地域選択(単一・changeOnSelect) changeOnSelect=true で中間ノードも確定値として選択可能。 選択パス:{" "} {regionPath.length > 0 ? pathToLabels(REGIONS, regionPath).join(" / ") : "未選択"} setRegionPath(v as string[])} changeOnSelect placeholder="地域を選択..." /> 複数経費カテゴリ(multiple) multiple=true で string[][] を管理。チェックボックスで複数パスを選択。 選択数:{" "} {multiPaths.length} パス setMultiPaths(v as string[][])} showSearch placeholder="カテゴリを選択..." /> 地域選択(expandTrigger="hover") expandTrigger="hover" で列のホバーだけで次の階層が展開(既定は "click")。 リーフはクリックで確定。 選択パス:{" "} {hoverPath.length > 0 ? pathToLabels(REGIONS, hoverPath).join(" / ") : "未選択"} setHoverPath(v as string[])} expandTrigger="hover" placeholder="地域を選択..." /> 配送区分(ノード単位フラグ・複数) ノードの disabled で「クール便」「国際配送」を選択不可(薄表示)に、isLeaf で「店頭受取」を 子なし確定ノードに。 選択数: {shippingPaths.length} パス setShippingPaths(v as string[][])} placeholder="配送区分を選択..." /> 組織選択(fieldNames でキー写像) 実 API 形状(id / name / items)を fieldNames={{ value: "id", label: "name", children: "items" }} で写像。 選択パス:{" "} {orgPath.length > 0 ? pathToLabels(ORG_UNITS, orgPath, ORG_KEYS).join(" / ") : "未選択"} setOrgPath(v as string[])} placeholder="組織を選択..." /> 非制御モード(defaultValue・allowClear={false}) defaultValue で初期パスを与え、状態を内部管理(非制御)。allowClear={false} で クリア(×)ボタンを無効化し、必須項目として常に値を保持。 無効状態(disabled・選択済み) disabled で操作不可。value を与えて「選択済みのまま無効化」も表現(編集権限なしの読み取り表示)。 無効時はクリア(×)も非表示。 ); }