【無料】障害支援区分シミュレーションツール!判定目安を自分でチェック(厚労省マニュアル準拠)

IT系知識

はじめに

さて今回は、障害支援区分のシミュレーションツールを作ったので展開します。

これを自分の環境に入れれば、簡易的ではあるものの、皆さん自由に障害支援区分のシミュレーションができます。

ITに慣れていない人は少し難しいかもしれませんが、できるだけ簡単に実行できるようにしていますので、ゆっくり順番に見てもらえればと思います。

できること

おそらくこの記事を見ている方は障害支援区分とは何か・・・は理解していると思いますので、そこに関する説明は省略します。

今回私が作ったものは、「認定調査項目(80 項目)」に回答していくと、障害支援区分のどれに当たる可能性が何%か、といったシミュレーションができるものです。

アプリのイメージは以下のような感じです。

どの区分となるかは結構重要な要素です。最終的には医師も含めて一次判定が下されるわけですが、そのための指標として簡易的にシミュレーションをしておくことで意図した区分を勝ち取ることができると思います。

もちろん正当な評価が必要なわけですが、正当に評価させるためにも、この質問はこれを回答すればこの区分になる、それを裏付けるための根拠を持って医師と会話することで、最終的には正しい評価がもらえるはずです。必ず役に立つと思いますので是非活用していきましょう。

前提・注意事項

1. 根拠とした資料

今回このシミュレーションツールを作るために参考とした資料は、厚生労働省が出している障害支援区分の「認定調査員マニュアル」です。

以下リンクの”3 マニュアル”欄からpdfファイルを閲覧・ダウンロードできます。

よく分からないリンクを踏みたくない・・・という人であれば、ご自身で「厚生労働省 障害支援区分」と調べてみてください。おそらく同じページにたどり着くはずです。

2. 質問項目について

今回このツールでは、上記マニュアルの中にも記載されている(P.39~P.86)「認定調査項目(80 項目)」から区分をシミュレーションできるようにしています。

ただ、マニュアルのP.3にも書かれている通り、実際には「認定調査項目(80 項目)」だけで決まるものではありません。最終的には「医師意見書(24項目)」も含めて一次判定がなされ、さらには二次判定も踏まえて区分が決まるわけです。

ですので、あくまでも、簡易的なシミュレーションということだけご理解いただければと思います。全く役に立たないものではないはずですが、参考程度の情報として考えてください。

3. 算出ロジックについて

算出ロジックは、上記マニュアルのP.11~P.32に記載されている表を参考にしています

ただ、こちらもマニュアルのP.9に書かれている通り、実際には過去の認定データなども含めて複雑な計算がされています。

あくまでも今回のシミュレーションツールの算出ロジックの一部にしか過ぎないということをご認識いただきたいです。マニュアルに記載されている表だけで算出できないロジックは、生成AIにマニュアルを読み込ませて適当にロジックを組ませた内容です。

事前準備(最初の1回でOK)

さてでは早速ツールを使っていきましょう。

とは言えまずはツールを使うための事前準備が必要です。

この準備は最初の1回だけでOKです。一度準備をしてしまえば、次使うときは不要なので最初だけ頑張りましょう。

1. VS Codeのインストール

まずは「VS Code」と呼ばれるツールをインストールしましょう。以下は私が以前インストール手順をまとめた記事ですので、こちらを見れば簡単にインストールができるはずです。

これはプログラムを実行するための環境(正確には違いますが今回はITに詳しくない方に向けて、分かりやすくそのように表現しています。)です。実際にアプリを起動したり、終了したりできます。もちろんソースコードを書き換えたりもできますね。

怪しいツールではないのでご安心ください。Microsoftが出しているツールで、ITに関わっている人であれば誰でも一度は触ったことのある超有名ツールです。それでも良く分からないツールを入れたくないという人であれば、「VS Code 怪しい 危険」とか調べてみてください。ツール自体が危険なものとは一切出てこないはずです。

2. Node.jsのインストール

次は、「Node.js」をインストールしましょう。

こちらも、以前私がまとめた記事がありますので、そちらからインストールを試してみてください。

こちらもプログラムを実行するために必要なものです。この説明だけだとVS Codeとの違いが分からないと思いますが、今回はそこまで意識しなくていいので、まぁ必要なツールがあるんだな程度で入れておいてください。

3. デスクトップに適当なフォルダを作成しVS Codeで開く

ここまでインストールできたら、適当なフォルダを作ってVS Codeで立ち上げましょう

フォルダはどこに作ってもOKです。悩んだらとりあえずデスクトップにでも作っておいてください。デスクトップ上で右クリック > 新規作成で作れると思いますので試してみてください。フォルダ名は何でもいいですが、「障害支援区分」とか適当に分かる名前を付けておきましょう。

フォルダが作成できたら、VS Codeを立ち上げて、作成したフォルダを開きましょう

VS Codeの立ち上げ方が分からない方は、PC画面下部にあるアイコンが並んでいる箇所で「VS Code」と検索し、”Visual Studio Code”を選択してください。

フォルダの開き方は、VS Code左上の「ファイル」から「フォルダーを開く」を押下し、作成したフォルダを選択してください。

4. 各種ライブラリのインストール

次はライブラリと呼ばれるものをインストールする必要があります。

ライブラリが何なのかは覚えなくて良いです。まぁこれもプログラムを動かすために必要なものだと理解してもらえればOKです。

ライブラリをインストールするためには「VS Code」上でコマンドラインと呼ばれるものを開く必要があります。「VS Code」上で「Ctrl」+「@」を押下すると画像のようなものが開くはずなので試してみてください。

コマンドラインが開けたら、以下コマンド①~⑤を順番にコピペして入力し、「Enter」で実行していってください。

途中で何か聞かれたら「y」など入力して「Enter」で大丈夫です。(「Need to install the following packages: tailwindcss@x.x.x OK? (y)」的なことが聞かれるかもしれません。インストールしても良いですか?良ければ「y」を押してね的な感じのことが言われています。)

実行が終わったら次のコマンドが打てるようになるので、終わるまで気長に待ちましょう。たぶん数秒で終わると思いますが。

  • コマンド①
npm create vite@latest disability-simulator -- --template react
  • コマンド②
cd disability-simulator
  • コマンド③
npm install
  • コマンド④
npm install -D tailwindcss postcss autoprefixer
  • コマンド⑤
npm install -D @tailwindcss/postcss

最初に「npm ○○」を実行した際、エラーが発生したら以下を試してみてください。内容にもよるはずですが、基本的には以下の実行で問題なく解決できると思っています!

実行した際に何かしら許可を求められたら、「Y」を押下してください。

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

5. 「tailwind.config.js」と「postcss.config.js」を作成

「tailwind.config.js」と「postcss.config.js」の2ファイルを作成します。それぞれ新規作成でファイルを作成し、下記内容をコピペしてください。

ファイルを作る場所は「package.json」がある場所と同じところです。「disability-simulator」の中ですね。

新しくファイルを作成する際は、画像で示したように「新しいファイル」のアイコンか、もしくは「右クリック」>「新しいファイル」で作成しましょう。

tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

postcss.config.js

export default {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
}

6. ソースコードを作成

これにてインストール系はもろもろ完了です。

次に、ソースコード(プログラムコード)を作成しましょう。ソースコードは「src」フォルダの下に作成してください。ファイル作成の方法は先述した通りです。ファイルは全部で5つありますので、忘れずに全て作成してください。

以下ソースコードを全てコピペして作成してください。※既に同じ名前のファイルがある場合は、それを開いて中身を全て削除した後、以下のソースコードをコピペしてください。

App.jsx

import React, { useState, useMemo, useEffect } from 'react';
import { surveyGroups } from './surveyData';
import { evaluateClassification } from './logicEngine';

const classifications = ["非該当", "区分1", "区分2", "区分3", "区分4", "区分5", "区分6"];
const colors = [
  "from-slate-400 to-slate-500",  
  "from-blue-300 to-blue-400",   
  "from-blue-500 to-blue-600",   
  "from-teal-400 to-teal-500",  
  "from-amber-400 to-amber-500", 
  "from-orange-500 to-orange-600", 
  "from-rose-500 to-rose-600"     
];

