ken_nogi/Pleasanter/ケアセブン(リハ)/docs/superpowers/plans/2026-08-28-B-calendar-copy-plan.md
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

33 KiB
Raw Blame History

【B】リハ訪問カレンダー独立動作化 + A/B予定コピー機能 実装計画

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 【B】リハ訪問カレンダー(505009)を【A】(482693)から独立して動作するようにし、月単位で【A】→【B】へ予定周回予定・個別予定・例外予定日をコピーする機能を追加する。

Architecture: Pleasanterのサイト設定・クライアントカスタムスクリプトscripts/1_訪問カレンダー.jsを直接編集するプロジェクト。SiteSettings構造変更列追加はフル更新updatesite用Node.jsスクリプトを新規作成して適用し、クライアントスクリプトの追記は既存の抽出済みファイル直接編集 → build-desired-config.jsapply-desired-config-full.js(部分更新 updatesitesettings)で反映する。

Tech Stack: PleasanterASP.NET製ーコード基盤カスタムJS/CSS、jQuery / jQuery UI、FullCalendar Scheduler 6.1.8、Node.js反映ツール群、ClaudePleasanter/.claude/js/共通実装)。

参照設計書: ケアセブン(リハ)/docs/superpowers/specs/2026-08-28-B-calendar-copy-design.md

Global Constraints

  • サーバー環境は本番のみ(--env=production)。ケアセブン(リハ)にdevelopment環境の対応サイトは無い。
  • apply-site-config.js は常にドライラン。Claude自身がAPIへ実送信するスクリプト--execute付き)は、その都度ユーザーが明示的に許可した場合のみ実行する。
  • フル更新(Mode:"full"/updatesite送信時は、取得済み生JSONgetsiteレスポンス)を丸ごとベースにコピーし変更箇所だけ差し替える。SiteSettings.Linksは省略しない。
  • 修正のたびに対象サイトのmodify/{リクエストラベル}/フォルダへ変更前後ファイル・差分・送信結果・summary_{timestamp}.mdを保存する。
  • APIキー等の秘密情報はチャット上に転記しない。
  • gitコミットはユーザーから明示指示があるまで行わない。
  • CheckG列に新規作成時の自動デフォルト値設定機能は持たせない不要と判明。コピー実行時にAPIで明示的にCheckG=trueをセットするのみ。6サイト共通で{ColumnName:"CheckG", LabelText:"A由来フラグ", DefaultInput:"false", NoWrap:true, Hide:true}Hideで編集画面から隠し、ユーザーが任意に変更できないようにする
  • Check型の値は実機確認済み: CheckHash: { CheckA: false } のようにCheckHashオブジェクト配下にboolean値で格納される取得・作成・削除APIとも確認済み。詳細はTask 1参照
  • 【A】側4サイト(482693/482696/482701/482700)は、SiteSettings改修列追加・スクリプト修正等は許可するが、レコード実データの新規登録・削除は禁止ユーザー明示指示、2026-08-28。API検証・動作確認で書込みが必要な場面は必ず【B】側(505009/505010/505011/505012)を使う。コピー機能Task7-8の設計上、削除・作成の対象は常に【B】側のみでありこの制約に抵触しない。
  • 本番環境(唯一の環境)での実装のため、通常以上に慎重に進める。読み取り専用の確認を優先し、書込みを伴う検証は影響範囲を最小化した上で都度ユーザーに確認してから実施する。

ファイル構成

ファイル 種別 役割
ClaudePleasanter/.claude/js/site-scripts/ケアセブン(リハ)/add-checkg-column.js 新規作成 周回予定・個別予定・例外予定日 計6サイトのSiteSettingsへCheckG列を追加し、フル更新用JSONを生成・送信する
ケアセブン(リハ)/configs/production/site-482693_【A】リハ訪問カレンダー/scripts/1_訪問カレンダー.js 修正 A/B切替ボタン、コピー共通ロジック、「▶Bへコピー」ボタン追加
ケアセブン(リハ)/configs/production/site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js 修正 参照テーブルID修正505010/505011/505012化、A/B切替ボタン、コピー共通ロジック、「▶Aからコピー」ボタン追加

