Microsoft SharePoint

コラボレーションドキュメント管理MicrosoftTeams統合イントラネットワークフロー

コラボレーションツール

Microsoft SharePoint

概要

Microsoft SharePointは、Microsoft提供の企業向けコラボレーション・コンテンツ管理プラットフォームです。ドキュメント管理、イントラネット構築、ワークフロー自動化、Office 365との完全統合を提供。Microsoft環境の大企業での標準採用となっており、Teams統合により現代的なコラボレーション体験を実現。レガシーシステムとの互換性を保ちながら進化し、2024-2025年にはAI機能、自動化ワークフロー、高度なドキュメント管理機能を大幅強化しています。

詳細

Microsoft SharePoint 2025年版は、AIとMicrosoft 365エコシステムの完全統合により、エンタープライズコラボレーションの最前線に立っています。SharePoint Copilot AIアシスタントによる自動タスク処理、コンテンツ分析、インサイト生成により生産性が劇的に向上。Teams、Outlook、OneDrive、Power Platform、Viva Suiteとのシームレス統合により、統合されたデジタルワークスペースを提供。2024年の主要アップデートには、AI駆動のドキュメント管理、スマートバージョン管理、電子署名統合(DocuSign、Adobe Acrobat Sign)、カスタムテーマ・フォント対応、高度なワークフロー自動化が含まれます。エンタープライズセキュリティ、コンプライアンス、ガバナンス機能により、大規模組織の厳格な要件に対応しています。

主な特徴

  • Teams完全統合: ファイル管理とコミュニケーションの統合プラットフォーム
  • AI駆動の自動化: SharePoint Copilotによるスマートコンテンツ管理
  • 高度なワークフロー: Power Automate統合による業務プロセス自動化
  • エンタープライズセキュリティ: 高度な権限管理とコンプライアンス機能
  • カスタマイズ性: カスタムテーマ、フォント、レイアウト対応
  • バージョン管理: スマートな版数管理とストレージ最適化

メリット・デメリット

メリット

  • Microsoft 365エコシステムとの完璧な統合によるワークフロー効率化
  • 強力なエンタープライズセキュリティとコンプライアンス機能
  • AIによる自動化とインテリジェントなコンテンツ管理
  • 大規模組織対応のスケーラビリティと安定性
  • 豊富なテンプレートとベストプラクティスの実装
  • Teams統合による現代的なコラボレーション体験
  • Power Platform統合による高度なカスタマイズ
  • 既存Microsoftインフラとの親和性

デメリット

  • 高額なライセンス費用(特に大規模組織)
  • 複雑な設定と管理による高い運用コスト
  • Microsoft環境以外との統合制限
  • 従来的なUI/UXによる学習コスト
  • オンプレミス版の段階的廃止によるクラウド移行圧力
  • カスタマイズには高度な技術知識が必要
  • 大量データ処理時のパフォーマンス課題
  • ベンダーロックインのリスク

参考ページ

書き方の例

基本設定とサイト構築

# SharePoint Online基本セットアップ
# 1. Microsoft 365管理センターでのSharePointライセンス確認
# 2. SharePoint管理センターでのサイト作成
# 3. Teams統合の設定
# 4. 権限とセキュリティ設定

# 基本的なサイト構造例
企業イントラネット/
├── ホーム/
│   ├── お知らせ
│   ├── 企業ニュース
│   └── イベント情報
├── 部門サイト/
│   ├── 人事部/
│   ├── 営業部/
│   └── 開発部/
├── プロジェクトサイト/
│   ├── プロジェクトA/
│   ├── プロジェクトB/
│   └── アーカイブ/
└── ナレッジベース/
    ├── 技術文書/
    ├── 業務マニュアル/
    └── FAQ/

SharePoint REST API基本操作

// SharePoint REST API基本設定
class SharePointAPI {
  constructor(siteUrl, accessToken) {
    this.siteUrl = siteUrl.replace(/\/$/, ''); // 末尾のスラッシュを除去
    this.accessToken = accessToken;
    this.headers = {
      'Authorization': `Bearer ${accessToken}`,
      'Accept': 'application/json; odata=verbose',
      'Content-Type': 'application/json; odata=verbose',
    };
  }

