kintone プロセス管理に連動する電子印添付方法

ITが浸透していない企業ではまだまだ紙文化が根強いのではないでしょうか？

kintone入れたけど、電子印も欲しいみたいな。。。
先日、プロセス管理のフローに連動した電子印の仕組みづくりを作ったのでその記録。

まずは検索。。。そりゃありますよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/bb7e0c0c-e970-f41b-1ee7-307e67a71c8a.png)

https://www.novelworks.jp/kintone_plugin/stamp_plugin?utm_source=Qiita0416&utm_medium=Qiita0416&utm_campaign=Qiita0416

買い切り１ドメイン税込165,000円です！
経費削減を謳ってる企業ではこういうコストの捻出もめんどくさい手続きが必要なのではないでしょうか。

最低限の機能だとなんかできそうな気がするのでやってみる。

参考にしたのは以下

■プロセス管理の状態

- [プロセス管理のアクションイベント](https://developer.cybozu.io/hc/ja/articles/201941974-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E8%A9%B3%E7%B4%B0%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88)

■ファイルのアップロード・ダウンロード

- [ファイルアップロードで必須となる3つの手順](
https://developer.cybozu.io/hc/ja/articles/200724665-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A7%E5%BF%85%E9%A0%88%E3%81%A8%E3%81%AA%E3%82%8B3%E3%81%A4%E3%81%AE%E6%89%8B%E9%A0%86)
- [ファイルダウンロードで必須となる2つの手順](
https://developer.cybozu.io/hc/ja/articles/200814380-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A7%E5%BF%85%E9%A0%88%E3%81%A8%E3%81%AA%E3%82%8B2%E3%81%A4%E3%81%AE%E6%89%8B%E9%A0%86)

■プロセス管理の定義取得

- [ アプリのプロセス管理の設定の取得
](https://developer.cybozu.io/hc/ja/articles/216972946)

## 作成までの流れ

1. 承認者と印鑑の紐付けを行うアプリ作成
2. 押印が必要なアプリ作成
3. プロセス管理設定
4. javascript適用
5. css適用

## 実行時のイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/2e46bad1-74b6-3adb-5cfe-b4d024bbc6ca.png)

では、作成開始！

## 1. 承認者と印鑑の紐付けを行うアプリ作成

承認者、承認印の２つを持つアプリを作成する。

|フィールド|フィールドコード|
|--|--|
|ユーザー選択|承認者|
|添付ファイル|承認印|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/8f6c0f53-efce-6ce9-d798-456db42cf2de.png)

## 2. 押印が必要なアプリ作成

出張申請アプリに適用してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/63cf0fc7-dc4e-cc8f-a37a-066310668964.png)

### プロセス管理を確認

プロセス管理の設定を確認すると、デフォルトでいろいろ設定されている。

今回は、上から２つ目の「承認する」となった段階で電子印を添付する流れとしよう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/705d60c1-f548-2835-600c-c61eed794c25.png)

### 電子印を添付する項目をフォームに追加

添付ファイルフィールドのフィールドコードは、「承認（出張費用未申請）」とする。
承認した時の次のステータスに移る際、添付ファイルフィールドに承認印を添付するイメージ。

> フィールドコードに全角の括弧は指定できないので、プロセス名を少し変更

承認（出張費用未申請）⇨ 承認・出張費用未申請

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/36e6a65b-6390-282e-3683-dd26ad72cfb8.png)

> 設定イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/cbefcfcf-db8a-f162-da36-b92bb115850c.png)

### javascript作成

以下のjavascriptの３箇所を実施している環境に合わせる。

- YOUR_DOMAIN
- STAMP_APP_ID
- STAMP_USER_CODE_FIELD_CD


