索引に戻る

売掛金の滞留・回収台帳

表
生成可能
売掛金の滞留・回収台帳

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

invoice-collection-aging-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

1.7 KB
「売掛金の滞留・回収台帳」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
滞留日数別の未回収残高と、請求単位の担当者・次の回収行動を優先づける。

## 渡す必要があるもの
- 請求ID、顧客、期限超過日数、残高、担当者、次の回収行動、状況を用意する。

## 入力の形
- `title` — string — 回収状況の結論
- `subtitle` — string? — 締め日・対象期間
- `organization` — string — 対象事業・回収チーム
- `currency` — string — 残高の通貨単位
- `invoices` — array (3–12) — 請求書(滞留日数、整合する滞留区分、残高、担当、次の回収行動、状況)

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 複数案の評価基準を比較して投資判断を行う場合は、加重意思決定スコアカードを使う。

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs invoice-collection-aging <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- https://design-vault.pages.dev/designs/invoice-collection-aging/source/template.tsx
- https://design-vault.pages.dev/designs/invoice-collection-aging/source/schema.ts
- https://design-vault.pages.dev/designs/invoice-collection-aging/source/sample-data.json
- https://design-vault.pages.dev/designs/invoice-collection-aging/sample.html

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

8.4 KB

/designs/invoice-collection-aging/source/template.tsx

中身を見る
import type { Props } from "./schema";

const CSS = String.raw`
.asset-invoice-collection-aging{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2.5rem;line-height:1.55;font-size:var(--text-body-sm)}
.asset-invoice-collection-aging *{box-sizing:border-box}.asset-invoice-collection-aging header{margin-bottom:1.25rem}.asset-invoice-collection-aging .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary);margin:0 0 .6rem}.asset-invoice-collection-aging h1{font-size:var(--text-h1);line-height:1.25;max-width:28em;margin:0}.asset-invoice-collection-aging .subtitle{color:var(--color-secondary);margin:.65rem 0 0}.asset-invoice-collection-aging .summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));border-block:1px solid var(--color-outline);margin:1.2rem 0}.asset-invoice-collection-aging .bucket{padding:.7rem .8rem;border-right:1px solid var(--color-outline);min-width:0}.asset-invoice-collection-aging .bucket:last-child{border-right:0}.asset-invoice-collection-aging .bucket span{display:block;color:var(--color-secondary);font-size:var(--text-label)}.asset-invoice-collection-aging .bucket strong{display:block;font-family:var(--font-mono);font-size:var(--text-h3);color:var(--color-primary);overflow-wrap:anywhere}.asset-invoice-collection-aging .bucket small{font-size:var(--text-label);color:var(--color-secondary)}.asset-invoice-collection-aging .table-head,.asset-invoice-collection-aging .invoice{display:grid;grid-template-columns:minmax(6rem,.75fr) minmax(8rem,1fr) minmax(5rem,.55fr) minmax(5.5rem,.65fr) minmax(8rem,1.25fr);gap:.7rem;align-items:start}.asset-invoice-collection-aging .table-head{padding:.5rem .6rem;background:var(--color-surface-subtle);font-size:var(--text-label);color:var(--color-secondary)}.asset-invoice-collection-aging .invoice{padding:.75rem .6rem;border-bottom:1px solid var(--color-outline)}.asset-invoice-collection-aging .invoice>*{min-width:0;overflow-wrap:anywhere}.asset-invoice-collection-aging code{font-family:var(--font-mono);font-size:var(--text-label)}.asset-invoice-collection-aging .customer{font-weight:600}.asset-invoice-collection-aging .amount{text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums}.asset-invoice-collection-aging .meta{font-size:var(--text-label);color:var(--color-secondary)}.asset-invoice-collection-aging .action{font-size:var(--text-label)}.asset-invoice-collection-aging .status{display:inline-block;margin-top:.2rem;border:1px solid var(--color-outline);padding:.05rem .35rem;color:var(--color-secondary)}.asset-invoice-collection-aging .still-svg{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media(max-width:760px){.asset-invoice-collection-aging{padding:1.5rem 1rem}.asset-invoice-collection-aging h1{font-size:var(--text-h2)}.asset-invoice-collection-aging .summary{grid-template-columns:repeat(3,minmax(0,1fr))}.asset-invoice-collection-aging .bucket{border-bottom:1px solid var(--color-outline)}.asset-invoice-collection-aging .table-head{display:none}.asset-invoice-collection-aging .invoice{grid-template-columns:minmax(0,1fr) auto;gap:.25rem .7rem;padding:.8rem .3rem}.asset-invoice-collection-aging .customer{grid-column:1}.asset-invoice-collection-aging .amount{grid-column:2;grid-row:1;text-align:right}.asset-invoice-collection-aging .id{grid-column:1}.asset-invoice-collection-aging .aging{grid-column:2;grid-row:2;text-align:right}.asset-invoice-collection-aging .action{grid-column:1/3}.asset-invoice-collection-aging .owner{grid-column:1/3}}
@media print{.asset-invoice-collection-aging{max-width:none;padding:1rem}.asset-invoice-collection-aging .invoice{break-inside:avoid}}
`;