  // リスト一覧取得
  async getLists() {
    try {
      const response = await fetch(`${this.siteUrl}/_api/web/lists`, {
        method: 'GET',
        headers: this.headers,
      });

      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }

      const data = await response.json();
      return data.d.results;
    } catch (error) {
      console.error('リスト取得エラー:', error);
      throw error;
    }
  }

  // リストアイテム取得
  async getListItems(listTitle, selectFields = '', filterQuery = '', orderBy = '') {
    try {
      let apiUrl = `${this.siteUrl}/_api/web/lists/getbytitle('${listTitle}')/items`;
      
      const params = new URLSearchParams();
      if (selectFields) params.append('$select', selectFields);
      if (filterQuery) params.append('$filter', filterQuery);
      if (orderBy) params.append('$orderby', orderBy);
      
      if (params.toString()) {
        apiUrl += `?${params.toString()}`;
      }

      const response = await fetch(apiUrl, {
        method: 'GET',
        headers: this.headers,
      });

      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }

      const data = await response.json();
      return data.d.results;
    } catch (error) {
      console.error('リストアイテム取得エラー:', error);
      throw error;
    }
  }

  // リストアイテム作成
  async createListItem(listTitle, itemData) {
    try {
      // Request Digestを取得
      const digestResponse = await fetch(`${this.siteUrl}/_api/contextinfo`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.accessToken}`,
          'Accept': 'application/json; odata=verbose',
        },
      });

      if (!digestResponse.ok) {
        throw new Error(`Digest取得エラー: ${digestResponse.status}`);
      }

      const digestData = await digestResponse.json();
      const requestDigest = digestData.d.GetContextWebInformation.FormDigestValue;

      // アイテム作成
      const response = await fetch(`${this.siteUrl}/_api/web/lists/getbytitle('${listTitle}')/items`, {
        method: 'POST',
        headers: {
          ...this.headers,
          'X-RequestDigest': requestDigest,
        },
        body: JSON.stringify(itemData),
      });

      if (!response.ok) {
        throw new Error(`アイテム作成エラー: ${response.status}`);
      }

      const data = await response.json();
      return data.d;
    } catch (error) {
      console.error('アイテム作成エラー:', error);
      throw error;
    }
  }

  // リストアイテム更新
  async updateListItem(listTitle, itemId, itemData) {
    try {
      // Request Digestを取得
      const digestResponse = await fetch(`${this.siteUrl}/_api/contextinfo`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.accessToken}`,
          'Accept': 'application/json; odata=verbose',
        },
      });

      const digestData = await digestResponse.json();
      const requestDigest = digestData.d.GetContextWebInformation.FormDigestValue;

      // アイテム更新
      const response = await fetch(`${this.siteUrl}/_api/web/lists/getbytitle('${listTitle}')/items(${itemId})`, {
        method: 'POST',
        headers: {
          ...this.headers,
          'X-RequestDigest': requestDigest,
          'X-HTTP-Method': 'MERGE',
          'IF-MATCH': '*',
        },
        body: JSON.stringify(itemData),
      });

      if (!response.ok) {
        throw new Error(`アイテム更新エラー: ${response.status}`);
      }

      return true;
    } catch (error) {
      console.error('アイテム更新エラー:', error);
      throw error;
    }
  }

  // ファイルアップロード
  async uploadFile(libraryName, fileName, fileContent) {
    try {
      // Request Digestを取得
      const digestResponse = await fetch(`${this.siteUrl}/_api/contextinfo`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.accessToken}`,
          'Accept': 'application/json; odata=verbose',
        },
      });

      const digestData = await digestResponse.json();
      const requestDigest = digestData.d.GetContextWebInformation.FormDigestValue;

      // ファイルアップロード
      const response = await fetch(
        `${this.siteUrl}/_api/web/GetFolderByServerRelativeUrl('${libraryName}')/Files/add(url='${fileName}',overwrite=true)`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.accessToken}`,
            'Accept': 'application/json; odata=verbose',
            'X-RequestDigest': requestDigest,
          },
          body: fileContent,
        }
      );

      if (!response.ok) {
        throw new Error(`ファイルアップロードエラー: ${response.status}`);
      }

      const data = await response.json();
      return data.d;
    } catch (error) {
      console.error('ファイルアップロードエラー:', error);
      throw error;
    }
  }

  // 検索実行
  async search(query, selectProperties = '', rowLimit = 50) {
    try {
      const searchUrl = `${this.siteUrl}/_api/search/query?querytext='${encodeURIComponent(query)}'&selectproperties='${selectProperties}'&rowlimit=${rowLimit}`;

      const response = await fetch(searchUrl, {
        method: 'GET',
        headers: this.headers,
      });

      if (!response.ok) {
        throw new Error(`検索エラー: ${response.status}`);
      }

      const data = await response.json();
      return data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
    } catch (error) {
      console.error('検索エラー:', error);
      throw error;
    }
  }
}