Task 1: Pleasanter API仕様の確定新規作成/削除/デフォルト値【完了・2026-08-28】

運用制約により、検証先は当初計画の482701【A】個別予定から505011【B】個別予定へ変更して実施した【A】側はレコード新規登録・削除禁止のため。505011にテストレコードを2件作成し内容確認後に即削除、本番に痕跡は残っていない。

確定した仕様:

  • create: POST {baseUrl}/pleasanter/api/items/{siteId}/create、Body: {ApiVersion, ApiKey, ClassHash:{...}, DateHash:{...}, NumHash:{...}, CheckHash:{...}, Body:"..."}。レスポンス: {Id, StatusCode, Message}Idはトップレベル直下)。
  • delete: POST {baseUrl}/pleasanter/api/items/{itemId}/delete、Body: {ApiVersion, ApiKey}のみ。レスポンス: {Id, StatusCode, Message}
  • CheckHashはboolean値をそのまま送信・保存できるCheckHash: {CheckA: true}で確認済み)。
  • Task 7のapiCreateItem/apiDeleteItem/pleasanterApiUrlは上記と完全に一致しており、コード修正不要。

未検証のまま残る事項Task 2実施時に確認:

  • SiteSettings.Columns[].DefaultInputのCheck型での値表現。CheckG列追加後、実機のNew画面で確認し、想定と異なれば列定義を再調整する。

design docの「未確定・実装時に要検証の事項」節・「本番検証時の運用制約」節に反映済み。


Task 2-4: CheckG列追加の作成・実行・反映確認【完了・2026-08-28】

Task 1完了後、ユーザーが本番管理画面から482696【A】周回予定へCheckGを手動追加(実物設定: LabelText:"A由来フラグ", DefaultInput:"false", NoWrap:true, Hide:true。この実物に合わせ、自動デフォルト値機能A側ON/B側OFFは不要と方針変更、代わりに列をHide:trueにして編集不可にする方針へ変更した。

実施内容:

  1. ClaudePleasanter/.claude/js/site-scripts/ケアセブン(リハ)/add-checkg-column.js を作成。6サイト482696/482701/482700/505010/505011/505012を対象に、482696の実物と同一のCheckG列定義DefaultInput:"false", NoWrap:true, Hide:trueを追加するスクリプト。既にCheckG列を持つサイトは自動スキップ冪等
  2. ドライラン実行→差分プレビューをユーザーへ提示→承認取得→--executeで本番へ送信。
  3. 482696は既存列検知によりスキップ、残り5サイト482701/482700/505010/505011/505012は全てHTTP 200で成功。
  4. 全17サイトをget-site-config.jsで再取得し、6サイト全てのColumnsに同一定義のCheckG列が存在することを確認済み。

確認済み最終状態6サイト共通:

{"ColumnName":"CheckG","LabelText":"A由来フラグ","DefaultInput":"false","NoWrap":true,"Hide":true}

差分プレビュー・送信結果は各サイトのmodify/checkg-column/フォルダに保存済み。


Task 5: 【B】側スクリプトの参照テーブルID修正

Files:

  • Modify: ケアセブン(リハ)/configs/production/site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js:6-11

  • Step 1: 冒頭の定数を修正する

修正前:

//本番用
let kokyakuTableId = '482703';
let resourceTableId = '482697';
let reigaiTableId = '482700';
let shukaiTableId = '482696';
let kobetsuTableId = '482701';
let holidayTableId = '482695';

修正後:

//本番用
let kokyakuTableId = '482703';
let resourceTableId = '482697';
let reigaiTableId = '505012';
let shukaiTableId = '505010';
let kobetsuTableId = '505011';
let holidayTableId = '482695';
  • Step 2: ブラウザで【B】画面を開き、【A】のデータが表示されなくなった現状505010/505011/505012にデータが無いため空表示になることを確認する

本番URL https://nextoffice.next-hd.co.jp/pleasanter/items/505009/index/ を開き、カレンダーが空周回予定・個別予定・例外予定日いずれも0件で表示されることを確認する。エラーが出ずに画面が表示されればOKこの時点ではまだデータが無いのが正しい状態


Task 6: A/B切替ボタンの実装

Files:

  • Modify: ケアセブン(リハ)/configs/production/site-482693_【A】リハ訪問カレンダー/scripts/1_訪問カレンダー.js
  • Modify: ケアセブン(リハ)/configs/production/site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js

Interfaces:

  • Produces: selfSiteId/otherSiteId/selfLabel/otherLabel 定数Task 7-8のコピー機能がotherSiteId等は使わないため後続タスクへの依存なし)

  • Step 1: 【A】側(482693)冒頭の定数群の直後に切替用定数を追加する