```javascript
(() => {
  "use strict";

  /**
   * kintoneドメイン
   */
  const YOUR_DOMAIN = 'your-domain';  // ← ここ(https://your-domain.cybozu.com/) 
  /**
   * 電子印管理アプリID
   */
  const STAMP_APP_ID = 6;  // ← ここ
  /**
   * 電子印管理アプリの承認者を表すフィールドコード
   */
  const STAMP_USER_CODE_FIELD_CD = "承認者";  // ← ここ
  /**
   * 印鑑未登録例外
   */
  class UserStampNotFoundError extends Error {
    constructor(message = "") {
      super(message);
      this.name = "UserStampNotFoundError";
    }
  }

  // 印鑑情報を取得する。
  const getUserStamp = async (userCode) => {
    const query = {
      app: STAMP_APP_ID,
      query: `${STAMP_USER_CODE_FIELD_CD} in ("${userCode}")`,
    };
    const stampRecords = await kintone.api(
      kintone.api.url("/k/v1/records.json", true),
      "GET",
      query
    );
    if (stampRecords.records.length === 0) {
      throw new UserStampNotFoundError();
    }
    return stampRecords.records[0];
  };

  // プロセス管理アクション実行時
  kintone.events.on(["app.record.detail.process.proceed"], async (event) => {
    const nStatus = event.nextStatus.value;

    // プロセス名に対応する項目がレコードにない場合、処理しない
    if (!event.record[nStatus]) {
      return event;
    }

    // 以下、承認印処理
    const record = event.record;
    // 印鑑情報取得
    let stampRecord;
    try {
      stampRecord = await getUserStamp(record.更新者.value.code);
    } catch (usnfE) {
      alert('印鑑未登録です');
      return false;
    }

    // ファイルダウンロード
    const fileDownloadKey = {
      fileKey: stampRecord.承認印.value[0].fileKey,
    };
    const fileDownloadUrl = kintone.api.urlForGet(
      "/k/v1/file",
      fileDownloadKey,
      true
    );
    const downloadXhr = new XMLHttpRequest();
    downloadXhr.open("GET", fileDownloadUrl);
    downloadXhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    downloadXhr.responseType = "blob";
    downloadXhr.onload = () => {
      if (downloadXhr.status !== 200) {
        return;
      }
      // ファイルアップロード
      const formData = new FormData();
      formData.append("__REQUEST_TOKEN__", kintone.getRequestToken());
      formData.append(
        "file",
        new Blob([downloadXhr.response]),
        stampRecord.承認印.value[0].name
      );

      const uploadXhr = new XMLHttpRequest();
      uploadXhr.open("POST", kintone.api.url("/k/v1/file", true), false);
      uploadXhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
      uploadXhr.responseType = "multipart/form-data";
      uploadXhr.onload = async () => {
        if (uploadXhr.status !== 200) {
          return;
        }
        const key = JSON.parse(uploadXhr.responseText).fileKey;
        const json = {
          app: kintone.app.getId(),
          id: kintone.app.record.getId(),
          record: {
            [nStatus]: {
              value: [{ fileKey: key }],
            },
          },
        };
        await kintone.api(kintone.api.url("/k/v1/record", true), "PUT", json);
        // 非同期のため印鑑画像の表示がうまくいかない時があるため、リロードする。
        location.reload();
      };
      uploadXhr.send(formData);
    };
    downloadXhr.send();

    return event;
  });
})();
```

### アプリにjavascriptを適用

javascriptに２つ定義を追加する。

- https://cdn.jsdelivr.net/npm/sweetalert2@11
- 作成したjavascriptファイル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/3c97e9a5-ab04-e427-44cf-f9e2d81a65f2.png)

### 動作確認

電子印管理アプリに登録した画像が添付できていました！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/8d767b68-8a6d-ad9e-fab7-8284cfe8c275.png)

承認印の余白が邪魔くさいのでレイアウトを調整します。

## ３．電子印添付ファイルフィールドのスタイル調整

### javascriptファイル修正
以下のコードをjavascriptファイルに追記する。