// 使用例
const sharepoint = new SharePointAPI(
  'https://yourcompany.sharepoint.com/sites/yoursite',
  'your-access-token'
);

// タスクリスト管理の例
async function manageTaskList() {
  try {
    // 新しいタスク作成
    const newTask = {
      __metadata: { type: 'SP.Data.TasksListItem' },
      Title: 'プロジェクト資料作成',
      Description: 'Q1プロジェクトの資料を作成する',
      AssignedTo: { __metadata: { type: 'SP.Data.UserGroupValue' }, LookupId: 12 },
      DueDate: '2025-01-31T00:00:00Z',
      Priority: '高',
      Status: '進行中',
    };

    const createdTask = await sharepoint.createListItem('Tasks', newTask);
    console.log('タスク作成完了:', createdTask.Title);

    // タスク一覧取得(期限でソート)
    const tasks = await sharepoint.getListItems(
      'Tasks',
      'ID,Title,Description,DueDate,Priority,Status',
      "Status ne '完了'",
      'DueDate asc'
    );

    console.log('未完了タスク:', tasks.length);
    tasks.forEach(task => {
      console.log(`- ${task.Title} (期限: ${task.DueDate})`);
    });

    // 期限が迫っているタスクの更新
    const urgentTasks = tasks.filter(task => {
      const dueDate = new Date(task.DueDate);
      const today = new Date();
      const diffTime = dueDate.getTime() - today.getTime();
      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
      return diffDays <= 3 && diffDays >= 0;
    });

    for (const task of urgentTasks) {
      await sharepoint.updateListItem('Tasks', task.ID, {
        __metadata: { type: 'SP.Data.TasksListItem' },
        Priority: '緊急',
      });
      console.log(`タスク "${task.Title}" を緊急に変更`);
    }

  } catch (error) {
    console.error('タスク管理エラー:', error);
  }
}

// ドキュメント検索の例
async function searchDocuments() {
  try {
    const searchResults = await sharepoint.search(
      'プロジェクト AND 仕様書',
      'Title,Path,Author,LastModifiedTime',
      20
    );

    console.log('検索結果:');
    searchResults.forEach(result => {
      const cells = result.Cells.results;
      const title = cells.find(c => c.Key === 'Title')?.Value || 'タイトル不明';
      const path = cells.find(c => c.Key === 'Path')?.Value || '';
      const author = cells.find(c => c.Key === 'Author')?.Value || '作成者不明';
      
      console.log(`- ${title} (作成者: ${author})`);
      console.log(`  パス: ${path}`);
    });

  } catch (error) {
    console.error('文書検索エラー:', error);
  }
}

Power Automate統合とワークフロー自動化

// Power Automate(旧Flow)との統合例
class SharePointWorkflow {
  constructor(siteUrl, accessToken) {
    this.sharepoint = new SharePointAPI(siteUrl, accessToken);
    this.flowApiUrl = 'https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple';
  }

