SCALE — Build Lab
開発パターン · TYPESCRIPT LIBRARY

契約データ管理パターン

CATEGORY開発パターン TYPETypeScript Library EFFORT60〜180分 DIFFICULTY
PRIMARY CODE
ts · lib/contracts-data.ts
// 契約書対応セクションのデータ層
// freeeサイン連携 + テンプレート管理 + AI修正 + 進捗トラッキング

export const FREEE_SIGN_URL = 'https://ninja-sign.com/teams/66605/documents';

export const CONTRACT_CATEGORIES = [
  '業務委託契約',
  '秘密保持契約 (NDA)',
  '取引基本契約',
  '個別契約',
  'サービス利用契約',
  '成果報酬契約',
  '雇用契約',
  'その他',
];

export type ContractStatus = 'draft' | 'reviewing' | 'sent' | 'signed' | 'canceled';

export const CONTRACT_STATUS: Record<ContractStatus, { label: string; color: string; dot: string }> = {
  draft: { label: '下書き', color: 'text-gray-400 bg-gray-500/10 border-gray-500/30', dot: 'bg-gray-400' },
  reviewing: { label: 'レビュー中', color: 'text-blue-400 bg-blue-500/10 border-blue-500/30', dot: 'bg-blue-400' },
  sent: { label: '送付済(先方確認中)', color: 'text-orange-400 bg-orange-500/10 border-orange-500/30', dot: 'bg-orange-400' },
  signed: { label: '締結完了', color: 'text-green-400 bg-green-500/10 border-green-500/30', dot: 'bg-green-400' },
  canceled: { label: 'キャンセル', color: 'text-red-400 bg-red-500/10 border-red-500/30', dot: 'bg-red-400' },
};

export interface ContractTemplate {
  id: string;
  name: string;
  category: string;
  body: string;
  memo: string;
  createdAt: string;
  updatedAt: string;
}

export interface Contract {
  id: string;
  title: string;
  counterparty: string;
  category: string;
  status: ContractStatus;
  freeeSignUrl: string;
  owner: string;
  sentDate: string;
  signedDate: string;
  deadline: string;
  memo: string;
  body: string;
  lastUpdated: string;
  createdAt: string;
}

const KEY_TEMPLATES = 'scale-contracts-templates';
const KEY_CONTRACTS = 'scale-contracts-contracts';

export function loadTemplates(): ContractTemplate[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(KEY_TEMPLATES);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}

export function saveTemplates(x: ContractTemplate[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_TEMPLATES, JSON.stringify(x));
}

export function loadContracts(): Contract[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(KEY_CONTRACTS);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}

export function saveContracts(x: Contract[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_CONTRACTS, JSON.stringify(x));
}