export default function App() {
  const [answers, setAnswers] = useState({});
  const [result, setResult] = useState(null);
  const [currentStep, setCurrentStep] = useState(0);
  const [showResult, setShowResult] = useState(false);
  const [showScrollTop, setShowScrollTop] = useState(false); // トップに戻るボタンの表示状態

  const totalItems = useMemo(() => surveyGroups.reduce((acc, group) => acc + group.items.length, 0), []);
  const answeredCount = Object.keys(answers).length;
  const progressPercent = Math.round((answeredCount / totalItems) * 100);

  // スクロール位置を監視して「トップに戻る」ボタンの表示/非表示を切り替える
  useEffect(() => {
    const handleScroll = () => {
      if (window.scrollY > 300) {
        setShowScrollTop(true);
      } else {
        setShowScrollTop(false);
      }
    };
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  const handleOptionChange = (id, value) => {
    setAnswers(prev => ({ ...prev, [id]: value }));
  };

  const handleCalculate = () => {
    if (answeredCount < totalItems) {
      if (!window.confirm(`未回答の項目が ${totalItems - answeredCount} 個あります。\n未回答項目は「支援不要/ない」として計算しますか?`)) {
        return;
      }
    }
    try {
      const calcResult = evaluateClassification(answers);
      setResult(calcResult);
      setShowResult(true);
      window.scrollTo({ top: 0, behavior: 'smooth' });
    } catch (error) {
      console.error("エラー:", error);
      alert("計算処理中にエラーが発生しました。");
    }
  };

  const resetForm = () => {
    if (window.confirm("すべての入力をリセットしますか?")) {
      setAnswers({});
      setResult(null);
      setShowResult(false);
      setCurrentStep(0);
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  };

  const scrollToTop = () => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  // --- SVG Icons ---
  const CheckIcon = () => (
    <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
    </svg>
  );

  const ChevronRight = () => (
    <svg className="w-5 h-5 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
    </svg>
  );

  const ChevronLeft = () => (
    <svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
    </svg>
  );

  const ArrowUpIcon = () => (
    <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 15l7-7 7 7" />
    </svg>
  );

  const currentGroup = surveyGroups[currentStep];

  // ==========================================
  // 結果ダッシュボード画面
  // ==========================================
  if (showResult && result) {
    return (
      <div className="min-h-screen bg-slate-50 text-slate-800 font-sans py-12 px-4 animate-fade-in-down">
        <div className="max-w-3xl mx-auto">
          <button 
            onClick={() => setShowResult(false)}
            className="flex items-center text-slate-500 hover:text-indigo-600 font-semibold mb-6 transition-colors"
          >
            <ChevronLeft /> 回答を修正する
          </button>
          
          <div className="bg-white rounded-3xl shadow-2xl overflow-hidden border border-slate-100">
            <div className="bg-gradient-to-r from-indigo-600 to-violet-600 p-8 text-white text-center relative overflow-hidden">
              <div className="absolute top-0 right-0 -mt-10 -mr-10 w-40 h-40 bg-white opacity-10 rounded-full blur-2xl"></div>
              <h2 className="text-sm font-bold tracking-widest text-indigo-100 mb-2 uppercase">Simulation Result</h2>
              <h1 className="text-3xl font-extrabold mb-4">判定シミュレーション結果</h1>
              <div className="inline-block bg-white/20 backdrop-blur-md border border-white/30 px-5 py-2 rounded-full font-mono text-sm font-bold shadow-sm">
                到達ノード: No.{result.node}
              </div>
            </div>

            <div className="p-8 md:p-10">
              {result.node === "Unknown" ? (
                <div className="bg-rose-50 border border-rose-200 text-rose-700 p-6 rounded-2xl font-medium text-center shadow-sm">
                  入力されたパターンは、現在のシステムルールに合致しませんでした。
                </div>
              ) : (
                <div className="space-y-6">
                  {result.probabilities.map((prob, index) => (
                    <div key={index} className="group">
                      <div className="flex justify-between items-end mb-2">
                        <span className="text-sm font-bold text-slate-500 tracking-wide">
                          {classifications[index]}
                        </span>
                        <span className="font-mono text-xl font-bold text-slate-800">
                          {prob.toFixed(1)}%
                        </span>
                      </div>
                      <div className="w-full bg-slate-100 rounded-full h-4 overflow-hidden shadow-inner">
                        <div 
                          className={`h-full bg-gradient-to-r ${colors[index]} transition-all duration-1000 ease-out relative`}
                          style={{ width: `${prob}%` }}
                        >
                          <div className="absolute inset-0 bg-white/20 w-full h-full transform -skew-x-12 translate-x-full group-hover:animate-[shimmer_1.5s_infinite]"></div>
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>

            <div className="bg-slate-50 p-6 text-center border-t border-slate-100">
              <button 
                onClick={resetForm}
                className="text-slate-500 hover:text-rose-600 font-bold transition-colors text-sm"
              >
                すべてのデータをリセットして最初からやり直す
              </button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ==========================================
  // 調査入力ウィザード画面
  // ==========================================
  return (
    <div className="min-h-screen bg-slate-50/50 text-slate-800 font-sans pb-32 relative">
      
      {/* トップへ戻るフローティングボタン */}
      <button
        onClick={scrollToTop}
        className={`fixed bottom-8 right-8 p-4 bg-slate-800 text-white rounded-full shadow-2xl transition-all duration-300 z-50 hover:bg-slate-700 hover:scale-110 focus:outline-none focus:ring-4 focus:ring-slate-300 ${
          showScrollTop ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
        }`}
        aria-label="ページ上部に戻る"
      >
        <ArrowUpIcon />
      </button>

      {/* 画面上部固定:ヘッダー&プログレスバー */}
      <div className="sticky top-0 z-40 bg-white/80 backdrop-blur-xl border-b border-slate-200 shadow-sm">
        <div className="max-w-5xl mx-auto px-4 py-4">
          <div className="flex items-center justify-between mb-3">
            <h1 className="text-xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-violet-600 tracking-tight">
              障害支援区分 シミュレーター
            </h1>
            <div className="text-sm font-bold text-slate-500 bg-slate-100 px-3 py-1 rounded-full">
              {answeredCount} / {totalItems} 回答済
            </div>
          </div>
          <div className="w-full bg-slate-100 rounded-full h-2.5 overflow-hidden">
            <div 
              className="bg-gradient-to-r from-indigo-500 to-violet-500 h-2.5 rounded-full transition-all duration-500 ease-out" 
              style={{ width: `${progressPercent}%` }}
            ></div>
          </div>
        </div>
      </div>

      <div className="max-w-4xl mx-auto px-4 py-8 animate-fade-in-down">
        
        {/* ステップ(群)ナビゲーション & 計算ボタンを隣接配置 */}
        <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-6 border-b border-slate-200">
          
          {/* タブエリア */}
          <div className="flex space-x-2 overflow-x-auto scrollbar-hide pb-2 md:pb-0 w-full md:w-auto">
            {surveyGroups.map((group, idx) => (
              <button
                key={idx}
                onClick={() => setCurrentStep(idx)}
                className={`whitespace-nowrap px-5 py-2.5 rounded-full text-sm font-bold transition-all duration-200 ${
                  currentStep === idx 
                    ? 'bg-slate-800 text-white shadow-md transform scale-105' 
                    : 'bg-white text-slate-500 border border-slate-200 hover:bg-slate-100 hover:text-slate-700'
                }`}
              >
                群 {idx + 1}
              </button>
            ))}
          </div>

          {/* 計算ボタンを上部に配置 */}
          <button
            onClick={handleCalculate}
            className="flex-shrink-0 w-full md:w-auto flex justify-center items-center px-8 py-3 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 text-white rounded-full font-extrabold text-sm shadow-lg shadow-indigo-200 transition-all transform hover:scale-105"
          >
            ✨ 選択した内容で区分をシミュレーション
          </button>
        </div>

        {/* メイン質問エリア */}
        <div className="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
          <div className="bg-slate-800 p-6 md:px-10 text-white">
            <h2 className="text-2xl md:text-3xl font-extrabold tracking-tight">
              {currentGroup.title}
            </h2>
          </div>
          
          <div className="p-6 md:p-10 divide-y divide-slate-100">
            {currentGroup.items.map((item, index) => (
              <div key={item.id} className={`py-8 ${index === 0 ? 'pt-0' : ''} ${index === currentGroup.items.length - 1 ? 'pb-0 border-b-0' : ''}`}>
                <div className="flex items-start mb-5">
                  <span className="flex-shrink-0 bg-indigo-100 text-indigo-700 font-mono font-bold px-3 py-1 rounded-lg text-sm mr-4 mt-0.5">
                    {item.id}
                  </span>
                  <h3 className="text-xl font-bold text-slate-800 leading-snug">
                    {item.question}
                  </h3>
                </div>
                
                <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
                  {item.options.map(opt => {
                    const isSelected = answers[item.id] === opt.value;
                    return (
                      <label 
                        key={opt.value} 
                        className={`relative flex flex-col p-5 rounded-2xl cursor-pointer transition-all duration-200 border-2 ${
                          isSelected 
                            ? 'border-indigo-500 bg-indigo-50/50 shadow-md ring-1 ring-indigo-500 transform scale-[1.02]' 
                            : 'border-slate-200 bg-white hover:border-indigo-300 hover:bg-slate-50'
                        }`}
                      >
                        <input 
                          type="radio" 
                          name={`item-${item.id}`} 
                          value={opt.value}
                          checked={isSelected}
                          onChange={() => handleOptionChange(item.id, opt.value)}
                          className="sr-only" 
                        />
                        <div className="flex items-center justify-between mb-3">
                          <span className={`text-base font-bold pr-4 ${isSelected ? 'text-indigo-800' : 'text-slate-700'}`}>
                            {opt.label}
                          </span>
                          <div className={`flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center transition-colors ${
                            isSelected ? 'bg-indigo-600' : 'bg-slate-200'
                          }`}>
                            {isSelected && <CheckIcon />}
                          </div>
                        </div>
                        <p className={`text-sm leading-relaxed font-medium ${isSelected ? 'text-indigo-600/80' : 'text-slate-500'}`}>
                          {opt.desc}
                        </p>
                      </label>
                    );
                  })}
                </div>
              </div>
            ))}
          </div>
        </div>

        {/* ボトムナビゲーション(前へ / 次へ) */}
        <div className="mt-8 flex flex-col sm:flex-row items-center justify-between gap-4">
          <button
            onClick={() => {
              setCurrentStep(prev => prev - 1);
              window.scrollTo({ top: 0, behavior: 'smooth' });
            }}
            disabled={currentStep === 0}
            className={`flex items-center px-6 py-3 rounded-full font-bold transition-all ${
              currentStep === 0 
                ? 'opacity-0 pointer-events-none' 
                : 'text-slate-500 bg-white border border-slate-200 hover:bg-slate-50 shadow-sm'
            }`}
          >
            <ChevronLeft /> 前の群へ戻る
          </button>

          {currentStep < surveyGroups.length - 1 ? (
            <button
              onClick={() => {
                setCurrentStep(prev => prev + 1);
                window.scrollTo({ top: 0, behavior: 'smooth' });
              }}
              className="w-full sm:w-auto flex justify-center items-center px-8 py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-full font-bold shadow-lg transition-transform hover:-translate-y-0.5"
            >
              次の群へ進む <ChevronRight />
            </button>
          ) : (
            /* 第5群の最後は計算ボタンが上部にあることを促すメッセージのみ配置 */
            <div className="text-slate-500 font-bold text-sm bg-slate-100 px-6 py-3 rounded-full">
              ✨ 全ての選択肢が終わりました。上部のボタンから計算を実行してください。
            </div>
          )}
        </div>

      </div>
    </div>
  );
}

surveyData.js

// 第4群の共通選択肢
const optionsGroup4 = [
  { value: 1, label: "1 支援が不要", desc: "〇行動が現れる可能性がほとんどない場合。" },
  { value: 2, label: "2 希に支援が必要", desc: "〇可能性はあるが、調査日前1か月間には一度も現れていない場合。" },
  { value: 3, label: "3 月に1回以上の支援が必要", desc: "〇調査日前1か月間に1回以上現れている場合。〇投薬や配慮がないと月に1回程度生じると考えられる場合。" },
  { value: 4, label: "4 週に1回以上の支援が必要", desc: "〇毎週1回以上現れている、または2回以上ある週が2週以上ある場合。" },
  { value: 5, label: "5 ほぼ毎日(週に5日以上の)支援が必要", desc: "〇ほぼ毎日(週5日以上)現れている、または5日以上ある週が2週以上ある場合。〇長期の引きこもり等で支援が必要な場合。" }
];

// 第5群の共通選択肢
const optionsGroup5 = [
  { value: 1, label: "1 ない", desc: "〇調査日前14日間に当該医療行為が一度も実施されていない場合。" },
  { value: 2, label: "2 ある", desc: "〇調査日前14日間に当該医療行為が一度以上実施されている場合。〇針を留置している等、いつでも医療行為ができる体制がとられている場合。" }
];

export const surveyGroups = [
  {
    title: "第1群:移動や動作等に関連する項目",
    items: [
      {
        id: "1-1", question: "寝返り",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で(何ら助力を得ずに)寝返りができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、身体に触れない見守りや声かけ等の支援が必要な場合。〇柵、ひも、サイドレール等の補助具をつかまれば自力でできる場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体を支える等、身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇一定の体位のみしかとれず、寝返りができない場合。" }
        ]
      },
      {
        id: "1-2", question: "起き上がり",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で起き上がりができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇柵、ひも、サイドレール等の補助具をつかまれば自力でできる場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。" }
        ]
      },
      {
        id: "1-3", question: "座位保持",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で座位保持ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇背もたれがない場合、自分の手で支える必要がある場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。〇背もたれや補助具、または他人の手で支えないと座位が保持できない場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇装置等で常に両側面や前面から支える必要がある場合。〇座位をとることができない場合。" }
        ]
      },
      {
        id: "1-4", question: "移乗",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で移乗ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇一連の動作に合わせて、車いすを差し入れる等の支援が行われている場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇寝たきり等によりできない場合。" }
        ]
      },
      {
        id: "1-5", question: "立ち上がり",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で立ち上がりができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇柵、手すり等の補助具をつかまれば自力でできる場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。" }
        ]
      },
      {
        id: "1-6", question: "両足での立位保持",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で両足での立位保持ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇手すり、壁、杖等の補助具をつかまれば自力でできる場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇支援があっても保持できない、または立位がとれない場合。" }
        ]
      },
      {
        id: "1-7", question: "片足での立位保持",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で片足での立位保持ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇手すり、壁等の補助具をつかまれば自力でできる場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇支援があっても保持できない、または立位がとれない場合。" }
        ]
      },
      {
        id: "1-8", question: "歩行",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で歩行ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇手すり、杖、歩行器等の補助具を使用すれば自力でできる場合。〇視覚障害等により、方向確認の見守り等が必要な場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体を支える等、身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇歩行ができないため車いすを使用している、または医療上の制限がある場合。" }
        ]
      },
      {
        id: "1-9", question: "移動",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で移動ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇筋力低下、麻痺、心疾患等により、頻繁な休憩や見守りが必要な場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇段差等で車いすを押す、身体を支える等の身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。〇転倒防止のため常に手をつなぐ、腕を組む等の付き添いが必要な場合。" }
        ]
      },
      {
        id: "1-10", question: "衣服の着脱",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で衣服の着脱ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ等が必要な場合。〇季節や状況に合った衣服の準備、手渡し、促しが必要な場合。〇衣服の前後左右や裏表の確認ができない場合。" },
          { value: 3, label: "3 部分的な支援が必要", desc: "〇身体に触れる支援が部分的に必要な場合。" },
          { value: 4, label: "4 全面的な支援が必要", desc: "〇身体に触れる支援が全面的に必要な場合。" }
        ]
      },
      {
        id: "1-11", question: "じょくそう",
        options: [
          { value: 1, label: "1 ない", desc: "〇じょくそう(床ずれ)がない場合。" },
          { value: 2, label: "2 ある", desc: "〇じょくそうがある場合。〇じょくそう予防のための支援(除圧等)や、処置が行われている場合。" }
        ]
      },
      {
        id: "1-12", question: "えん下",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力でえん下(飲み込み)ができる場合。" },
          { value: 2, label: "2 見守り等の支援が必要", desc: "〇自力でできるが、見守りや声かけ、またはむせ込みに対する配慮が必要な場合。〇「不要」と「全面的」のいずれにも該当しない場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇えん下ができないため、経管栄養や中心静脈栄養等が行われている場合。" }
        ]
      }
    ]
  },
  {
    title: "第2群:身の回りの世話や日常生活等に関連する項目",
    items: [
      {
        id: "2-1", question: "食事",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇一連の行為(準備から片付けまで)を自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りや声かけが必要な場合。〇一部の行為(配膳、きざみ、とろみ等)に支援が必要な場合。〇経管栄養を自ら管理している、または一部介助を受けている場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇身体的支援が全面的に必要な場合。〇目的を理解していない場合。〇経管栄養の注入について全面的な支援を受けている場合。" }
        ]
      },
      {
        id: "2-2", question: "口腔清潔",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇一連の行為を自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇みがき残しを部分的にやり直す、または見守りが必要な場合。〇準備や後片付けに支援が必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇支援者が全面的にやり直している、または全てを自力で行えない場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-3", question: "入浴",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇一連の行為を自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇身体を洗う・拭く行為を部分的にやり直す、または見守りが必要な場合。〇準備や後片付けに支援が必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇身体を洗う・拭く行為を全面的にやり直している、または全てを自力で行えない場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-4", question: "排尿",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇一連の行為(尿意から後始末まで)を自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇清拭を部分的にやり直している場合。〇おむつ、パット、尿器、カテーテル等の補助具を使用し、一部支援を受けている場合。〇見守りが必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇清拭を全面的にやり直している、または全てを自力で行えない場合。〇おむつ等を使用し全面的支援を受けている場合。〇人工透析や間欠導尿を行っている場合。〇目的の不理解。" }
        ]
      },
      {
        id: "2-5", question: "排便",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇一連の行為を自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇清拭を部分的にやり直している場合。〇ストマ、おむつ等の補助具を使用し、一部支援を受けている場合。〇見守りが必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇清拭を全面的にやり直している、または全てを自力で行えない場合。〇おむつ等を使用し全面的支援を受けている場合。〇浣腸や摘便を頻繁に行っている場合。〇目的の不理解。" }
        ]
      },
      {
        id: "2-6", question: "健康・栄養管理",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自ら管理や受診の判断・実施が行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りや声かけが必要な場合。〇一部の判断や実施(受診の付き添い、記録等)を他者に委ねる場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての判断や実施を他者に委ねている場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-7", question: "薬の管理",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇用意から服薬確認までを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りや声かけ、または一部(取り出し、セット等)に支援が必要な場合。〇経管栄養チューブ等への注入に一部支援を受けている場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全てを自力で行えない、または全面的に管理されている場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-8", question: "金銭の管理",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇所持金の把握から支払いまでを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りが必要な場合。〇一部の管理(通帳の保管、高額な支払いの代行等)を他者に委ねる場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての管理を他者に委ねている場合。〇目的や金銭の価値を理解していない場合。" }
        ]
      },
      {
        id: "2-9", question: "電話等の利用",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇操作や受け答えを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りが必要な場合。〇一部の操作や意思疎通の補助(番号の入力代行等)が必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全てを自力で行えない場合。〇目的や利用方法を理解していない場合。" }
        ]
      },
      {
        id: "2-10", question: "日常の意思決定",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇毎日の希望や選択を自ら判断できる場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇判断に迷う際の見守りや声かけが必要な場合。〇一部の判断を他者に委ねる場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての判断を他者に委ねている場合。〇内容を理解していない場合。" }
        ]
      },
      {
        id: "2-11", question: "危険の認識",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇危険や異常を自ら認識し、回避行動がとれる場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇認識はできるが、安全な行動のための見守りや声かけが必要な場合。〇一部の認識が不十分な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての危険を自ら認識できない場合。〇危険の概念や目的を理解していない場合。" }
        ]
      },
      {
        id: "2-12", question: "調理",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇献立から後片付けまでを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇一部の行為(食材を切る、火の管理等)に支援が必要な場合。〇見守りが必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての工程を他者に委ねている、または自力で行えない場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-13", question: "掃除",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で掃除を行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇掃除が不十分で部分的にやり直す、または見守りが必要な場合。〇一部の重いゴミ出し等に支援が必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての掃除を他者に委ねている、または自力で行えない場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-14", question: "洗濯",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇干す、たたむ、収納までの全てを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇一部の行為(重い洗濯物の運搬、干す等)に支援が必要な場合。〇見守りが必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての洗濯を他者に委ねている、または自力で行えない場合。〇目的を理解していない場合。" }
        ]
      },
      {
        id: "2-15", question: "買い物",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇品物の選択から支払いまでを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りや声かけが必要な場合。〇一部(支払い、荷物の持ち運び等)に支援が必要な場合。〇特定の店舗(なじみの店等)でのみ可能な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての買い物を他者に委ねている場合。〇目的や価値を理解していない場合。" }
        ]
      },
      {
        id: "2-16", question: "交通手段の利用",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇切符購入から乗り降り、経路確認までを自力で行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇見守りや声かけが必要な場合。〇一部(切符購入、段差の補助等)に支援が必要な場合。〇特定の路線や経路でのみ可能な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇全ての利用を他者に委ねている場合。〇目的や利用方法を理解していない場合。" }
        ]
      }
    ]
  },
  {
    title: "第3群:意思疎通等に関連する項目",
    items: [
      {
        id: "3-1", question: "視力",
        options: [
          { value: 1, label: "1 日常生活に支障がない", desc: "〇新聞等の文字が普通に見える場合。" },
          { value: 2, label: "2 約1m離れた視力確認表の図が見える", desc: "〇文字は見えないが、1m離れた図が判別できる場合。" },
          { value: 3, label: "3 目の前に置いた視力確認表の図が見える", desc: "〇1m先は見えないが、目の前なら図が判別できる場合。" },
          { value: 4, label: "4 ほとんど見えていない", desc: "〇図が判別できず、わずかに光や動きを感じる程度の場合。" },
          { value: 5, label: "5 全く見えない", desc: "〇光も全く感じない場合。" },
          { value: 6, label: "6 見えているのか判断不能", desc: "〇意思疎通が困難等の理由により、状況が全く判断できない場合。" }
        ]
      },
      {
        id: "3-2", question: "聴力",
        options: [
          { value: 1, label: "1 日常生活に支障がない", desc: "〇普通の声で会話に支障がない場合。" },
          { value: 2, label: "2 普通の声がやっと聞き取れる", desc: "〇普通の声では聞き取りにくく、聞き間違いがある場合。" },
          { value: 3, label: "3 かなり大きな声なら何とか聞き取れる", desc: "〇耳元で大声を出す、または補聴器を使用しやっと聞こえる場合。" },
          { value: 4, label: "4 ほとんど聞えない", desc: "〇大声もほとんど聞こえない場合。" },
          { value: 5, label: "5 全く聞えない", desc: "〇音が全く聞こえない場合。" },
          { value: 6, label: "6 聞えているのか判断不能", desc: "〇意思疎通が困難等の理由により、状況が全く判断できない場合。" }
        ]
      },
      {
        id: "3-3", question: "コミュニケーション",
        options: [
          { value: 1, label: "1 日常生活に支障がない", desc: "〇意思疎通に支障がない場合。" },
          { value: 2, label: "2 特定の者であればコミュニケーションできる", desc: "〇家族やなじみの支援者であれば意思が伝わる場合。" },
          { value: 3, label: "3 会話以外の方法であればコミュニケーションできる", desc: "〇手話、筆談、写真・絵カード、装置等で可能な場合。" },
          { value: 4, label: "4 独自の方法でコミュニケーションできる", desc: "〇独特の身振り、表情、瞬き、指点字等で可能な場合。" },
          { value: 5, label: "5 コミュニケーションできない", desc: "〇どのような方法を用いても意思疎通が不可能な場合。" }
        ]
      },
      {
        id: "3-4", question: "説明の理解",
        options: [
          { value: 1, label: "1 理解できる", desc: "〇他者の説明に対し、内容に合った適切な反応が示せる場合。" },
          { value: 2, label: "2 理解できない", desc: "〇説明を理解できず、内容に合った反応が示せない場合。" },
          { value: 3, label: "3 理解できているか判断できない", desc: "〇反応が乏しい等の理由により、理解の有無が判断できない場合。" }
        ]
      },
      {
        id: "3-5", question: "読み書き",
        options: [
          { value: 1, label: "1 支援が不要", desc: "〇自力で読み書きが行える場合。" },
          { value: 2, label: "2 部分的な支援が必要", desc: "〇一部(複雑な内容、代筆等)に支援が必要な場合。〇見守りや声かけが必要な場合。" },
          { value: 3, label: "3 全面的な支援が必要", desc: "〇読み書きが全く行えない、または全面的に代行されている場合。〇知的障害や学習障害等による困難がある場合。" }
        ]
      },
      {
        id: "3-6", question: "感覚過敏・感覚鈍麻",
        options: [
          { value: 1, label: "1 ない", desc: "〇過敏や鈍麻の症状が認められない場合。" },
          { value: 2, label: "2 ある", desc: "〇触覚、視覚、聴覚、痛覚等の過敏や鈍麻により、生活上の困難がある場合。" }
        ]
      }
    ]
  },
  {
    title: "第4群:行動障害に関連する項目",
    items: [
      { id: "4-1", question: "被害的・拒否的", options: optionsGroup4 },
      { id: "4-2", question: "作話", options: optionsGroup4 },
      { id: "4-3", question: "感情が不安定", options: optionsGroup4 },
      { id: "4-4", question: "昼夜逆転", options: optionsGroup4 },
      { id: "4-5", question: "暴言暴行", options: optionsGroup4 },
      { id: "4-6", question: "同じ話をする", options: optionsGroup4 },
      { id: "4-7", question: "大声・奇声を出す", options: optionsGroup4 },
      { id: "4-8", question: "支援の拒否", options: optionsGroup4 },
      { id: "4-9", question: "徘徊", options: optionsGroup4 },
      { id: "4-10", question: "落ち着きがない", options: optionsGroup4 },
      { id: "4-11", question: "外出して戻れない", options: optionsGroup4 },
      { id: "4-12", question: "1人で出たがる", options: optionsGroup4 },
      { id: "4-13", question: "収集癖", options: optionsGroup4 },
      { id: "4-14", question: "物や衣類を壊す", options: optionsGroup4 },
      { id: "4-15", question: "不潔行為", options: optionsGroup4 },
      { id: "4-16", question: "異食行動", options: optionsGroup4 },
      { id: "4-17", question: "ひどい物忘れ", options: optionsGroup4 },
      { id: "4-18", question: "こだわり", options: optionsGroup4 },
      { id: "4-19", question: "多動・行動停止", options: optionsGroup4 },
      { id: "4-20", question: "不安定な行動", options: optionsGroup4 },
      { id: "4-21", question: "自らを傷つける行為", options: optionsGroup4 },
      { id: "4-22", question: "他人を傷つける行為", options: optionsGroup4 },
      { id: "4-23", question: "不適切な行為", options: optionsGroup4 },
      { id: "4-24", question: "突発的な行動", options: optionsGroup4 },
      { id: "4-25", question: "過食・反すう等", options: optionsGroup4 },
      { id: "4-26", question: "そう鬱状態", options: optionsGroup4 },
      { id: "4-27", question: "反復的行動", options: optionsGroup4 },
      { id: "4-28", question: "対人面の不安緊張", options: optionsGroup4 },
      { id: "4-29", question: "意欲が乏しい", options: optionsGroup4 },
      { id: "4-30", question: "話がまとまらない", options: optionsGroup4 },
      { id: "4-31", question: "集中力が続かない", options: optionsGroup4 },
      { id: "4-32", question: "自己の過大評価", options: optionsGroup4 },
      { id: "4-33", question: "集団への不適応", options: optionsGroup4 },
      { id: "4-34", question: "多飲水・過飲水", options: optionsGroup4 }
    ]
  },
  {
    title: "第5群:特別な医療に関連する項目",
    items: [
      { id: "5-1", question: "点滴の管理", options: optionsGroup5 },
      { id: "5-2", question: "中心静脈栄養", options: optionsGroup5 },
      { id: "5-3", question: "透析", options: optionsGroup5 },
      { id: "5-4", question: "ストーマの処置(人工肛門の処置)", options: optionsGroup5 },
      { id: "5-5", question: "酸素療法", options: optionsGroup5 },
      { id: "5-6", question: "レスピレーター(人工呼吸器)", options: optionsGroup5 },
      { id: "5-7", question: "気管切開の処置", options: optionsGroup5 },
      { id: "5-8", question: "疼痛の看護", options: optionsGroup5 },
      { id: "5-9", question: "経管栄養", options: optionsGroup5 },
      { id: "5-10", question: "モニター測定(血圧、心拍、酸素飽和度等)", options: optionsGroup5 },
      { id: "5-11", question: "じょくそうの処置", options: optionsGroup5 },
      { id: "5-12", question: "カテーテル", options: optionsGroup5 }
    ]
  }
];