  // 承認ワークフロートリガー
  async triggerApprovalWorkflow(documentUrl, approvers, requestMessage) {
    try {
      // SharePoint上のドキュメント情報取得
      const fileInfo = await this.getFileInfo(documentUrl);
      
      // Power Automate HTTPトリガーの呼び出し
      const workflowData = {
        documentTitle: fileInfo.Title,
        documentUrl: documentUrl,
        requestedBy: fileInfo.Author.Title,
        approvers: approvers,
        message: requestMessage,
        timestamp: new Date().toISOString(),
      };

      // カスタムHTTPトリガーエンドポイント
      const response = await fetch('https://prod-xx.eastus.logic.azure.com:443/workflows/xxxxx/triggers/manual/paths/invoke', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(workflowData),
      });

      if (!response.ok) {
        throw new Error(`ワークフロートリガーエラー: ${response.status}`);
      }

      const result = await response.json();
      return result;

    } catch (error) {
      console.error('承認ワークフローエラー:', error);
      throw error;
    }
  }

  // ドキュメント情報取得
  async getFileInfo(documentUrl) {
    try {
      // URLからサーバー相対パスを取得
      const url = new URL(documentUrl);
      const serverRelativeUrl = url.pathname;

      const response = await fetch(
        `${this.sharepoint.siteUrl}/_api/web/GetFileByServerRelativeUrl('${serverRelativeUrl}')?$expand=Author,CheckedOutByUser,ModifiedBy`,
        {
          method: 'GET',
          headers: this.sharepoint.headers,
        }
      );

      if (!response.ok) {
        throw new Error(`ファイル情報取得エラー: ${response.status}`);
      }

      const data = await response.json();
      return data.d;

    } catch (error) {
      console.error('ファイル情報取得エラー:', error);
      throw error;
    }
  }

  // 定期レポート生成ワークフロー
  async scheduleWeeklyReport(listName, reportRecipients) {
    try {
      // 今週のデータ取得
      const oneWeekAgo = new Date();
      oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
      const isoDate = oneWeekAgo.toISOString();

      const weeklyData = await this.sharepoint.getListItems(
        listName,
        'ID,Title,Created,Modified,Author/Title',
        `Created ge datetime'${isoDate}'`,
        'Created desc'
      );

      // レポートデータ集計
      const reportData = {
        period: `${oneWeekAgo.toLocaleDateString()} - ${new Date().toLocaleDateString()}`,
        totalItems: weeklyData.length,
        itemsByDay: this.groupByDay(weeklyData),
        topContributors: this.getTopContributors(weeklyData),
        summary: this.generateSummary(weeklyData),
      };

      // Power Automate経由でメール送信
      const emailData = {
        to: reportRecipients,
        subject: `週次レポート - ${listName} (${reportData.period})`,
        body: this.generateEmailBody(reportData),
        isHtml: true,
      };

      // Email送信トリガー
      const response = await fetch('https://prod-xx.eastus.logic.azure.com:443/workflows/xxxxx/triggers/manual/paths/invoke', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(emailData),
      });

      return reportData;

    } catch (error) {
      console.error('週次レポートエラー:', error);
      throw error;
    }
  }

  // 日別グループ化
  groupByDay(items) {
    return items.reduce((groups, item) => {
      const date = new Date(item.Created).toLocaleDateString();
      if (!groups[date]) {
        groups[date] = [];
      }
      groups[date].push(item);
      return groups;
    }, {});
  }

  // トップ貢献者の取得
  getTopContributors(items) {
    const contributors = items.reduce((counts, item) => {
      const author = item.Author ? item.Author.Title : '不明';
      counts[author] = (counts[author] || 0) + 1;
      return counts;
    }, {});

    return Object.entries(contributors)
      .sort(([,a], [,b]) => b - a)
      .slice(0, 5)
      .map(([name, count]) => ({ name, count }));
  }

  // サマリー生成
  generateSummary(items) {
    return {
      averagePerDay: (items.length / 7).toFixed(1),
      peakDay: this.getPeakDay(items),
      categories: this.getCategoryDistribution(items),
    };
  }

  // メール本文生成
  generateEmailBody(reportData) {
    return `
      <h2>週次レポート - ${reportData.period}</h2>
      
      <h3>概要</h3>
      <ul>
        <li>総件数: ${reportData.totalItems}件</li>
        <li>1日平均: ${reportData.summary.averagePerDay}件</li>
        <li>最も活発な日: ${reportData.summary.peakDay}</li>
      </ul>
      
      <h3>トップ貢献者</h3>
      <ol>
        ${reportData.topContributors.map(c => `<li>${c.name}: ${c.count}件</li>`).join('')}
      </ol>
      
      <h3>日別内訳</h3>
      <table border="1" style="border-collapse: collapse;">
        <tr><th>日付</th><th>件数</th></tr>
        ${Object.entries(reportData.itemsByDay).map(([date, items]) => 
          `<tr><td>${date}</td><td>${items.length}</td></tr>`
        ).join('')}
      </table>
    `;
  }
}