export function genId(p = 'c'): string {
  return `${p}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
}

export function todayStr(): string {
  return new Date().toISOString().slice(0, 10);
}

export function nowISO(): string {
  return new Date().toISOString();
}

// 締結進捗の遅延判定(sent状態で deadline が今日+3日以内、あるいは 7日以上 送付済み)
export function isStalled(c: Contract): boolean {
  if (c.status !== 'sent') return false;
  if (c.deadline && c.deadline < todayStr()) return true;
  if (c.sentDate) {
    const days = Math.floor((new Date(todayStr()).getTime() - new Date(c.sentDate).getTime()) / 86400000);
    if (days >= 7) return true;
  }
  return false;
}

// 期日までの残日数(負なら過ぎている)
export function daysUntilDeadline(deadline: string): number | null {
  if (!deadline) return null;
  const now = new Date(todayStr()).getTime();
  const dl = new Date(deadline).getTime();
  return Math.floor((dl - now) / 86400000);
}

// 初期シード: よく使う契約書の雛形を投入(初回のみ)
export function seedIfEmpty() {
  if (typeof window === 'undefined') return;
  if (loadTemplates().length > 0) return;
  const now = nowISO();
  const seeds: ContractTemplate[] = [
    {
      id: genId('tpl'),
      name: '業務委託基本契約書(標準)',
      category: '業務委託契約',
      body: '第1条(目的)\n甲乙は、甲が乙に対し以下の業務を委託し、乙はこれを受託することに合意する。\n\n第2条(委託業務)\n委託する業務の詳細は別途個別契約書にて定めるものとする。\n\n第3条(契約期間)\n本契約の期間は、締結日から1年間とする。期間満了の1ヶ月前までに書面により申し出がないときは、同一条件でさらに1年間延長されるものとする。\n\n第4条(委託料)\n委託料は別途個別契約書にて定めるものとし、甲は乙に対し当該委託料を翌月末日までに支払うものとする。\n\n第5条(秘密保持)\n本契約に関連して知り得た情報を第三者に開示・漏洩してはならない。\n\n第6条(反社会的勢力の排除)\n甲乙は、反社会的勢力でないことを相互に表明し、今後も該当しないことを確約する。\n\n第7条(合意管轄)\n本契約に関する紛争は、東京地方裁判所を第一審の専属的合意管轄裁判所とする。\n\n以上、本契約の成立を証するため、本書2通を作成し、甲乙記名押印の上、各1通を保有する。',
      memo: '',
      createdAt: now,
      updatedAt: now,
    },
    {
      id: genId('tpl'),
      name: '秘密保持契約書(NDA)',
      category: '秘密保持契約 (NDA)',
      body: '第1条(秘密情報の定義)\n本契約における「秘密情報」とは、開示当事者が相手方に対し開示する一切の技術上・営業上の情報であって、秘密である旨を明示したものをいう。\n\n第2条(秘密保持義務)\n受領当事者は、開示当事者の事前の書面による承諾がない限り、秘密情報を第三者に開示・漏洩してはならない。\n\n第3条(使用目的)\n受領当事者は、秘密情報を本契約の目的以外に使用してはならない。\n\n第4条(契約期間)\n本契約の有効期間は、締結日から2年間とする。ただし、秘密保持義務は本契約終了後も3年間存続するものとする。\n\n第5条(合意管轄)\n本契約に関する紛争は、東京地方裁判所を第一審の専属的合意管轄裁判所とする。\n\n以上、本契約の成立を証するため、本書2通を作成し、甲乙記名押印の上、各1通を保有する。',
      memo: '',
      createdAt: now,
      updatedAt: now,
    },
    {
      id: genId('tpl'),
      name: '成果報酬型 個別契約書',
      category: '成果報酬契約',
      body: '第1条(業務内容)\n甲は乙に対し、以下の業務を委託する。\n・対象業界:\n・対象サービス:\n・トスアップ条件:\n\n第2条(成果報酬)\n1. 成果定義:有効商談1件\n2. 報酬単価:1件あたり金◯◯◯円(税別)\n3. 支払条件:当月末締・翌月末日振込\n\n第3条(有効商談の定義)\n以下の条件を全て満たすものを「有効商談」とする。\n(1)\n(2)\n\n第4条(契約期間)\n本契約の期間は◯年◯月◯日から◯年◯月◯日までとする。\n\n第5条(合意管轄)\n東京地方裁判所を第一審の専属的合意管轄裁判所とする。',
      memo: 'SCALEのBDR成果報酬の標準フォーマット',
      createdAt: now,
      updatedAt: now,
    },
  ];
  saveTemplates(seeds);
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 契約管理
  • サブスク管理
  • パートナー契約

契約データ管理パターン

:LiTarget: 用途

契約書・更新期日・支払条件をTypeScriptで型安全に管理するパターン。

:LiSparkle: 特徴

  • 契約一覧
  • 期日管理
  • 支払条件
  • 型安全

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: lib/contracts-data.ts の中身そのもの。コピペ即可。

// 契約書対応セクションのデータ層
// freeeサイン連携 + テンプレート管理 + AI修正 + 進捗トラッキング

export const FREEE_SIGN_URL = 'https://ninja-sign.com/teams/66605/documents';

export const CONTRACT_CATEGORIES = [
  '業務委託契約',
  '秘密保持契約 (NDA)',
  '取引基本契約',
  '個別契約',
  'サービス利用契約',
  '成果報酬契約',
  '雇用契約',
  'その他',
];

export type ContractStatus = 'draft' | 'reviewing' | 'sent' | 'signed' | 'canceled';

export const CONTRACT_STATUS: Record<ContractStatus, { label: string; color: string; dot: string }> = {
  draft: { label: '下書き', color: 'text-gray-400 bg-gray-500/10 border-gray-500/30', dot: 'bg-gray-400' },
  reviewing: { label: 'レビュー中', color: 'text-blue-400 bg-blue-500/10 border-blue-500/30', dot: 'bg-blue-400' },
  sent: { label: '送付済(先方確認中)', color: 'text-orange-400 bg-orange-500/10 border-orange-500/30', dot: 'bg-orange-400' },
  signed: { label: '締結完了', color: 'text-green-400 bg-green-500/10 border-green-500/30', dot: 'bg-green-400' },
  canceled: { label: 'キャンセル', color: 'text-red-400 bg-red-500/10 border-red-500/30', dot: 'bg-red-400' },
};

export interface ContractTemplate {
  id: string;
  name: string;
  category: string;
  body: string;
  memo: string;
  createdAt: string;
  updatedAt: string;
}

export interface Contract {
  id: string;
  title: string;
  counterparty: string;
  category: string;
  status: ContractStatus;
  freeeSignUrl: string;
  owner: string;
  sentDate: string;
  signedDate: string;
  deadline: string;
  memo: string;
  body: string;
  lastUpdated: string;
  createdAt: string;
}

const KEY_TEMPLATES = 'scale-contracts-templates';
const KEY_CONTRACTS = 'scale-contracts-contracts';

export function loadTemplates(): ContractTemplate[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(KEY_TEMPLATES);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}

export function saveTemplates(x: ContractTemplate[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_TEMPLATES, JSON.stringify(x));
}

export function loadContracts(): Contract[] {
  if (typeof window === 'undefined') return [];
  try {
    const v = localStorage.getItem(KEY_CONTRACTS);
    return v ? JSON.parse(v) : [];
  } catch { return []; }
}

export function saveContracts(x: Contract[]) {
  if (typeof window === 'undefined') return;
  localStorage.setItem(KEY_CONTRACTS, JSON.stringify(x));
}

export function genId(p = 'c'): string {
  return `${p}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
}