```javascript
  // 詳細画面、追加画面、更新画面表示時
  kintone.events.on(
    [
      "app.record.detail.show",
      "app.record.create.show",
      "app.record.edit.show",
    ],
    async (event) => {
      // プロセス管理の設定を取得する。
      const processDefine = await kintone.api(
        kintone.api.url("/k/v1/app/status.json", true),
        "GET",
        { app: kintone.app.getId() }
      );
      if (!processDefine.enable) {
        Swal.fire({
          icon: "error",
          title: "プロセス設定未定義エラー",
          html: `プロセス設定が設定されていません。<br>電子印を使用する場合、プロセス設定を行ってください。`,
        });

        return event;
      }
      const processNames = processDefine.actions.map((action) => action.to);

      // 承認印添付ファイルフィールドにスタイルを適用する。
      const attachFileFields = Object.values(
        cybozu.data.page.FORM_DATA.schema.table.fieldList
      ).filter((value) => processNames.includes(value.var));
      attachFileFields.forEach((field) => {
        const element = document.getElementsByClassName(`field-${field.id}`);
        if (element.length === 0) {
          return event;
        }
        element[0].classList.add("approval-stamp-field");
      });

      return event;
    }
  );
```

### cssファイル作成

この辺はお好みで適宜変更する。

```css
.approval-stamp-field {
    width: 50px !important;
    min-width: 67px !important;
}
.approval-stamp-field div {
    width: 50px;
}
.approval-stamp-field .control-label-gaia {
    margin: 0;
    text-align: right;
}
.approval-stamp-field .control-value-gaia {
    min-height: 50px;
}
```

### javascriptとcssを再度アプリに登録して動作確認

電子印欄のサイズの変更ができました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/d978acc6-bf8b-8fb1-577d-d400cdbd64d3.png)

### 最後に電子印が未登録の場合

javascriptの印鑑情報取得を以下の内容に書き換える。

```javascript
    // 印鑑情報取得
    let stampRecord;
    try {
      stampRecord = await getUserStamp(record.更新者.value.code);
    } catch (usnfE) {
      Swal.fire({
        icon: "error",
        title: "電子印取得エラー",
        html: `承認者(${record.更新者.value.name})の電子印が登録されていません。<br>印鑑設定で電子印を登録後、再度実行してください。`,
        footer: `<a href="https://${YOUR_DOMAIN}.cybozu.com/k/${STAMP_APP_ID}/" target="_blank">印鑑設定はこちら</a>`,
      });

      return false;
    }
```

cssに以下の定義を追加する。

```css
.swal2-footer {
    background-color: #f8d7da;
}

.swal2-footer a {
    color: #721c24;
    padding-bottom: 15px;
}
```

### 動作確認

こんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1742664/beef148f-9511-ad5d-2987-32fb84839119.png)

## ４．javascript解説

### 4.1 セットアップ

電子印アプリ設定

```javascript
  /**
   * kintoneドメイン
   */
  const YOUR_DOMAIN = "your-domain";
  /**
   * 電子印管理アプリID
   */
  const STAMP_APP_ID = 6;
  /**
   * 電子印管理アプリの社員名を表すフィールドコード
   */
  const STAMP_USER_CODE_FIELD_CD = "承認者"; // 
```

### 4.2 プロセス管理実行時

１. スタートは、`kintone.events.on(["app.record.detail.process.proceed"], async (event) => {`ここ

２. プロセス管理のフローから電子印対応フェーズかチェックする。

```javascript
    const nStatus = event.nextStatus.value;

    // プロセス名に対応する項目がレコードにない場合、処理しない
    if (!event.record[nStatus]) {
      return event;
    }
```

３. 承認者の電子印を取得する。

```javascript
    // 以下、承認印処理
    const record = event.record;
    // 印鑑情報取得
    let stampRecord;
    try {
      stampRecord = await getUserStamp(record.更新者.value.code);
    } catch (usnfE) {
      Swal.fire({
        icon: "error",
        title: "電子印未登録",
        html: `承認者(${record.更新者.value.name})の電子印が登録されていません。<br>印鑑設定で電子印を登録後、再度実行してください。`,
        footer: `<a href="https://tiw-k.cybozu.com/k/${STAMP_APP_ID}/" target="_blank">印鑑設定はこちら</a>`,
      });

      return false;
    }
```