// 使用例
const workflow = new SharePointWorkflow(
  'https://yourcompany.sharepoint.com/sites/yoursite',
  'your-access-token'
);

// ドキュメント承認ワークフローの開始
async function initiateDocumentApproval() {
  try {
    const result = await workflow.triggerApprovalWorkflow(
      'https://yourcompany.sharepoint.com/sites/yoursite/Documents/Project_Spec.docx',
      ['[email protected]', '[email protected]'],
      'プロジェクト仕様書の承認をお願いします。'
    );
    
    console.log('承認ワークフロー開始:', result);
  } catch (error) {
    console.error('承認ワークフローエラー:', error);
  }
}

// 週次レポート自動生成
async function generateAutomaticWeeklyReport() {
  try {
    const reportData = await workflow.scheduleWeeklyReport(
      'ProjectTasks',
      ['[email protected]', '[email protected]']
    );
    
    console.log('週次レポート生成完了:', reportData);
  } catch (error) {
    console.error('週次レポートエラー:', error);
  }
}

Teams統合とモダンコラボレーション

// Microsoft Teams + SharePoint統合
class TeamsSharePointIntegration {
  constructor(teamsApiUrl, sharePointSiteUrl, accessToken) {
    this.teamsApiUrl = teamsApiUrl;
    this.sharepoint = new SharePointAPI(sharePointSiteUrl, accessToken);
    this.accessToken = accessToken;
  }

  // Teams チャネルとSharePointフォルダの同期
  async syncTeamsChannel(teamId, channelId, documentLibrary) {
    try {
      // Teamsチャネル情報取得
      const channelInfo = await this.getChannelInfo(teamId, channelId);
      
      // SharePointフォルダ作成/確認
      const folderName = channelInfo.displayName.replace(/[^a-zA-Z0-9]/g, '_');
      await this.ensureFolderExists(documentLibrary, folderName);
      
      // チャネルタブにSharePointフォルダを追加
      await this.addSharePointTab(teamId, channelId, folderName, documentLibrary);
      
      return {
        channelName: channelInfo.displayName,
        folderPath: `/${documentLibrary}/${folderName}`,
        tabAdded: true,
      };

    } catch (error) {
      console.error('Teams-SharePoint同期エラー:', error);
      throw error;
    }
  }

  // Teamsチャネル情報取得
  async getChannelInfo(teamId, channelId) {
    const response = await fetch(`${this.teamsApiUrl}/teams/${teamId}/channels/${channelId}`, {
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      throw new Error(`Teams API Error: ${response.status}`);
    }

    return await response.json();
  }

  // SharePointフォルダ存在確認・作成
  async ensureFolderExists(libraryName, folderName) {
    try {
      // フォルダ存在確認
      const response = await fetch(
        `${this.sharepoint.siteUrl}/_api/web/GetFolderByServerRelativeUrl('${libraryName}/${folderName}')`,
        {
          method: 'GET',
          headers: this.sharepoint.headers,
        }
      );

      if (response.status === 404) {
        // フォルダが存在しない場合は作成
        await this.createFolder(libraryName, folderName);
      }

    } catch (error) {
      if (error.message.includes('404')) {
        await this.createFolder(libraryName, folderName);
      } else {
        throw error;
      }
    }
  }

  // SharePointフォルダ作成
  async createFolder(libraryName, folderName) {
    // Request Digest取得
    const digestResponse = await fetch(`${this.sharepoint.siteUrl}/_api/contextinfo`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Accept': 'application/json; odata=verbose',
      },
    });