let holidayTableId = '482695'; の直後に追加:

let holidayTableId = '482695';

//A/B切替・コピー機能用
let selfSiteId = '482693';
let selfLabel = 'A';
let otherSiteId = '505009';
let otherLabel = 'B';
  • Step 2: 【B】側(505009)にも同様に追加する

let holidayTableId = '482695'; の直後に追加:

let holidayTableId = '482695';

//A/B切替・コピー機能用
let selfSiteId = '505009';
let selfLabel = 'B';
let otherSiteId = '482693';
let otherLabel = 'A';
  • Step 3: 両ファイルのinitialize()内、buttonArea構築部分にA/B切替ボタンを追加する

現状482693/505009共通:

    let button0 = document.createElement('button');
    button0.type = 'button';
    button0.id = 'button0';
    button0.innerHTML = ' ▶顧客リスト ';
    button0.setAttribute('onclick', 'location.href="/pleasanter/items/' + kokyakuTableId + '/index/"');

    buttonArea.append(button0);
    buttonArea.append(selectMonth);

修正後(両ファイル共通コード。selfLabel/otherLabel/otherSiteIdは各ファイルのStep1/2で定義済みの値を使う:

    let buttonSelf = document.createElement('button');
    buttonSelf.type = 'button';
    buttonSelf.id = 'buttonSelf';
    buttonSelf.innerHTML = ' 【' + selfLabel + '】 ';
    buttonSelf.disabled = true;

    let buttonOther = document.createElement('button');
    buttonOther.type = 'button';
    buttonOther.id = 'buttonOther';
    buttonOther.innerHTML = ' 【' + otherLabel + '】へ ';
    buttonOther.setAttribute('onclick', 'location.href="/pleasanter/items/' + otherSiteId + '/index/"');

    let button0 = document.createElement('button');
    button0.type = 'button';
    button0.id = 'button0';
    button0.innerHTML = ' ▶顧客リスト ';
    button0.setAttribute('onclick', 'location.href="/pleasanter/items/' + kokyakuTableId + '/index/"');

    buttonArea.append(buttonSelf);
    buttonArea.append(buttonOther);
    buttonArea.append(button0);
    buttonArea.append(selectMonth);
  • Step 4: ボタンのスタイル設定を追加する

既存の$('#button0').button();の直後(両ファイル共通)に追加:

    $('#button0').button();

    ///////////////////
    $('#buttonSelf').css({
        'margin': '0 5px 5px 0',
        'font-size': '1.5em',
        'font-weight': 'bold',
        'background-color': '#4a90d9',
        'color': '#fff',
    });
    $('#buttonOther').css({
        'margin': '0 5px 5px 0',
        'font-size': '1.5em',
    });
    $('#buttonOther').button();
  • Step 5: ブラウザで動作確認する

【A】画面(482693)と【B】画面(505009)それぞれを開き、左端に【A】【B】ボタンが表示され、現在表示中のサイトに対応するボタンが青色でハイライトされていること、もう一方のボタンをクリックすると相手サイトの画面へ遷移することを確認する。


Task 7: コピー機能コアロジックの実装(両ファイル共通)

Task 1で確定したAPI仕様create/delete のURL・ボディ形式に基づき実装する。以下のコードはTask 1の検証結果が本計画作成時点の仮定CheckHashはboolean、URLパターンはhttps://{domain}/pleasanter/api/items/{id}/{action}と一致する前提で書かれている。Task 1で異なる結果が出た場合はこのコードのAPI呼び出し部分pleasanterApiUrl/apiCreateItem/apiDeleteItem)のみ実測値に合わせて修正する。

Files:

  • Modify: ケアセブン(リハ)/configs/production/site-482693_【A】リハ訪問カレンダー/scripts/1_訪問カレンダー.js
  • Modify: ケアセブン(リハ)/configs/production/site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js

Interfaces:

  • Consumes: apiKey(既存グローバル変数)、shukaiTableId/kobetsuTableId/reigaiTableId既存、Task5で505009側は修正済み

  • Produces: executeCopy(sourceMonth, targetMonth, tableIds) 関数Task 8で使用。tableIds = { sourceShukai, sourceKobetsu, sourceReigai, targetShukai, targetKobetsu, targetReigai }sourceMonth/targetMonth = { year, month }、戻り値はPromise

  • Produces: showCopyModal(direction, currentMonthDate, onConfirm) 関数(direction'toB'または'fromA'onConfirm(otherMonth)が確定時に呼ばれる)

  • Step 1: 日付操作ヘルパーを両ファイルの末尾(openEventData関数の後)に追加する

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////// A/B予定コピー機能 ////////////////////////

// 指定年月の月初0時と翌月月初0時排他的終端を返す
function getMonthRange(year, month) {
    let start = new Date(year, month - 1, 1, 0, 0, 0);
    let end = new Date(year, month, 1, 0, 0, 0);
    return { start: start, end: end };
}

// Pleasanterの日付文字列 'YYYY/MM/DD HH:mm:ss' を組み立てる
function formatPleasanterDate(d) {
    return d.getFullYear() + '/' +
        (d.getMonth() + 1).toString().padStart(2, '0') + '/' +
        d.getDate().toString().padStart(2, '0') + ' ' +
        d.getHours().toString().padStart(2, '0') + ':' +
        d.getMinutes().toString().padStart(2, '0') + ':' +
        d.getSeconds().toString().padStart(2, '0');
}

// 日付文字列を別の年月へシフトする(日はそのまま、月末超過時はその月の末日に丸める)
function shiftDateToMonth(dateStr, targetYear, targetMonth) {
    let d = new Date(dateStr);
    let day = d.getDate();
    let lastDay = new Date(targetYear, targetMonth, 0).getDate();
    if (day > lastDay) day = lastDay;
    let newDate = new Date(targetYear, targetMonth - 1, day, d.getHours(), d.getMinutes(), d.getSeconds());
    return formatPleasanterDate(newDate);
}

// 周回予定レコードが指定月に有効か判定するcreateEvents()の除外判定と同じロジック)
function isActiveInMonth(item, monthRange) {
    const NOT_SET = '1899-12-30T00:00:00';
    let activeStart = (item.DateHash.DateE && item.DateHash.DateE.indexOf(NOT_SET) !== 0) ? new Date(item.DateHash.DateE) : null;
    let activeEnd = (item.DateHash.DateF && item.DateHash.DateF.indexOf(NOT_SET) !== 0) ? new Date(item.DateHash.DateF) : null;
    if (activeStart && monthRange.end.getTime() <= activeStart.getTime()) return false;
    if (activeEnd && monthRange.start.getTime() > activeEnd.getTime()) return false;
    return true;
}
  • Step 2: API呼び出しヘルパーを続けて追加する
function pleasanterApiUrl(idOrSiteId, action) {
    return 'https://' + document.domain + '/pleasanter/api/items/' + idOrSiteId + '/' + action;
}

function apiGetItems(siteId, view) {
    return new Promise(function (resolve, reject) {
        $p.apiGet({
            id: siteId,
            async: true,
            data: {
                ApiVersion: 1.1,
                ApiKey: apiKey,
                View: view
            },
            done: function (data) { resolve(data.Response.Data); },
            fail: function (err) { reject(err); }
        });
    });
}

function apiCreateItem(siteId, data) {
    return $.ajax({
        url: pleasanterApiUrl(siteId, 'create'),
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(Object.assign({ ApiVersion: 1.1, ApiKey: apiKey }, data))
    });
}

function apiDeleteItem(itemId) {
    return $.ajax({
        url: pleasanterApiUrl(itemId, 'delete'),
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({ ApiVersion: 1.1, ApiKey: apiKey })
    });
}
  • Step 3: コピー対象月データの削除処理を追加する
// 指定サイトの対象月・CheckG=trueのレコードのみ削除する
async function deleteTargetMonthData(siteId, dateColumn, range) {
    let filterHash = {};
    filterHash[dateColumn] = '["' + formatPleasanterDate(range.start) + ',' + formatPleasanterDate(range.end) + '"]';
    let items = await apiGetItems(siteId, { ColumnFilterHash: filterHash });
    for (let i = 0; i < items.length; i++) {
        let item = items[i];
        if (!item.CheckHash || item.CheckHash.CheckG !== true) continue;
        await apiDeleteItem(item.ResultId);
    }
}
  • Step 4: 3テーブル分の作成データ組み立て関数を追加する
function buildShukaiCreateBody(item, tgtRange) {
    let c = item.ClassHash;
    let tgtEnd = new Date(tgtRange.end);
    tgtEnd.setDate(tgtEnd.getDate() - 1);
    return {
        ClassHash: {
            ClassA: c.ClassA, ClassC: c.ClassC, ClassD: c.ClassD, ClassE: c.ClassE,
            ClassF: c.ClassF, ClassV: c.ClassV, ClassG: c.ClassG, ClassH: c.ClassH,
            Class001: c.Class001, Class002: c.Class002, Class003: c.Class003,
            Class004: c.Class004, Class005: c.Class005,
            Class010: c.Class010, Class011: c.Class011
        },
        NumHash: { NumA: item.NumHash.NumA, NumB: item.NumHash.NumB },
        DateHash: {
            DateE: formatPleasanterDate(tgtRange.start),
            DateF: formatPleasanterDate(tgtEnd),
            Date001: item.DateHash.Date001, Date002: item.DateHash.Date002,
            Date003: item.DateHash.Date003, Date004: item.DateHash.Date004,
            Date005: item.DateHash.Date005
        },
        CheckHash: { CheckG: true },
        Body: item.Body
    };
}

function buildKobetsuCreateBody(item, targetMonth) {
    let c = item.ClassHash;
    return {
        ClassHash: {
            ClassA: c.ClassA, ClassF: c.ClassF, ClassE: c.ClassE, ClassB: c.ClassB,
            ClassV: c.ClassV, ClassG: c.ClassG, Class011: c.Class011
        },
        NumHash: { NumA: item.NumHash.NumA, NumB: item.NumHash.NumB },
        DateHash: {
            DateA: shiftDateToMonth(item.DateHash.DateA, targetMonth.year, targetMonth.month)
        },
        CheckHash: { CheckG: true, CheckA: item.CheckHash.CheckA },
        Body: item.Body
    };
}

function buildReigaiCreateBody(item, targetMonth) {
    let c = item.ClassHash;
    return {
        ClassHash: { ClassA: c.ClassA, ClassK: c.ClassK },
        DateHash: {
            DateB: shiftDateToMonth(item.DateHash.DateB, targetMonth.year, targetMonth.month)
        },
        CheckHash: { CheckG: true }
    };
}
  • Step 5: コピー実行メイン関数を追加する
// sourceMonth/targetMonth: { year, month }
// tableIds: { sourceShukai, sourceKobetsu, sourceReigai, targetShukai, targetKobetsu, targetReigai }
async function executeCopy(sourceMonth, targetMonth, tableIds) {
    let srcRange = getMonthRange(sourceMonth.year, sourceMonth.month);
    let tgtRange = getMonthRange(targetMonth.year, targetMonth.month);

    await deleteTargetMonthData(tableIds.targetShukai, 'DateE', tgtRange);
    await deleteTargetMonthData(tableIds.targetKobetsu, 'DateA', tgtRange);
    await deleteTargetMonthData(tableIds.targetReigai, 'DateB', tgtRange);

    let shukaiItems = await apiGetItems(tableIds.sourceShukai, {});
    for (let i = 0; i < shukaiItems.length; i++) {
        if (!isActiveInMonth(shukaiItems[i], srcRange)) continue;
        await apiCreateItem(tableIds.targetShukai, buildShukaiCreateBody(shukaiItems[i], tgtRange));
    }

    let kobetsuFilter = { DateA: '["' + formatPleasanterDate(srcRange.start) + ',' + formatPleasanterDate(srcRange.end) + '"]' };
    let kobetsuItems = await apiGetItems(tableIds.sourceKobetsu, { ColumnFilterHash: kobetsuFilter });
    for (let i = 0; i < kobetsuItems.length; i++) {
        await apiCreateItem(tableIds.targetKobetsu, buildKobetsuCreateBody(kobetsuItems[i], targetMonth));
    }

    let reigaiFilter = { DateB: '["' + formatPleasanterDate(srcRange.start) + ',' + formatPleasanterDate(srcRange.end) + '"]' };
    let reigaiItems = await apiGetItems(tableIds.sourceReigai, { ColumnFilterHash: reigaiFilter });
    for (let i = 0; i < reigaiItems.length; i++) {
        await apiCreateItem(tableIds.targetReigai, buildReigaiCreateBody(reigaiItems[i], targetMonth));
    }
}
  • Step 6: コピー用モーダルUIを追加する
// direction: 'toB'A画面から。コピー元=表示中月固定、モーダルでコピー先月を選ぶ)
//            'fromA'B画面から。コピー先=表示中月固定、モーダルでコピー元月を選ぶ)
function showCopyModal(direction, currentMonthDate, onConfirm) {
    if ($('#copyModalOverlay')[0]) $('#copyModalOverlay').remove();

    let fixedMonthLabel = currentMonthDate.getFullYear() + '年' + (currentMonthDate.getMonth() + 1) + '月';

    let overlay = document.createElement('div');
    overlay.id = 'copyModalOverlay';
    overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);z-index:9999;display:flex;align-items:center;justify-content:center;';

    let box = document.createElement('div');
    box.style.cssText = 'background:#fff;padding:20px;border-radius:6px;min-width:320px;box-shadow:0 2px 10px rgba(0,0,0,0.3);';

    let title = document.createElement('div');
    title.style.cssText = 'font-weight:bold;font-size:1.2em;margin-bottom:10px;';
    title.textContent = direction === 'toB' ? '【B】へコピー' : '【A】からコピー';
    box.appendChild(title);

    let desc = document.createElement('div');
    desc.style.marginBottom = '10px';
    desc.textContent = direction === 'toB'
        ? ('コピー元A、表示中: ' + fixedMonthLabel)
        : ('コピー先B、表示中: ' + fixedMonthLabel);
    box.appendChild(desc);

    let selectLabel = document.createElement('div');
    selectLabel.textContent = direction === 'toB' ? 'コピー先Bの月を選択:' : 'コピー元Aの月を選択:';
    box.appendChild(selectLabel);

    let select = document.createElement('select');
    select.id = 'copyModalMonthSelect';
    for (let i = -2; i <= 6; i++) {
        let d = new Date();
        d.setDate(1);
        d.setMonth(d.getMonth() + i);
        let opt = document.createElement('option');
        opt.value = d.getFullYear() + '-' + (d.getMonth() + 1);
        opt.textContent = d.getFullYear() + '年' + (d.getMonth() + 1) + '月';
        select.appendChild(opt);
    }
    box.appendChild(select);

    let btnArea = document.createElement('div');
    btnArea.style.marginTop = '15px';
    btnArea.style.textAlign = 'right';

    let cancelBtn = document.createElement('button');
    cancelBtn.textContent = 'キャンセル';
    cancelBtn.style.marginRight = '8px';
    cancelBtn.onclick = function () { $('#copyModalOverlay').remove(); };

    let okBtn = document.createElement('button');
    okBtn.textContent = 'この内容でコピーする';
    okBtn.onclick = function () {
        let val = select.value.split('-');
        let otherMonth = { year: Number(val[0]), month: Number(val[1]) };
        let confirmMsg = direction === 'toB'
            ? ('A『' + fixedMonthLabel + '』の予定を、B『' + otherMonth.year + '年' + otherMonth.month + '月』へコピーします。\nB側の同月・A由来の既存予定は削除されます。よろしいですか')
            : ('A『' + otherMonth.year + '年' + otherMonth.month + '月』の予定を、B『' + fixedMonthLabel + '』へコピーします。\nB側の同月・A由来の既存予定は削除されます。よろしいですか');
        if (!confirm(confirmMsg)) return;
        $('#copyModalOverlay').remove();
        onConfirm(otherMonth);
    };

    btnArea.appendChild(cancelBtn);
    btnArea.appendChild(okBtn);
    box.appendChild(btnArea);

    overlay.appendChild(box);
    document.body.appendChild(overlay);
}
  • Step 7: 両ファイルへ同一内容でStep1〜6を追加したことを確認する
