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>
659 lines
33 KiB
Markdown
659 lines
33 KiB
Markdown
# 【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.js` → `apply-desired-config-full.js`(部分更新 `updatesitesettings`)で反映する。
|
||
|
||
**Tech Stack:** Pleasanter(ASP.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`)送信時は、取得済み生JSON(`getsite`レスポンス)を丸ごとベースにコピーし変更箇所だけ差し替える。`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サイト共通):**
|
||
```json
|
||
{"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: 冒頭の定数を修正する**
|
||
|
||
修正前:
|
||
```javascript
|
||
//本番用
|
||
let kokyakuTableId = '482703';
|
||
let resourceTableId = '482697';
|
||
let reigaiTableId = '482700';
|
||
let shukaiTableId = '482696';
|
||
let kobetsuTableId = '482701';
|
||
let holidayTableId = '482695';
|
||
```
|
||
|
||
修正後:
|
||
```javascript
|
||
//本番用
|
||
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';` の直後に追加:
|
||
|
||
```javascript
|
||
let holidayTableId = '482695';
|
||
|
||
//A/B切替・コピー機能用
|
||
let selfSiteId = '482693';
|
||
let selfLabel = 'A';
|
||
let otherSiteId = '505009';
|
||
let otherLabel = 'B';
|
||
```
|
||
|
||
- [ ] **Step 2: 【B】側(505009)にも同様に追加する**
|
||
|
||
`let holidayTableId = '482695';` の直後に追加:
|
||
|
||
```javascript
|
||
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共通):
|
||
```javascript
|
||
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で定義済みの値を使う):
|
||
```javascript
|
||
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();`の直後(両ファイル共通)に追加:
|
||
```javascript
|
||
$('#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`関数の後)に追加する**
|
||
|
||
```javascript
|
||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
//////////////////////// 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呼び出しヘルパーを続けて追加する**
|
||
|
||
```javascript
|
||
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: コピー対象月データの削除処理を追加する**
|
||
|
||
```javascript
|
||
// 指定サイトの対象月・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テーブル分の作成データ組み立て関数を追加する**
|
||
|
||
```javascript
|
||
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: コピー実行メイン関数を追加する**
|
||
|
||
```javascript
|
||
// 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を追加する**
|
||
|
||
```javascript
|
||
// 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を追加したことを確認する**
|
||
|
||
```bash
|
||
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_訪問カレンダー.js`(`initialize()`内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);`の直後にコピーボタン追加**
|
||
|
||
```javascript
|
||
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)、同じ位置に対称のコピーボタンを追加**
|
||
|
||
```javascript
|
||
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();`の直後に追加:
|
||
|
||
```javascript
|
||
$('#buttonOther').button();
|
||
|
||
///////////////////
|
||
$('#buttonCopy').css({
|
||
'margin': '0 5px 5px 0',
|
||
'font-size': '1.5em',
|
||
});
|
||
$('#buttonCopy').button();
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: 反映と総合動作確認
|
||
|
||
**Files:**
|
||
- 反映対象: 482693/505009の`scripts/1_訪問カレンダー.js`(部分更新 `updatesitesettings`、`build-desired-config.js`→`apply-desired-config-full.js`)
|
||
|
||
- [ ] **Step 1: 482693の反映用JSON生成(ドライラン)**
|
||
|
||
```bash
|
||
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生成(ドライラン)**
|
||
|
||
```bash
|
||
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: 承認後、両サイトへ実送信する**
|
||
|
||
```bash
|
||
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: 再取得して反映を確認する**
|
||
|
||
```bash
|
||
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間で一致。
|