    const digestData = await digestResponse.json();
    const requestDigest = digestData.d.GetContextWebInformation.FormDigestValue;

    // フォルダ作成
    const response = await fetch(
      `${this.sharepoint.siteUrl}/_api/web/folders`,
      {
        method: 'POST',
        headers: {
          ...this.sharepoint.headers,
          'X-RequestDigest': requestDigest,
        },
        body: JSON.stringify({
          __metadata: { type: 'SP.Folder' },
          ServerRelativeUrl: `/${libraryName}/${folderName}`,
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`フォルダ作成エラー: ${response.status}`);
    }

    return await response.json();
  }

  // TeamsチャネルにSharePointタブ追加
  async addSharePointTab(teamId, channelId, folderName, libraryName) {
    const tabData = {
      displayName: `${folderName} ファイル`,
      '[email protected]': `${this.teamsApiUrl}/appCatalogs/teamsApps/2a527703-1f6f-4559-a332-d8a7d288cd88`, // SharePoint アプリID
      configuration: {
        entityId: '',
        contentUrl: `${this.sharepoint.siteUrl}/${libraryName}/${folderName}`,
        websiteUrl: `${this.sharepoint.siteUrl}/${libraryName}/${folderName}`,
        removeUrl: null,
      },
    };

    const response = await fetch(`${this.teamsApiUrl}/teams/${teamId}/channels/${channelId}/tabs`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(tabData),
    });

    if (!response.ok) {
      throw new Error(`Teams タブ追加エラー: ${response.status}`);
    }

    return await response.json();
  }

  // Teams会議録をSharePointに自動保存
  async saveMeetingNotes(teamId, meetingData) {
    try {
      // 会議録HTML生成
      const meetingNotesHtml = this.generateMeetingNotesHtml(meetingData);
      
      // ファイル名生成(日付・タイトル・バージョン管理)
      const fileName = `会議録_${meetingData.date}_${meetingData.title.replace(/[^a-zA-Z0-9]/g, '_')}.html`;
      
      // SharePointドキュメントライブラリにアップロード
      const uploadResult = await this.sharepoint.uploadFile(
        'MeetingNotes',
        fileName,
        meetingNotesHtml
      );

      // メタデータ更新
      await this.updateMeetingFileMetadata(uploadResult.Name, meetingData);

      return {
        fileName: fileName,
        url: uploadResult.ServerRelativeUrl,
        uploadedAt: new Date().toISOString(),
      };

    } catch (error) {
      console.error('会議録保存エラー:', error);
      throw error;
    }
  }