cd "/c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter/ケアセブン(リハ)/configs/production" && \
diff <(sed -n '/A\/B予定コピー機能/,$p' "site-482693_【A】リハ訪問カレンダー/scripts/1_訪問カレンダー.js") \
     <(sed -n '/A\/B予定コピー機能/,$p' "site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js")

Expected: 差分なし(コピー機能コアロジック部分は両ファイルで完全一致する)。


Task 8: コピーボタンの結線【A】画面・【B】画面

Files:

  • Modify: ケアセブン(リハ)/configs/production/site-482693_【A】リハ訪問カレンダー/scripts/1_訪問カレンダー.jsinitialize()内buttonArea構築部
  • Modify: ケアセブン(リハ)/configs/production/site-505009_【B】リハ訪問カレンダー/scripts/1_訪問カレンダー.js(同上)

Interfaces:

  • Consumes: Task6のbuttonArea/button0要素、Task7のshowCopyModal/executeCopy

  • Step 1: 【A】側(482693)、Task6で追加したbuttonArea.append(button0);の直後にコピーボタン追加

    buttonArea.append(buttonSelf);
    buttonArea.append(buttonOther);
    buttonArea.append(button0);

    let buttonCopy = document.createElement('button');
    buttonCopy.type = 'button';
    buttonCopy.id = 'buttonCopy';
    buttonCopy.innerHTML = '▶Bへコピー';
    buttonCopy.onclick = function () {
        let nowdate = new Date(document.getElementById('nowdate').getAttribute('nowdate'));
        showCopyModal('toB', nowdate, function (otherMonth) {
            let sourceMonth = { year: nowdate.getFullYear(), month: nowdate.getMonth() + 1 };
            executeCopy(sourceMonth, otherMonth, {
                sourceShukai: shukaiTableId, sourceKobetsu: kobetsuTableId, sourceReigai: reigaiTableId,
                targetShukai: '505010', targetKobetsu: '505011', targetReigai: '505012'
            }).then(function () {
                alert('コピーが完了しました。');
            }).catch(function (err) {
                console.error(err);
                alert('コピー中にエラーが発生しました。');
            });
        });
    };
    buttonArea.append(buttonCopy);

    buttonArea.append(selectMonth);
  • Step 2: 【B】側(505009)、同じ位置に対称のコピーボタンを追加
    buttonArea.append(buttonSelf);
    buttonArea.append(buttonOther);
    buttonArea.append(button0);

    let buttonCopy = document.createElement('button');
    buttonCopy.type = 'button';
    buttonCopy.id = 'buttonCopy';
    buttonCopy.innerHTML = '▶Aからコピー';
    buttonCopy.onclick = function () {
        let nowdate = new Date(document.getElementById('nowdate').getAttribute('nowdate'));
        showCopyModal('fromA', nowdate, function (otherMonth) {
            let targetMonth = { year: nowdate.getFullYear(), month: nowdate.getMonth() + 1 };
            executeCopy(otherMonth, targetMonth, {
                sourceShukai: '482696', sourceKobetsu: '482701', sourceReigai: '482700',
                targetShukai: shukaiTableId, targetKobetsu: kobetsuTableId, targetReigai: reigaiTableId
            }).then(function () {
                alert('コピーが完了しました。');
            }).catch(function (err) {
                console.error(err);
                alert('コピー中にエラーが発生しました。');
            });
        });
    };
    buttonArea.append(buttonCopy);

    buttonArea.append(selectMonth);
  • Step 3: コピーボタンのスタイル設定を追加する