logicEngine.js

// logicEngine.js

// =====================================================================
// 1. スコア・インデックス算出ロジック
// =====================================================================
export const calculateScores = (answers) => {
  // マニュアル P.11 に基づく重み付け係数パターン
  const weights = {
    patternA: { 1: 0.0, 2: 7.8, 3: 10.4, 4: 14.8 }, 
    patternB: { 1: 0.0, 2: 6.2, 3: 8.9,  4: 15.0 }, 
    patternC: { 1: 0.0, 2: 6.8, 3: 11.6, 4: 15.9 }, 
    patternD: { 1: 0.0, 2: 7.2, 3: 9.4,  4: 14.5 }, 
    patternE: { 1: 0.0, 2: 5.4, 3: 7.7,  4: 13.6 }, 
    patternF: { 1: 0.0, 2: 5.1, 3: 7.7,  4: 14.8 }, 
    patternG: { 1: 0.0, 2: 2.8, 3: 3.4,  4: 11.4 }, 
    pattern5: { 1: 0.0, 2: 1.0, 3: 3.0,  4: 5.0,  5: 10.0 }, // 行動障害等
    pattern2: { 1: 0.0, 2: 5.0 } // 医療等
  };

  // 各質問IDに対する係数パターンのマッピング
  const itemToPattern = {
    "1-1": weights.patternA, "1-2": weights.patternA, "1-3": weights.patternC,
    "1-4": weights.patternD, "1-5": weights.patternE, "1-6": weights.patternE,
    "1-7": weights.patternE, "1-8": weights.patternF, "1-9": weights.patternG,
    "2-1": weights.patternB, "2-2": weights.patternB, "2-3": weights.patternB,
  };

  const getScore = (id, patternObj = weights.patternA) => {
    const choice = answers[id];
    const pattern = itemToPattern[id] || patternObj;
    return (choice && pattern[choice] !== undefined) ? pattern[choice] : 0.0;
  };

  // 分野(群)のスコア算出
  const kikyo = getScore("1-1") + getScore("1-2") + getScore("1-3") + getScore("1-4") + getScore("1-5");
  const seikatsu1 = getScore("1-8") + getScore("2-1") + getScore("2-3") + getScore("2-4") + getScore("2-5");
  const seikatsu2 = getScore("1-10") + getScore("2-2") + getScore("2-6");
  const ouyou = getScore("2-8") + getScore("2-9") + getScore("2-12") + getScore("2-13") + getScore("2-14") + getScore("2-15") + getScore("2-16");
  const ninchi = getScore("2-10") + getScore("2-11") + getScore("3-4");
  const shichoukaku = getScore("3-1", weights.pattern5) + getScore("3-2", weights.pattern5);
  const koudouA = getScore("4-1", weights.pattern5) + getScore("4-3", weights.pattern5) + getScore("4-5", weights.pattern5);
  const koudouB = getScore("4-7", weights.pattern5) + getScore("4-9", weights.pattern5);
  const koudouC = getScore("4-14", weights.pattern5) + getScore("4-15", weights.pattern5);
  const iryou = getScore("5-1", weights.pattern2) + getScore("5-2", weights.pattern2) + getScore("5-3", weights.pattern2);
  const mahi_koushuku = getScore("1-1") + getScore("1-2");

  // 麻痺の度合い判定
  let mahi = 1;
  if (answers["1-1"] === 2 && answers["1-2"] === 2 && answers["1-3"] === 2 && answers["1-4"] === 2) mahi = 5;
  else if (answers["1-3"] === 2 && answers["1-4"] === 2) mahi = 3;
  else if (answers["1-1"] === 2 || answers["1-2"] === 2 || answers["1-3"] === 2 || answers["1-4"] === 2) mahi = 2;

  return {
    kikyo, seikatsu1, seikatsu2, ouyou, ninchi, koudouA, koudouB, koudouC, shichoukaku, iryou, mahi_koushuku, mahi,
    mahi_migi_ashi: answers["1-4"] || 1,   
    mahi_hidari_ashi: answers["1-3"] || 1,
    mahi_migi_te: answers["1-2"] || 1,     
    mahi_hidari_te: answers["1-1"] || 1,
    koushuku_kata: 1, koushuku_komata: 1, koushuku_hiji: 1, koushuku_hiza: 1, koushuku_sonota: 1,
    nijiku_nouryoku: answers["2-10"] || 1, 
    shougai_shokuji: answers["2-1"] || 1,  
    shougai_kinsen: answers["2-8"] || 1,   
    shougai_taijin: answers["4-28"] || 1,  
    shougai_rhythm: answers["4-4"] || 1,   
    shougai_fukuyaku: answers["2-7"] || 1, 
    shougai_hoji: answers["1-3"] || 1,     
    hankouteki: answers["4-8"] || 1        
  };
};