const bucketOrder = ["current", "1–30", "31–60", "61–90", "90+"] as const;
const bucketNames = { current: "期限内", "1–30": "1–30日", "31–60": "31–60日", "61–90": "61–90日", "90+": "90日超" };
const statusNames = { "contact-needed": "連絡要", promised: "支払約束", disputed: "異議あり", "in-progress": "対応中", escalated: "上申", paid: "入金済" };
const money = (amount: number, currency: string) => `${currency} ${new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 2 }).format(amount)}`;

function Still({ data }: { data: Props }) {
  const sums = Object.fromEntries(bucketOrder.map((bucket) => [bucket, data.invoices.filter((invoice) => invoice.bucket === bucket).reduce((sum, invoice) => sum + invoice.amount, 0)]));
  return <svg className="still-svg" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" aria-hidden="true">
    <rect width="1280" height="720" fill="var(--color-surface)" />
    <text x="48" y="45" fontSize="14" fontFamily="sans-serif" fill="var(--color-secondary)">{data.organization} · {data.currency}</text>
    <text x="48" y="87" fontSize="27" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{data.title}</text>
    <text x="48" y="123" fontSize="13" fontFamily="sans-serif" fill="var(--color-secondary)">滞留年齢別の残高と、請求ごとの担当・次の回収行動</text>
    {bucketOrder.map((bucket, index) => <g key={bucket}>
      <text x={50 + index * 238} y="171" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">{bucketNames[bucket]}</text>
      <text x={50 + index * 238} y="204" fontSize="20" fontWeight="700" fontFamily="monospace" fill="var(--color-on-surface)">{money(sums[bucket], data.currency)}</text>
      <line x1={48 + index * 238} x2={268 + index * 238} y1="220" y2="220" stroke="var(--color-outline)" />
    </g>)}
    {data.invoices.slice(0, 8).map((invoice, index) => {
      const y = 266 + index * 51;
      return <g key={invoice.id}>
        <line x1="48" x2="1232" y1={y - 17} y2={y - 17} stroke="var(--color-outline)" />
        <text x="52" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-tertiary)">{invoice.id}</text>
        <text x="165" y={y + 3} fontSize="12" fontFamily="sans-serif" fill="var(--color-on-surface)">{invoice.customer}</text>
        <text x="555" y={y + 3} fontSize="11" fontFamily="sans-serif" fill="var(--color-secondary)">{invoice.daysOverdue}日 · {bucketNames[invoice.bucket]}</text>
        <text x="750" y={y + 3} fontSize="12" fontFamily="monospace" fill="var(--color-on-surface)">{money(invoice.amount, data.currency)}</text>
        <text x="940" y={y + 3} fontSize="11" fontFamily="sans-serif" fill="var(--color-secondary)">{invoice.owner}</text>
      </g>;
    })}
    <text x="48" y="704" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">{data.invoices.length}請求 · 合計 {money(data.invoices.reduce((sum, invoice) => sum + invoice.amount, 0), data.currency)}</text>
  </svg>;
}

export default function Template(data: Props) {
  const summaries = bucketOrder.map((bucket) => ({ bucket, rows: data.invoices.filter((invoice) => invoice.bucket === bucket), total: data.invoices.filter((invoice) => invoice.bucket === bucket).reduce((sum, invoice) => sum + invoice.amount, 0) }));
  return <section className="asset asset-invoice-collection-aging">
    <style>{CSS}</style>
    <header>
      <p className="eyebrow">{data.organization} · 売掛金回収 · {data.currency}</p>
      <h1>{data.title}</h1>
      {data.subtitle && <p className="subtitle">{data.subtitle}</p>}
    </header>
    <div className="summary" aria-label="滞留日数別残高">
      {summaries.map(({ bucket, rows, total }) => <div className="bucket" key={bucket}><span>{bucketNames[bucket]}</span><strong>{money(total, data.currency)}</strong><small>{rows.length}件</small></div>)}
    </div>
    <div className="table-head" aria-hidden="true"><span>請求書</span><span>顧客</span><span>滞留</span><span>残高</span><span>次の回収行動</span></div>
    {data.invoices.map((invoice) => <article className="invoice" key={invoice.id}>
      <div className="id"><code>{invoice.id}</code><span className="status">{statusNames[invoice.status]}</span></div>
      <div className="customer">{invoice.customer}<div className="meta">担当: {invoice.owner}</div></div>
      <div className="aging">{invoice.daysOverdue === 0 ? "期限内" : `${invoice.daysOverdue}日`}<div className="meta">{bucketNames[invoice.bucket]}</div></div>
      <div className="amount">{money(invoice.amount, data.currency)}</div>
      <div className="action">{invoice.nextAction}</div>
    </article>)}
    <Still data={data} />
  </section>;
}

schema.ts

1.9 KB

/designs/invoice-collection-aging/source/schema.ts

中身を見る
import { z } from "zod";