両ファイルのTask6 Step4で追加した$('#buttonOther').button();の直後に追加:

    $('#buttonOther').button();

    ///////////////////
    $('#buttonCopy').css({
        'margin': '0 5px 5px 0',
        'font-size': '1.5em',
    });
    $('#buttonCopy').button();

Task 9: 反映と総合動作確認

Files:

  • 反映対象: 482693/505009のscripts/1_訪問カレンダー.js(部分更新 updatesitesettingsbuild-desired-config.jsapply-desired-config-full.js

  • Step 1: 482693の反映用JSON生成ドライラン

cd "/c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter" && node .claude/js/build-desired-config.js --project="ケアセブン(リハ)" --env=production site-482693
node .claude/js/apply-desired-config-full.js --project="ケアセブン(リハ)" --env=production site-482693

Expected: 差分プレビュー・送信Bodyが表示され、scripts配下1_訪問カレンダー.jsの変更が反映対象として表示される。エラーなし。

  • Step 2: 505009の反映用JSON生成ドライラン
cd "/c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter" && node .claude/js/build-desired-config.js --project="ケアセブン(リハ)" --env=production site-505009
node .claude/js/apply-desired-config-full.js --project="ケアセブン(リハ)" --env=production site-505009

Expected: 同上。

  • Step 3: プレビュー内容をユーザーに提示し、実行許可を得る

両サイトの差分プレビューをユーザーへ提示し、実送信の許可を得るまで次に進まない。

  • Step 4: 承認後、両サイトへ実送信する
cd "/c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter" && node .claude/js/apply-desired-config-full.js --project="ケアセブン(リハ)" --env=production site-482693 --execute
node .claude/js/apply-desired-config-full.js --project="ケアセブン(リハ)" --env=production site-505009 --execute

Expected: 両サイトともHTTP 200・成功メッセージ。

  • Step 5: 再取得して反映を確認する
cd "/c/Users/k.nogi/#GitHub/ken_nogi/ClaudePleasanter" && node .claude/js/get-site-config.js --project="ケアセブン(リハ)" --env=production

siteid.jsonは既に17サイト分の最終形になっている前提。全件再取得される

  • Step 6: ブラウザで総合動作確認

以下を本番環境(https://nextoffice.next-hd.co.jp/pleasanter/items/482693/index/ および .../505009/index/)で確認する:

  1. 【A】画面: 【A】【B】切替ボタンが表示され、【A】がハイライトされている
  2. 【A】画面で「▶Bへコピー」→モーダルで適当な月を選択→確認ダイアログ→実行→「コピーが完了しました」表示
  3. 【B】画面へ切替ボタンで遷移し、コピーした月に予定が表示されることを確認
  4. 【B】画面で「▶Aからコピー」→モーダルで【A】側の月を選択→実行→正しく反映されることを確認
  5. 【B】画面で手動で1件予定を追加し、再度コピー実行してもその手動追加分が消えないことを確認A由来フラグの削除範囲限定が機能していることの確認
  • Step 7: modify記録とsummary作成

configs/production/site-482693_【A】リハ訪問カレンダー/modify/{リクエストラベル}/新規作成へ、Task5〜9の変更前後ファイル・反映結果・summary_{timestamp}.mdを保存する。505009側も同様に記録する。


Self-Review メモ

  • Spec coverage: design doc「2.1 独立動作化」→Task5、「2.2 A/B切替ボタン」→Task6、「2.3 予定コピー機能」抽出条件・書き込み内容・A由来フラグ・UI操作フロー・実装方式→Task2-4, 7-8、「未確定事項」→Task1で全て解消。
  • Placeholder scan: 各ステップに完全なコード・コマンドを記載済み。「TBD」「後で実装」等は含まれない。
  • Type consistency: executeCopy(sourceMonth, targetMonth, tableIds)のシグネチャはTask7 Step5定義・Task8 Step1/2呼び出しで一致。showCopyModal(direction, currentMonthDate, onConfirm)も同様。tableIdsのキー名(sourceShukaiもTask7/8間で一致。