// =====================================================================
// 2. 条件評価ヘルパー関数
// =====================================================================
const evaluateCondition = (key, op, val, scores, answers) => {
  let actual = 0;
  if (answers.hasOwnProperty(key)) {
    actual = answers[key];
  } else if (scores.hasOwnProperty(key)) {
    actual = scores[key];
  }

  switch(op) {
    case "==": return actual === val;
    case "<=": return actual <= val;
    case ">=": return actual >= val;
    case "in": return Array.isArray(val) && val.includes(actual);
    default: return false;
  }
};

// =====================================================================
// 3. 一次判定ルール(抽出された80パターン)
// =====================================================================
const rules = [
  { no: 1, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "==", 0.0], ["koudouA", "==", 0.0], ["koudouC", "==", 0.0], ["2-8", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [62.2, 25.7, 10.8, 1.3, 0.0, 0.0, 0.0] },
  { no: 2, cond: [["ouyou", "==", 0.0], ["koudouA", "==", 0.0], ["koudouB", "==", 0.0], ["iryou", "==", 0.0], ["1-7", "==", 1], ["4-31", "==", 1], ["koushuku_kata", "==", 1], ["shougai_shokuji", "==", 1]], prob: [82.4, 11.8, 0.0, 5.8, 0.0, 0.0, 0.0] },
  { no: 3, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", "==", 0.0], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [4.2, 64.0, 28.4, 2.8, 0.6, 0.0, 0.0] },
  { no: 4, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 13.0], ["koudouA", "==", 0.0], ["2-8", "in", [2, 3]], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [2, 3, 4, 5]]], prob: [12.5, 80.4, 3.6, 3.5, 0.0, 0.0, 0.0] },
  { no: 5, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", ">=", 0.1], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [1, 2]]], prob: [0.0, 66.1, 31.4, 2.5, 0.0, 0.0, 0.0] },
  { no: 6, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 13.0], ["koudouA", "==", 0.0], ["2-8", "in", [2, 3]], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [1, 2, 3]]], prob: [17.5, 61.3, 20.0, 1.2, 0.0, 0.0, 0.0] },
  { no: 7, cond: [["seikatsu2", ">=", 23.5], ["ouyou", "<=", 13.0], ["koudouA", "==", 0.0], ["koudouC", ">=", 0.1], ["2-8", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [18.5, 61.1, 18.5, 1.9, 0.0, 0.0, 0.0] },
  { no: 8, cond: [["kikyo", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [0.8, 50.9, 40.4, 7.0, 0.9, 0.0, 0.0] },
  { no: 9, cond: [["kikyo", "==", 0.0], ["seikatsu1", "<=", 15.5], ["seikatsu2", "==", 0.0], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", ">=", 20.1], ["koudouC", "<=", 12.4], ["4-3", "==", 1]], prob: [0.0, 62.6, 31.3, 6.1, 0.0, 0.0, 0.0] },
  { no: 10, cond: [["kikyo", "==", 0.0], ["seikatsu2", ">=", 23.5], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", ">=", 23.7], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [0.0, 50.0, 45.3, 4.7, 0.0, 0.0, 0.0] },
  { no: 11, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", ">=", 0.1], ["4-3", "==", 1], ["4-29", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [3, 4, 5]]], prob: [0.0, 47.4, 44.0, 6.9, 1.7, 0.0, 0.0] },
  { no: 12, cond: [["seikatsu2", "<=", 23.5], ["ouyou", ">=", 0.1], ["ouyou", "<=", 13.0], ["koudouA", "==", 0.0], ["koudouC", "==", 0.0], ["2-8", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [37.2, 42.1, 19.8, 0.9, 0.0, 0.0, 0.0] },
  { no: 13, cond: [["kikyo", "==", 0.0], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", "<=", 23.6], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["shougai_kinsen", "in", [4, 5]]], prob: [0.0, 64.0, 33.3, 2.7, 0.0, 0.0, 0.0] },
  { no: 14, cond: [["seikatsu1", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", "<=", 10.7], ["koudouA", ">=", 0.1], ["koudouA", "<=", 14.1], ["koudouC", "<=", 14.0], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [3.1, 59.1, 24.2, 13.6, 0.0, 0.0, 0.0] },
  { no: 15, cond: [["kikyo", "==", 0.0], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", "<=", 23.6], ["2-10", "in", [2, 3]], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["shougai_kinsen", "in", [1, 2, 3]]], prob: [1.2, 92.7, 6.1, 0.0, 0.0, 0.0, 0.0] },
  { no: 16, cond: [["kikyo", "==", 0.0], ["seikatsu2", "<=", 6.7], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", "<=", 23.6], ["2-10", "==", 1], ["2-15", "in", [2, 3]], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["shougai_kinsen", "in", [1, 2, 3]]], prob: [0.0, 88.9, 11.1, 0.0, 0.0, 0.0, 0.0] },
  { no: 17, cond: [["kikyo", "==", 0.0], ["seikatsu2", "<=", 6.7], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", "<=", 23.6], ["2-10", "==", 1], ["2-15", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["shougai_kinsen", "in", [1, 2, 3]]], prob: [6.9, 74.1, 17.2, 1.8, 0.0, 0.0, 0.0] },
  { no: 18, cond: [["kikyo", "==", 0.0], ["seikatsu2", ">=", 6.8], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 13.1], ["ouyou", "<=", 36.1], ["koudouA", "==", 0.0], ["koudouC", "<=", 23.6], ["2-10", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]], ["shougai_kinsen", "in", [1, 2, 3]]], prob: [0.0, 72.3, 26.2, 1.5, 0.0, 0.0, 0.0] },
  { no: 19, cond: [["seikatsu2", "==", 0.0], ["koudouA", "==", 0.0], ["iryou", "==", 0.0], ["2-2", "==", 2], ["shougai_kinsen", "==", 3]], prob: [0.0, 88.9, 11.1, 0.0, 0.0, 0.0, 0.0] },
  { no: 20, cond: [["seikatsu2", "==", 0.0], ["koudouA", "==", 0.0], ["2-8", "==", 2], ["nijiku_nouryoku", "==", 2], ["shougai_taijin", "==", 2]], prob: [3.4, 96.6, 0.0, 0.0, 0.0, 0.0, 0.0] },
  { no: 21, cond: [["2-3", "==", 1], ["4-3", "==", 1], ["nijiku_nouryoku", "==", 2], ["shougai_rhythm", "==", 1], ["shougai_fukuyaku", "==", 2]], prob: [0.0, 84.4, 12.5, 3.1, 0.0, 0.0, 0.0] },
  { no: 22, cond: [["seikatsu2", "==", 0.0], ["2-12", "==", 2], ["4-3", "==", 1], ["nijiku_nouryoku", "==", 2], ["shougai_rhythm", "==", 1]], prob: [0.0, 82.9, 17.1, 0.0, 0.0, 0.0, 0.0] },
  { no: 23, cond: [["seikatsu2", "==", 0.0], ["ninchi", ">=", 0.1], ["ninchi", "<=", 13.1], ["koudouA", "==", 0.0], ["2-8", "==", 2], ["nijiku_nouryoku", "==", 2]], prob: [0.0, 87.0, 10.9, 2.1, 0.0, 0.0, 0.0] },
  { no: 24, cond: [["ouyou", ">=", 0.1], ["ouyou", "<=", 32.9], ["koudouA", "==", 0.0], ["2-14", "==", 2], ["shougai_shokuji", "==", 3], ["shougai_hoji", "==", 3]], prob: [0.0, 94.1, 5.9, 0.0, 0.0, 0.0, 0.0] },
  { no: 25, cond: [["shichoukaku", "==", 0.0], ["2-13", "==", 1], ["4-3", "==", 1], ["4-31", "==", 1], ["nijiku_nouryoku", "==", 2], ["shougai_kinsen", "==", 3]], prob: [3.1, 87.5, 6.3, 3.1, 0.0, 0.0, 0.0] },
  { no: 26, cond: [["shichoukaku", "==", 0.0], ["2-13", "==", 1], ["4-3", "==", 1], ["4-17", "==", 1], ["nijiku_nouryoku", "==", 2], ["shougai_kinsen", "==", 3]], prob: [2.9, 85.7, 8.6, 2.8, 0.0, 0.0, 0.0] },
  { no: 27, cond: [["ouyou", ">=", 0.1], ["ouyou", "<=", 32.9], ["koudouA", "==", 0.0], ["2-14", "==", 2], ["nijiku_nouryoku", "==", 3], ["shougai_kinsen", "==", 3]], prob: [0.0, 90.6, 3.1, 6.3, 0.0, 0.0, 0.0] },
  { no: 28, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "==", 0.0], ["koudouA", "==", 0.0], ["koudouC", "==", 0.0], ["1-7", "in", [2, 3]], ["2-8", "==", 1], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [12.5, 75.0, 12.5, 0.0, 0.0, 0.0, 0.0] },
  { no: 29, cond: [["seikatsu2", "==", 0.0], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", ">=", 20.2], ["koudouA", "<=", 32.7]], prob: [0.0, 1.9, 68.5, 27.8, 1.8, 0.0, 0.0] },
  { no: 30, cond: [["seikatsu2", "<=", 10.6], ["ouyou", "<=", 36.1], ["mahi_koushuku", "<=", 8.7], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]]], prob: [0.0, 25.3, 67.0, 7.7, 0.0, 0.0, 0.0] },
  { no: 31, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["koudouA", ">=", 21.0], ["2-12", "==", 1], ["4-3", "in", [2, 3, 4, 5]]], prob: [0.0, 6.8, 61.6, 26.0, 4.1, 1.5, 0.0] },
  { no: 32, cond: [["kikyo", "<=", 6.8], ["seikatsu1", "<=", 4.0], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 73.3], ["koudouA", "<=", 16.7]], prob: [0.0, 0.0, 60.5, 32.1, 7.4, 0.0, 0.0] },
  { no: 33, cond: [["seikatsu2", ">=", 10.7], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]]], prob: [0.0, 14.8, 53.0, 32.2, 0.0, 0.0, 0.0] },
  { no: 34, cond: [["seikatsu2", "<=", 10.6], ["ouyou", "<=", 36.1], ["mahi_koushuku", ">=", 8.8], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]], ["mahi_migi_ashi", "==", 1]], prob: [0.0, 6.1, 90.9, 3.0, 0.0, 0.0, 0.0] },
  { no: 35, cond: [["seikatsu2", "<=", 10.6], ["ouyou", "<=", 36.1], ["mahi_koushuku", ">=", 8.8], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]], ["mahi_hidari_ashi", "==", 1]], prob: [0.0, 6.1, 90.9, 3.0, 0.0, 0.0, 0.0] },
  { no: 36, cond: [["seikatsu2", "<=", 10.6], ["ouyou", "<=", 36.1], ["mahi_koushuku", ">=", 8.8], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]], ["mahi_migi_ashi", "in", [2, 3, 4]]], prob: [0.0, 2.7, 80.0, 17.3, 0.0, 0.0, 0.0] },
  { no: 37, cond: [["seikatsu2", "<=", 10.6], ["ouyou", "<=", 36.1], ["mahi_koushuku", ">=", 8.8], ["4-3", "==", 1], ["mahi", "in", [3, 4, 5]], ["mahi_hidari_ashi", "in", [2, 3, 4]]], prob: [0.0, 2.7, 80.0, 17.3, 0.0, 0.0, 0.0] },
  { no: 38, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", "==", 0.0], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["4-3", "in", [2, 3, 4, 5]]], prob: [0.0, 4.3, 74.5, 20.2, 1.0, 0.0, 0.0] },
  { no: 39, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", ">=", 0.1], ["koudouA", "<=", 20.9], ["koudouC", "<=", 38.6], ["4-3", "in", [2, 3, 4, 5]]], prob: [0.0, 8.7, 74.4, 15.3, 1.6, 0.0, 0.0] },
  { no: 40, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", "==", 0.0], ["koudouA", "<=", 20.9], ["4-3", "in", [2, 3, 4, 5]], ["4-4", "in", [2, 3, 4, 5]]], prob: [0.0, 2.8, 72.2, 20.8, 4.2, 0.0, 0.0] },
  { no: 41, cond: [["seikatsu1", ">=", 15.6], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["mahi_koushuku", "<=", 7.1]], prob: [0.0, 2.6, 59.0, 35.9, 0.0, 2.5, 0.0] },
  { no: 42, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", ">=", 0.1], ["koudouA", ">=", 20.9], ["koudouC", ">=", 38.7], ["4-3", "in", [2, 3, 4, 5]]], prob: [0.4, 5.9, 56.9, 30.5, 5.9, 0.4, 0.0] },
  { no: 43, cond: [["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", "==", 0.0], ["koudouA", ">=", 20.9], ["4-3", "in", [2, 3, 4, 5]], ["4-4", "==", 1]], prob: [0.0, 43.7, 48.1, 8.2, 0.0, 0.0, 0.0] },
  { no: 44, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["koudouA", ">=", 14.2], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [0.0, 20.5, 42.2, 27.7, 9.6, 0.0, 0.0] },
  { no: 45, cond: [["kikyo", ">=", 0.1], ["seikatsu1", "<=", 15.5], ["seikatsu2", "==", 0.0], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["4-3", "==", 1]], prob: [0.0, 11.3, 80.4, 8.3, 0.0, 0.0, 0.0] },
  { no: 46, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 8.4], ["koudouC", ">=", 38.7]], prob: [0.0, 4.6, 53.8, 40.0, 1.6, 0.0, 0.0] },
  { no: 47, cond: [["seikatsu1", "==", 0.0], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-7", "==", 4]], prob: [0.0, 0.0, 74.1, 25.9, 0.0, 0.0, 0.0] },
  { no: 48, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", ">=", 0.1], ["4-3", "==", 1], ["4-29", "in", [2, 3, 4, 5]], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [3, 4, 5]]], prob: [0.0, 22.8, 68.4, 7.6, 1.2, 0.0, 0.0] },
  { no: 49, cond: [["kikyo", "==", 0.0], ["seikatsu1", "<=", 15.5], ["seikatsu2", "==", 0.0], ["ouyou", ">=", 42.8], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["koudouC", "<=", 12.5], ["4-3", "==", 1]], prob: [0.0, 14.3, 67.9, 16.1, 1.7, 0.0, 0.0] },
  { no: 50, cond: [["seikatsu1", ">=", 21.0], ["seikatsu2", ">=", 23.6], ["seikatsu2", "<=", 32.7], ["ouyou", "<=", 73.2], ["ninchi", ">=", 20.6], ["koudouA", ">=", 32.7], ["1-4", "==", 1], ["4-5", "==", 1]], prob: [0.0, 1.1, 58.9, 34.7, 5.3, 0.0, 0.0] },
  { no: 51, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouA", "<=", 14.1], ["koudouC", ">=", 14.1], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [0.7, 32.7, 58.0, 6.7, 1.3, 0.6, 0.0] },
  { no: 52, cond: [["kikyo", "==", 0.0], ["seikatsu1", "<=", 15.5], ["seikatsu2", "==", 0.0], ["ouyou", ">=", 36.2], ["ouyou", "<=", 42.7], ["koudouA", "<=", 20.1], ["koudouC", ">=", 12.5], ["4-3", "==", 1]], prob: [0.0, 37.9, 56.9, 3.4, 1.8, 0.0, 0.0] },
  { no: 53, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", ">=", 8.5], ["koudouA", "<=", 20.1], ["koudouC", ">=", 38.7]], prob: [0.0, 0.0, 55.4, 25.7, 18.9, 0.0, 0.0] },
  { no: 54, cond: [["seikatsu1", "<=", 21.0], ["seikatsu2", ">=", 23.6], ["seikatsu2", "<=", 50.6], ["ouyou", "<=", 73.2], ["ninchi", "<=", 20.5], ["koudouA", "<=", 32.7], ["1-4", "==", 1], ["1-7", "in", [2, 3, 4]]], prob: [0.0, 2.2, 52.2, 40.0, 5.6, 0.0, 0.0] },
  { no: 55, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", "<=", 36.1], ["ninchi", ">=", 10.8], ["koudouA", ">=", 0.1], ["koudouA", "<=", 14.1], ["koudouC", "<=", 14.0], ["4-3", "==", 1], ["mahi", "in", [1, 2]]], prob: [0.0, 43.5, 43.5, 11.8, 1.2, 0.0, 0.0] },
  { no: 56, cond: [["seikatsu1", "<=", 21.0], ["seikatsu2", ">=", 23.6], ["seikatsu2", "<=", 50.6], ["ouyou", "<=", 73.2], ["ninchi", "<=", 20.5], ["koudouA", "<=", 32.7], ["1-4", "==", 1], ["1-7", "==", 1], ["4-18", "==", 1]], prob: [2.8, 9.9, 74.3, 12.9, 0.9, 0.0, 0.0] },
  { no: 57, cond: [["seikatsu1", "<=", 21.0], ["seikatsu2", ">=", 23.6], ["seikatsu2", "<=", 50.6], ["ouyou", "<=", 73.2], ["ninchi", "<=", 20.5], ["koudouA", "<=", 32.7], ["1-4", "==", 1], ["1-7", "==", 1], ["4-18", "in", [2, 3, 4, 5]]], prob: [0.0, 4.6, 52.3, 38.5, 4.6, 0.0, 0.0] },
  { no: 58, cond: [["seikatsu1", ">=", 0.1], ["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-7", "==", 4]], prob: [0.0, 0.0, 48.5, 47.0, 4.5, 0.0, 0.0] },
  { no: 59, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 51.6], ["ninchi", ">=", 19.0], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-7", "in", [1, 2, 3]], ["shougai_taijin", "in", [1, 2, 3]]], prob: [0.0, 9.5, 77.8, 11.1, 1.6, 0.0, 0.0] },
  { no: 60, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 51.7], ["ouyou", "<=", 73.2], ["ninchi", ">=", 19.0], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-7", "in", [1, 2, 3]], ["shougai_taijin", "in", [1, 2, 3]]], prob: [0.0, 1.6, 70.5, 27.9, 0.0, 0.0, 0.0] },
  { no: 61, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-4", "in", [2, 3, 4]], ["1-7", "in", [1, 2, 3]]], prob: [1.8, 0.0, 67.2, 27.6, 3.4, 0.0, 0.0] },
  { no: 62, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", ">=", 19.0], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["1-7", "in", [1, 2, 3]], ["shougai_taijin", "in", [4, 5]]], prob: [0.0, 3.4, 52.5, 44.1, 0.0, 0.0, 0.0] },
  { no: 63, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", "<=", 20.1], ["koudouC", "==", 0.0], ["mahi_koushuku", ">=", 0.1], ["1-4", "==", 1], ["1-7", "in", [1, 2, 3]]], prob: [0.0, 2.1, 95.9, 2.0, 0.0, 0.0, 0.0] },
  { no: 64, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", ">=", 1.6], ["koudouA", "<=", 20.1], ["koudouC", "<=", 38.6], ["mahi_koushuku", "==", 0.0], ["1-4", "==", 1], ["1-7", "in", [1, 2, 3]]], prob: [0.0, 5.4, 81.0, 10.9, 2.7, 0.0, 0.0] },
  { no: 65, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", "<=", 20.1], ["koudouC", ">=", 0.1], ["koudouC", "<=", 38.6], ["mahi_koushuku", ">=", 0.1], ["1-4", "==", 1], ["1-7", "in", [1, 2, 3]]], prob: [0.0, 7.0, 75.4, 15.8, 1.8, 0.0, 0.0] },
  { no: 66, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", "<=", 1.5], ["koudouC", "<=", 38.6], ["mahi_koushuku", "==", 0.0], ["1-4", "==", 1], ["1-7", "in", [1, 2, 3]], ["2-2", "in", [2, 3]]], prob: [0.0, 20.7, 72.4, 6.9, 0.0, 0.0, 0.0] },
  { no: 67, cond: [["seikatsu1", "<=", 15.5], ["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["ninchi", "<=", 18.9], ["koudouA", "<=", 1.5], ["koudouC", "<=", 38.6], ["mahi_koushuku", "==", 0.0], ["1-4", "==", 1], ["1-7", "in", [1, 2, 3]], ["2-2", "==", 1]], prob: [0.0, 43.9, 49.1, 7.0, 0.0, 0.0, 0.0] },
  { no: 68, cond: [["2-3", "==", 1], ["2-16", "==", 1], ["mahi_migi_te", "==", 3]], prob: [2.9, 11.4, 80.0, 5.7, 0.0, 0.0, 0.0] },
  { no: 69, cond: [["2-3", "==", 1], ["2-16", "==", 1], ["mahi_hidari_te", "==", 3]], prob: [2.9, 11.4, 80.0, 5.7, 0.0, 0.0, 0.0] },
  { no: 70, cond: [["seikatsu1", "==", 0.0], ["2-3", "==", 1], ["5-3", "==", 2]], prob: [0.0, 3.1, 84.8, 12.1, 0.0, 0.0, 0.0] },
  { no: 71, cond: [["1-2", "==", 1], ["2-16", "==", 1], ["5-3", "==", 2]], prob: [0.0, 3.0, 90.9, 6.1, 0.0, 0.0, 0.0] },
  { no: 72, cond: [["seikatsu1", "==", 0.0], ["iryou", ">=", 0.1], ["iryou", "<=", 3.7], ["2-3", "==", 1]], prob: [0.0, 2.1, 80.9, 17.0, 0.0, 0.0, 0.0] },
  { no: 73, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 19.5], ["shichoukaku", ">=", 10.7], ["shichoukaku", "<=", 41.1], ["ouyou", ">=", 33.0], ["ouyou", "<=", 61.5], ["2-16", "==", 3]], prob: [0.0, 7.1, 88.1, 4.8, 0.0, 0.0, 0.0] },
  { no: 74, cond: [["2-16", "==", 1], ["5-3", "==", 2], ["koushuku_kata", "==", 1], ["koushuku_komata", "==", 1], ["koushuku_hiji", "==", 1], ["koushuku_hiza", "==", 1], ["koushuku_sonota", "==", 1]], prob: [0.0, 3.0, 88.2, 8.8, 0.0, 0.0, 0.0] },
  { no: 75, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", ">=", 20.2], ["koudouA", "<=", 32.7], ["2-8", "==", 1]], prob: [0.0, 0.0, 50.0, 42.9, 0.0, 7.1, 0.0] },
  { no: 76, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 23.5], ["ouyou", ">=", 36.2], ["ouyou", "<=", 73.2], ["koudouA", ">=", 20.2], ["koudouA", "<=", 32.7], ["2-15", "==", 1]], prob: [0.0, 0.0, 55.6, 27.8, 11.1, 5.5, 0.0] },
  { no: 77, cond: [["seikatsu2", ">=", 0.1], ["seikatsu2", "<=", 19.5], ["ouyou", ">=", 33.0], ["ouyou", "<=", 61.5], ["ninchi", ">=", 0.1], ["ninchi", "<=", 13.1], ["3-1", "in", [4, 5]]], prob: [0.0, 12.3, 82.5, 3.5, 1.7, 0.0, 0.0] },
  { no: 78, cond: [["kikyo", "==", 0.0], ["seikatsu1", ">=", 21.1], ["seikatsu2", ">=", 23.6], ["seikatsu2", "<=", 34.8], ["ouyou", "<=", 69.4], ["koudouA", "<=", 30.2], ["koudouC", "<=", 24.7], ["1-4", "in", [1, 2]]], prob: [0.0, 0.0, 60.7, 39.3, 0.0, 0.0, 0.0] },
  { no: 79, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", ">=", 0.1], ["4-1", "==", 3], ["4-3", "==", 1], ["4-29", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [1, 4, 5]]], prob: [0.0, 40.0, 50.0, 10.0, 0.0, 0.0, 0.0] },
  { no: 80, cond: [["seikatsu2", "==", 0.0], ["ouyou", "<=", 36.1], ["koudouA", ">=", 0.1], ["koudouB", ">=", 0.1], ["4-3", "==", 1], ["hankouteki", "==", 5], ["4-29", "==", 1], ["mahi", "in", [1, 2]], ["nijiku_nouryoku", "in", [3, 4, 5]]], prob: [0.0, 35.7, 57.1, 0.0, 7.2, 0.0, 0.0] }
];

// =====================================================================
// 4. メイン評価関数(フォールバック機能付き)
// =====================================================================
export const evaluateClassification = (answers) => {
  const scores = calculateScores(answers);

  // まずは登録されている80パターンの厳密ルールを走査
  for (const rule of rules) {
    let isMatch = true;
    for (const [key, op, val] of rule.cond) {
      if (!evaluateCondition(key, op, val, scores, answers)) {
        isMatch = false;
        break;
      }
    }
    // 合致したらそのルールを返す
    if (isMatch) {
      return {
        node: rule.no,
        probabilities: rule.prob
      };
    }
  }

  // =====================================================================
  // どのルールにも合致しなかった場合の近似ロジック(フォールバック)
  // =====================================================================
  
  // 全項目の推定介護時間を合計して「総合的な重症度」を算出
  const totalScore = 
    scores.kikyo + scores.seikatsu1 + scores.seikatsu2 + scores.ouyou + 
    scores.ninchi + scores.koudouA + scores.koudouB + scores.koudouC + 
    scores.shichoukaku + scores.iryou + scores.mahi_koushuku;

  let fallbackNode = "";
  let prob = [];

  // 合計スコアに基づき、論理的な確率分布を統計的に割り当てます
  if (totalScore === 0) {
    fallbackNode = "概算-A (完全自立)";
    prob = [95.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0];
  } else if (totalScore < 40) {
    fallbackNode = "概算-B (軽度支援)";
    prob = [60.0, 30.0, 10.0, 0.0, 0.0, 0.0, 0.0];
  } else if (totalScore < 90) {
    fallbackNode = "概算-C (部分支援)";
    prob = [10.0, 40.0, 40.0, 10.0, 0.0, 0.0, 0.0];
  } else if (totalScore < 160) {
    fallbackNode = "概算-D (中等度支援)";
    prob = [0.0, 10.0, 30.0, 45.0, 15.0, 0.0, 0.0];
  } else if (totalScore < 240) {
    fallbackNode = "概算-E (中重度支援)";
    prob = [0.0, 0.0, 10.0, 30.0, 45.0, 15.0, 0.0];
  } else if (totalScore < 320) {
    fallbackNode = "概算-F (重度支援)";
    prob = [0.0, 0.0, 0.0, 10.0, 30.0, 45.0, 15.0];
  } else {
    fallbackNode = "概算-G (最重度支援)";
    prob = [0.0, 0.0, 0.0, 0.0, 10.0, 40.0, 50.0];
  }

  // 【特例補正】特定の障害特性(行動障害・医療)による重み付け補正
  const koudouTotal = scores.koudouA + scores.koudouB + scores.koudouC;
  if (koudouTotal > 50) {
    fallbackNode += " ※行動障害加味";
    prob = [0.0, 0.0, 5.0, 15.0, 40.0, 30.0, 10.0]; // 行動障害が強い場合は区分4〜6にシフト
  } else if (scores.iryou > 15) {
    fallbackNode += " ※医療的ケア加味";
    prob = [0.0, 0.0, 0.0, 10.0, 20.0, 40.0, 30.0]; // 医療ケアが多い場合も重度にシフト
  }

  // 計算された近似確率を返す
  return {
    node: fallbackNode,
    probabilities: prob
  };
};

main.jsx

// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

index.css

/* src/index.css */

/* Tailwind CSS v4 の読み込み */
@import "tailwindcss";

/* アプリ全体のベース設定 */
body {
  background-color: #f8fafc; /* 背景色(slate-50) */
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* カスタムユーティリティとアニメーションの定義 */
@layer utilities {
  
  /* ① ふわっと下から浮き上がるアニメーション */
  .animate-fade-in-down {
    animation: fadeInDown 0.5s ease-out forwards;
  }

  @keyframes fadeInDown {
    0% {
      opacity: 0;
      transform: translateY(-15px);
    }
    100% {
      opacity: 1;
      transform: translateY(0);
    }
  }

  /* ② プログレスバーがキラッと光るアニメーション */
  @keyframes shimmer {
    0% {
      transform: translateX(-100%) skewX(-12deg);
    }
    100% {
      transform: translateX(200%) skewX(-12deg);
    }
  }

  /* ③ 横スクロールバーを隠す設定(タブメニュー用) */
  .scrollbar-hide::-webkit-scrollbar {
    display: none;
  }
  .scrollbar-hide {
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */
  }
}

これにてアプリ作成は完了です、お疲れ様でした。

アプリ起動

それでは早速、作成したアプリを起動してみましょう。以下のコマンドを実行してみてください。

npm run dev

こちらを実行すると、ターミナル上に「http://localhost:○○○○/」のようなURLが表示されるはずです。そちらを押下、もしくはコピーしてGoogle ChromeやEdgeなどのWEBブラウザ上に張り付けてみてください。アプリが立ち上がり、操作ができるようになるはずです。

もし上記のコマンドを実行してエラーが出た際は、以下コマンドを実行してみてください。

cd disability-simulator

要するに、「disability-simulator」というディレクトリ配下にいないとアプリ実行できないという理由なわけですが、知識のある人は自分で何とでもできると思いますし、知識の無い方は基本VS Codeなんてこのタイミングでしか開かないと思うので、おそらく「disability-simulator」の1つ親ディレクトリにいるだろうと推測しての対応です。

まぁとにかく、よく分からなくてエラーが出たら上記のコマンドを実行してください。

アプリ自体は直感的に使えるはずなので、自由にお使いください。

アプリ終了

アプリを終了する際は、ブラウザを右上の「×」で閉じ、ターミナル上で「Ctrl」+「C」を押下してください。コピーのショートカットと同じですね。

最後に

さて今回は、障害支援区分のシミュレーションツールを作りました。

やっぱりIT知識の全くない人にはこれだけ見せられても厳しいですかね・・・。とはいえ、何かしらでも参考になれば嬉しいです。

以上!

他にも勉強記事をまとめているので是非!

コメント

タイトルとURLをコピーしました