export function todayStr(): string {
  return new Date().toISOString().slice(0, 10);
}

export function nowISO(): string {
  return new Date().toISOString();
}

// 締結進捗の遅延判定(sent状態で deadline が今日+3日以内、あるいは 7日以上 送付済み)
export function isStalled(c: Contract): boolean {
  if (c.status !== 'sent') return false;
  if (c.deadline && c.deadline < todayStr()) return true;
  if (c.sentDate) {
    const days = Math.floor((new Date(todayStr()).getTime() - new Date(c.sentDate).getTime()) / 86400000);
    if (days >= 7) return true;
  }
  return false;
}

// 期日までの残日数(負なら過ぎている)
export function daysUntilDeadline(deadline: string): number | null {
  if (!deadline) return null;
  const now = new Date(todayStr()).getTime();
  const dl = new Date(deadline).getTime();
  return Math.floor((dl - now) / 86400000);
}

// 初期シード: よく使う契約書の雛形を投入(初回のみ)
export function seedIfEmpty() {
  if (typeof window === 'undefined') return;
  if (loadTemplates().length > 0) return;
  const now = nowISO();
  const seeds: ContractTemplate[] = [
    {
      id: genId('tpl'),
      name: '業務委託基本契約書(標準)',
      category: '業務委託契約',
      body: '第1条(目的)\n甲乙は、甲が乙に対し以下の業務を委託し、乙はこれを受託することに合意する。\n\n第2条(委託業務)\n委託する業務の詳細は別途個別契約書にて定めるものとする。\n\n第3条(契約期間)\n本契約の期間は、締結日から1年間とする。期間満了の1ヶ月前までに書面により申し出がないときは、同一条件でさらに1年間延長されるものとする。\n\n第4条(委託料)\n委託料は別途個別契約書にて定めるものとし、甲は乙に対し当該委託料を翌月末日までに支払うものとする。\n\n第5条(秘密保持)\n本契約に関連して知り得た情報を第三者に開示・漏洩してはならない。\n\n第6条(反社会的勢力の排除)\n甲乙は、反社会的勢力でないことを相互に表明し、今後も該当しないことを確約する。\n\n第7条(合意管轄)\n本契約に関する紛争は、東京地方裁判所を第一審の専属的合意管轄裁判所とする。\n\n以上、本契約の成立を証するため、本書2通を作成し、甲乙記名押印の上、各1通を保有する。',
      memo: '',
      createdAt: now,
      updatedAt: now,
    },
    {
      id: genId('tpl'),
      name: '秘密保持契約書(NDA)',
      category: '秘密保持契約 (NDA)',
      body: '第1条(秘密情報の定義)\n本契約における「秘密情報」とは、開示当事者が相手方に対し開示する一切の技術上・営業上の情報であって、秘密である旨を明示したものをいう。\n\n第2条(秘密保持義務)\n受領当事者は、開示当事者の事前の書面による承諾がない限り、秘密情報を第三者に開示・漏洩してはならない。\n\n第3条(使用目的)\n受領当事者は、秘密情報を本契約の目的以外に使用してはならない。\n\n第4条(契約期間)\n本契約の有効期間は、締結日から2年間とする。ただし、秘密保持義務は本契約終了後も3年間存続するものとする。\n\n第5条(合意管轄)\n本契約に関する紛争は、東京地方裁判所を第一審の専属的合意管轄裁判所とする。\n\n以上、本契約の成立を証するため、本書2通を作成し、甲乙記名押印の上、各1通を保有する。',
      memo: '',
      createdAt: now,
      updatedAt: now,
    },
    {
      id: genId('tpl'),
      name: '成果報酬型 個別契約書',
      category: '成果報酬契約',
      body: '第1条(業務内容)\n甲は乙に対し、以下の業務を委託する。\n・対象業界:\n・対象サービス:\n・トスアップ条件:\n\n第2条(成果報酬)\n1. 成果定義:有効商談1件\n2. 報酬単価:1件あたり金◯◯◯円(税別)\n3. 支払条件:当月末締・翌月末日振込\n\n第3条(有効商談の定義)\n以下の条件を全て満たすものを「有効商談」とする。\n(1)\n(2)\n\n第4条(契約期間)\n本契約の期間は◯年◯月◯日から◯年◯月◯日までとする。\n\n第5条(合意管轄)\n東京地方裁判所を第一審の専属的合意管轄裁判所とする。',
      memo: 'SCALEのBDR成果報酬の標準フォーマット',
      createdAt: now,
      updatedAt: now,
    },
  ];
  saveTemplates(seeds);
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/contracts-data.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加