# gas/ — Apps Script 連携スクリプト

Beene業務ポータルの外部連携（Google Workspace側）を担当する GAS 群。

| ファイル | 役割 |
|---|---|
| `setup_masters.gs` | スプシのマスタ15シートを一括生成 |
| `setup_forms.gs` | 施術記録／物販／会員入退会の3フォーム生成 |
| `aggregate.gs` | 取引明細→月次サマリJSON出力＋Webhook |
| `sync.gs` | スタッフ個人原本シート → ポータル staff JSON 同期 |
| `sync_evaluations.gs` | 人事評価シート同期 |
| `backup_notify.gs` | バックアップと通知 |
| **`sync_calendar.gs`** | **`data/imports/appointments.json` → 店舗別Googleカレンダー同期** |
| **`sync_threads.gs`** | **Threads API → 3アカウントの投稿エンゲージ収集（シート蓄積＋ポータル配信）。設定は `docs/threads-setup.md`** |

---

## sync_calendar.gs セットアップ

### 1. プロジェクト作成
[script.google.com](https://script.google.com) で新規プロジェクト → `sync_calendar.gs` の中身を貼付。
（既存の Beene プロジェクトに追加してもOK）

### 2. 設定値を埋める
ファイル冒頭の以下を確認：
```javascript
const PORTAL_BASE_URL = 'https://beene-gyomu-portal.pages.dev';  // 既に正しい
const BASIC_AUTH_USER = 'beene';
const BASIC_AUTH_PASS = 'beene-gyomu-2026';  // functions/_middleware.js と一致

const STORE_CALENDARS = {
  // 'beene-sapporo-kotoni': 'xxxxx@group.calendar.google.com',
  // 'beene-akita':          'xxxxx@group.calendar.google.com',
  // ...
};
```

### 3. 店舗ごとのカレンダー作成
[Google カレンダー](https://calendar.google.com)で店舗1つにつき1つカレンダーを作成：
1. 左サイドバー「他のカレンダー」→ `+` → 「新しいカレンダーを作成」
2. 名前例：`Beene秋田 予約台帳`
3. 作成後、設定画面 → 「カレンダーの統合」 → **カレンダーID**（`...@group.calendar.google.com`）をコピー
4. `STORE_CALENDARS` の対応する store_id に貼付

**店舗ID一覧**（`data/schema/stores.json` より）：
- `beene-sapporo-kotoni` … Beene札幌琴似
- `beene-akita` … Beene秋田
- `beene-shibuya` … Beene渋谷
- `halicht-yokote` … HALicht横手
- `harislim-fc` … Hari Slim FC

### 4. 権限承認
GASエディタで `syncCalendarTest` を選択 → 実行ボタン → 初回は権限承認画面：
- Googleカレンダー（読み書き）
- 外部URLへのアクセス（UrlFetchApp）

承認後、ログ画面（表示 → ログ）で結果を確認。

### 5. 自動実行トリガー
左メニュー「トリガー」→ 「トリガーを追加」：
- 関数: `syncCalendarAll`
- イベントソース: 時間主導型
- 時間ベースのトリガー: 分ベースのタイマー / 15分おき

これで import.py → デプロイ → 15分以内にカレンダー反映の流れが回ります。

### 6. ポータルから状態確認用 Webhook（任意）
左上「デプロイ」→ 「ウェブアプリとしてデプロイ」：
- 実行ユーザー: 自分
- アクセス: `Workspace内の全員`（または自分のみ）
- 公開URL（`/exec`）をコピー

ポータル側 `appointments.html` の同期状態カードがこのURLを叩いて最終結果を表示します（次工程で実装）。

---

## 仕組み（重要）

- **冪等同期**：リピット予約番号 (`external_reservation_id`) を `event.setTag()` に格納し、それを冪等キーに使用
- **削除も同期**：取り込みデータから消えた予約 = キャンセル扱いでイベント削除
- **±60日の窓**：パフォーマンス上、走査範囲を前後60日に限定
- **キャンセル予約**：`status = cancelled / no_show` も削除対象

## トラブル対応

| 症状 | 原因と対策 |
|---|---|
| `Calendar not found` | `STORE_CALENDARS` のカレンダーIDが間違い、または共有権限が無い |
| `Portal fetch failed: 401` | Basic Auth ユーザー名／パスワードが `functions/_middleware.js` と不一致 |
| `Portal fetch failed: 404` | `data/imports/appointments.json` が未生成。まず import.py を実行 |
| イベントが重複作成された | 既存イベントに `EXT_ID_PROP` タグが無い（手動作成分）。一度全削除して再同期で解消 |
| 同期されない店舗がある | `STORE_CALENDARS` に該当 store_id が未登録、またはカレンダーIDが空 |