実行している関数

```javascript
  /**
   * 印鑑未登録例外
   */
  class UserStampNotFoundError extends Error {
    constructor(message = "") {
      super(message);
      this.name = "UserStampNotFoundError";
    }
  }

  // 印鑑情報を取得する。
  const getUserStamp = async (userCode) => {
    const query = {
      app: STAMP_APP_ID,
      query: `${STAMP_USER_CODE_FIELD_CD} in ("${userCode}")`,
    };
    const stampRecords = await kintone.api(
      kintone.api.url("/k/v1/records.json", true),
      "GET",
      query
    );
    if (stampRecords.records.length === 0) {
      throw new UserStampNotFoundError();
    }
    return stampRecords.records[0];
  };
```

４. 電子印管理アプリから画像をダウンロード、承認画像としてアップロード

```javascript
    // ファイルダウンロード
    const fileDownloadKey = {
      fileKey: stampRecord.承認印.value[0].fileKey,
    };
    const fileDownloadUrl = kintone.api.urlForGet(
      "/k/v1/file",
      fileDownloadKey,
      true
    );
    const downloadXhr = new XMLHttpRequest();
    downloadXhr.open("GET", fileDownloadUrl);
    downloadXhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    downloadXhr.responseType = "blob";
    downloadXhr.onload = () => {
      if (downloadXhr.status !== 200) {
        return;
      }
      // ファイルアップロード
      const formData = new FormData();
      formData.append("__REQUEST_TOKEN__", kintone.getRequestToken());
      formData.append(
        "file",
        new Blob([downloadXhr.response]),
        stampRecord.承認印.value[0].name
      );

      const uploadXhr = new XMLHttpRequest();
      uploadXhr.open("POST", kintone.api.url("/k/v1/file", true), false);
      uploadXhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
      uploadXhr.responseType = "multipart/form-data";
      uploadXhr.onload = async () => {
        if (uploadXhr.status !== 200) {
          return;
        }
        const key = JSON.parse(uploadXhr.responseText).fileKey;
        const json = {
          app: kintone.app.getId(),
          id: kintone.app.record.getId(),
          record: {
            [nStatus]: {
              value: [{ fileKey: key }],
            },
          },
        };
        await kintone.api(kintone.api.url("/k/v1/record", true), "PUT", json);
        // 印鑑画像の表示がうまくいかない時があるため、リロード
        location.reload();
      };
      uploadXhr.send(formData);
    };
    downloadXhr.send();

    return event;
  });
```

５．css適用

１. スタートは、`kintone.events.on(
    [
      "app.record.detail.show",
      "app.record.create.show",
      "app.record.edit.show",
    ],
    async (event) => {`ここ

２. プロセス管理の設定を取得する

```javascript
      // プロセス管理の設定を取得する。
      const processDefine = await kintone.api(
        kintone.api.url("/k/v1/app/status.json", true),
        "GET",
        { app: kintone.app.getId() }
      );
      if (!processDefine.enable) {
        Swal.fire({
          icon: "error",
          title: "プロセス設定未定義エラー",
          html: `プロセス設定が設定されていません。<br>電子印を使用する場合、プロセス設定を行ってください。`,
        });

        return event;
      }
```

３．電子印を添付するフィールドにclass属性を追加する。

```javascript
      const processNames = processDefine.actions.map((action) => action.to);

      // 承認印添付ファイルフィールドにスタイルを適用する。
      const attachFileFields = Object.values(
        cybozu.data.page.FORM_DATA.schema.table.fieldList
      ).filter((value) => processNames.includes(value.var));
      attachFileFields.forEach((field) => {
        const element = document.getElementsByClassName(`field-${field.id}`);
        if (element.length === 0) {
          return event;
        }
        element[0].classList.add("approval-stamp-field");
      });

      return event;
```

### ５．さいごに

簡易的な方法ですが、なんとか電子印の添付が組み込めました。

> 今回のリソース

https://github.com/second-fiddle/kintone/tree/main/stamp-paste