  // 会議録HTML生成
  generateMeetingNotesHtml(meetingData) {
    return `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>会議録 - ${meetingData.title}</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; }
        .header { border-bottom: 2px solid #0078d4; padding-bottom: 10px; margin-bottom: 20px; }
        .section { margin-bottom: 20px; }
        .attendees { display: flex; flex-wrap: wrap; gap: 10px; }
        .attendee { background: #f3f2f1; padding: 5px 10px; border-radius: 15px; }
        .action-item { background: #fff4ce; padding: 10px; border-left: 4px solid #ffb900; margin: 5px 0; }
        .decision { background: #dff6dd; padding: 10px; border-left: 4px solid #107c10; margin: 5px 0; }
    </style>
</head>
<body>
    <div class="header">
        <h1>${meetingData.title}</h1>
        <p><strong>日時:</strong> ${meetingData.date} ${meetingData.time}</p>
        <p><strong>場所:</strong> ${meetingData.location}</p>
    </div>

    <div class="section">
        <h2>参加者</h2>
        <div class="attendees">
            ${meetingData.attendees.map(attendee => `<span class="attendee">${attendee}</span>`).join('')}
        </div>
    </div>

    <div class="section">
        <h2>議題</h2>
        <ol>
            ${meetingData.agenda.map(item => `<li>${item}</li>`).join('')}
        </ol>
    </div>

    <div class="section">
        <h2>議事内容</h2>
        ${meetingData.notes}
    </div>

    <div class="section">
        <h2>決定事項</h2>
        ${meetingData.decisions.map(decision => `<div class="decision">${decision}</div>`).join('')}
    </div>

    <div class="section">
        <h2>アクションアイテム</h2>
        ${meetingData.actionItems.map(item => 
          `<div class="action-item">
             <strong>${item.task}</strong><br>
             担当: ${item.assignee} | 期限: ${item.dueDate}
           </div>`
        ).join('')}
    </div>

    <div class="section">
        <h2>次回会議</h2>
        <p>${meetingData.nextMeeting || '未定'}</p>
    </div>
</body>
</html>`;
  }

  // 会議ファイルメタデータ更新
  async updateMeetingFileMetadata(fileName, meetingData) {
    try {
      const metadata = {
        __metadata: { type: 'SP.Data.MeetingNotesItem' },
        MeetingDate: meetingData.date,
        MeetingTitle: meetingData.title,
        Attendees: meetingData.attendees.join('; '),
        ActionItemsCount: meetingData.actionItems.length,
        DecisionsCount: meetingData.decisions.length,
      };

      // ファイルのリストアイテムID取得
      const fileResponse = await fetch(
        `${this.sharepoint.siteUrl}/_api/web/GetFileByServerRelativeUrl('/MeetingNotes/${fileName}')/ListItemAllFields`,
        {
          method: 'GET',
          headers: this.sharepoint.headers,
        }
      );

      const fileData = await fileResponse.json();
      const itemId = fileData.d.ID;

      // メタデータ更新
      await this.sharepoint.updateListItem('MeetingNotes', itemId, metadata);

    } catch (error) {
      console.error('メタデータ更新エラー:', error);
      // メタデータ更新エラーは非致命的エラーとして処理
    }
  }
}

// 使用例
const teamsIntegration = new TeamsSharePointIntegration(
  'https://graph.microsoft.com/v1.0',
  'https://yourcompany.sharepoint.com/sites/yoursite',
  'your-access-token'
);

// Teams-SharePoint同期の実行
async function setupTeamsSharePointSync() {
  try {
    const result = await teamsIntegration.syncTeamsChannel(
      'team-id-123',
      'channel-id-456',
      'Shared Documents'
    );
    
    console.log('同期完了:', result);
  } catch (error) {
    console.error('同期エラー:', error);
  }
}

// 会議録の自動保存
async function saveMeetingToSharePoint() {
  const meetingData = {
    title: 'Q1計画レビュー会議',
    date: '2025-01-15',
    time: '10:00-11:00',
    location: 'Teams会議',
    attendees: ['田中', '佐藤', '鈴木', '高橋'],
    agenda: [
      'Q4実績確認',
      'Q1目標設定',
      'リソース配分検討',
      'リスク分析'
    ],
    notes: '<p>Q4の実績は目標を上回り、Q1も積極的な目標設定を行う方針となった。</p>',
    decisions: [
      'Q1売上目標を15%上方修正',
      '新規プロジェクトを2件追加承認'
    ],
    actionItems: [
      { task: 'Q1詳細計画書作成', assignee: '田中', dueDate: '2025-01-20' },
      { task: 'リソース配分表更新', assignee: '佐藤', dueDate: '2025-01-18' }
    ],
    nextMeeting: '2025-02-15 10:00 Q1進捗レビュー'
  };

  try {
    const result = await teamsIntegration.saveMeetingNotes('team-id-123', meetingData);
    console.log('会議録保存完了:', result);
  } catch (error) {
    console.error('会議録保存エラー:', error);
  }
}