Back to index

Invoice Collection Aging Register

Table
Generates documents
Invoice Collection Aging Register

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

invoice-collection-aging-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.5 KB
Build an HTML document with "Invoice Collection Aging Register". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
Prioritize overdue receivables by aging bucket and assign the next collection action to each invoice.

## What you have to supply
- Provide invoice ID, customer, days overdue, balance, owner, next collection action, and status.

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

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Use a weighted decision scorecard to compare criteria for an investment decision.

## How to build it
Put the input in a JSON file and run:

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

The output is one self-contained file and fetches nothing at render time.

## The source
- 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

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

8.4 KB

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

Show contents
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

Show contents
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

Show contents
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

Show contents
{
  "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" }
  ]
}