const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const bucketSchema = z.enum(["current", "1–30", "31–60", "61–90", "90+"]);
const invoiceSchema = z.object({
  id: short.describe("請求書の一意な識別子"),
  customer: short.describe("顧客または取引先"),
  daysOverdue: z.number().int().min(0).max(365).describe("支払期限からの経過日数"),
  bucket: bucketSchema.describe("経過日数から決まる滞留区分"),
  amount: z.number().positive().describe("未回収残高(通貨単位はヘッダーで指定)"),
  owner: short.describe("回収の担当者"),
  nextAction: text.describe("次に実施する具体的な回収アクション"),
  status: z.enum(["contact-needed", "promised", "disputed", "in-progress", "escalated", "paid"]).describe("回収状況"),
}).strict();

const expectedBucket = (days: number) => days === 0 ? "current" : days <= 30 ? "1–30" : days <= 60 ? "31–60" : days <= 90 ? "61–90" : "90+";

export const schema = z.object({
  title: text.describe("回収状況の結論"),
  subtitle: text.optional().describe("締め日・対象期間"),
  organization: short.describe("対象事業・回収チーム"),
  currency: short.describe("残高の通貨単位"),
  invoices: z.array(invoiceSchema).min(3).max(12).describe("請求単位の回収対象一覧"),
}).strict().superRefine((data, ctx) => {
  const ids = data.invoices.map(({ id }) => id);
  if (new Set(ids).size !== ids.length) {
    ctx.addIssue({ code: "custom", path: ["invoices"], message: "請求書IDは一意にすること" });
  }
  data.invoices.forEach((invoice, index) => {
    if (invoice.bucket !== expectedBucket(invoice.daysOverdue)) {
      ctx.addIssue({ code: "custom", path: ["invoices", index, "bucket"], message: "滞留区分は経過日数に一致させること" });
    }
  });
});

export type Props = z.infer<typeof schema>;

meta.yaml

1.7 KB

/designs/invoice-collection-aging/source/meta.yaml

中身を見る
id: invoice-collection-aging
name:
  ja: 売掛金の滞留・回収台帳
  en: Invoice Collection Aging Register
purpose:
  ja: 滞留日数別の未回収残高と、請求単位の担当者・次の回収行動を優先づける。
  en: Prioritize overdue receivables by aging bucket and assign the next collection action to each invoice.
status: published
still: ./still.png
category: operations
catalogOrder: 835
jobs:
  - prioritize-diagnose
  - assign-governance
useCases:
  - kpi
  - status
layoutKinds:
  - table
  - stack
tags:
  - accounts-receivable
  - collections
  - aging
  - cash-flow
  - responsive
whenToUse:
  - ja: 未回収残高を滞留日数で区分し、担当者ごとの具体的な次の連絡や上申を決めるとき。
    en: Use to bucket overdue balances by age and assign the next collection contact or escalation.
avoidWhen:
  - ja: 複数案の評価基準を比較して投資判断を行う場合は、加重意思決定スコアカードを使う。
    en: Use a weighted decision scorecard to compare criteria for an investment decision.
requiredInputs:
  - ja: 請求ID、顧客、期限超過日数、残高、担当者、次の回収行動、状況を用意する。
    en: Provide invoice ID, customer, days overdue, balance, owner, next collection action, and status.
relatedLayouts:
  - action-register
  - waterfall
dataSchema:
  title: string — 回収状況の結論
  subtitle: string? — 締め日・対象期間
  organization: string — 対象事業・回収チーム
  currency: string — 残高の通貨単位
  invoices: array (3–12) — 請求書(滞留日数、整合する滞留区分、残高、担当、次の回収行動、状況)
sources: []

sample-data.json

1.1 KB

/designs/invoice-collection-aging/source/sample-data.json

中身を見る
{
  "title": "90日超の延滞残高に管理者を割り当て、約束日を再確認する",
  "subtitle": "2026年9月締め · 回収予定と次の連絡を確認",
  "organization": "Northstar Wholesale",
  "currency": "USD",
  "invoices": [
    { "id": "INV-1048", "customer": "Cedar Works", "daysOverdue": 12, "bucket": "1–30", "amount": 18400, "owner": "Mina Sato", "nextAction": "9/26までに支払予定日を確認する", "status": "promised" },
    { "id": "INV-1072", "customer": "Harbor Medical", "daysOverdue": 46, "bucket": "31–60", "amount": 32750, "owner": "Ren Ito", "nextAction": "納品証明を添えて経理責任者へ連絡する", "status": "disputed" },
    { "id": "INV-0991", "customer": "Alpine Transit", "daysOverdue": 108, "bucket": "90+", "amount": 51200, "owner": "Aki Mori", "nextAction": "営業責任者同席で支払計画を合意する", "status": "escalated" },
    { "id": "INV-1103", "customer": "Lumen Foods", "daysOverdue": 0, "bucket": "current", "amount": 9200, "owner": "Mina Sato", "nextAction": "期限前の請求受領を確認する", "status": "contact-needed" }
  ]
}