# System-Wide Paper Document Phase 0-1 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the reusable system-wide paper-document foundation and apply it to the highest-priority warehouse documents: special inbound/outbound, production material outbound, and production ledger inbound settlement.

**Architecture:** Add generic `Document*` form components under `frontend/src/components/documentForms`, then make existing warehouse wrappers delegate to them so current warehouse documents do not break. Convert only the P0 warehouse forms in `InventoryLedgerView.vue` to the shared paper control language, preserving all existing data models, computed values, validation, submit functions, archive behavior, and backend APIs.

**Tech Stack:** Vue 3 SFC, Vite, project CSS in `frontend/src/styles/main.css`, Node built-in test runner.

---

## Scope

This plan covers:

- Phase 0: system-wide paper document component foundation.
- Phase 1: warehouse P0 documents.
- P0 warehouse documents: `特殊入库 / 特殊出库`, `生产出库`, `生产台账入库结算单`.

This plan does not cover:

- Reworking sales, purchase, receipt, quality, delivery, return pages beyond ensuring the new generic foundation does not break them.
- Backend archive schema changes. Existing `document_archives` and document archive routes remain unchanged.
- Business logic, inventory calculation, production calculation, PDF generation rules, or database migrations.

## File Structure

- Create `frontend/src/components/documentForms/DocumentFormShell.vue`: generic form shell around `DocumentPaper`.
- Create `frontend/src/components/documentForms/DocumentSection.vue`: generic paper section wrapper.
- Create `frontend/src/components/documentForms/DocumentActionBar.vue`: generic action bar that teleports to the paper bottom.
- Create `frontend/src/components/documentForms/DocumentField.vue`: generic paper field wrapper for input/select/textarea/read-only slots.
- Create `frontend/src/components/documentForms/DocumentSelectorPanel.vue`: paper-native panel for searchable select/multi-select controls.
- Create `frontend/src/components/documentForms/DocumentInlineSummary.vue`: paper-native summary strip.
- Create `frontend/src/components/documentForms/DocumentSettlePanel.vue`: paper-native settle/exception panel.
- Modify `frontend/src/components/documentForms/DocumentPaper.vue`: add generic action host while keeping warehouse-compatible host.
- Modify `frontend/src/components/documentForms/WarehouseDocumentFormShell.vue`: delegate to `DocumentFormShell`.
- Modify `frontend/src/components/documentForms/WarehouseDocumentSection.vue`: delegate to `DocumentSection`.
- Modify `frontend/src/components/documentForms/WarehouseDocumentActionBar.vue`: delegate to `DocumentActionBar`.
- Modify `frontend/src/views/InventoryLedgerView.vue`: convert P0 warehouse forms.
- Modify `frontend/src/styles/main.css`: add generic paper document styles and keep compatibility aliases.
- Modify `frontend/src/views/InventoryLedgerView.test.js`: add P0 regression checks.
- Create `frontend/src/components/documentForms/documentForms.test.js`: component foundation source-level regression checks.

## Task 1: Lock Generic Document Foundation Tests

**Files:**
- Create: `frontend/src/components/documentForms/documentForms.test.js`
- Read: `frontend/src/components/documentForms/DocumentPaper.vue`
- Read: `frontend/src/components/documentForms/WarehouseDocumentFormShell.vue`
- Read: `frontend/src/styles/main.css`

- [ ] **Step 1: Write the failing test**

Create `frontend/src/components/documentForms/documentForms.test.js`:

```js
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

function readComponent(name) {
  return readFileSync(join(__dirname, name), "utf8");
}

const mainCss = readFileSync(resolve(__dirname, "../../styles/main.css"), "utf8");

describe("document form foundation", () => {
  it("provides generic system-wide paper form components", () => {
    [
      "DocumentFormShell.vue",
      "DocumentSection.vue",
      "DocumentActionBar.vue",
      "DocumentField.vue",
      "DocumentSelectorPanel.vue",
      "DocumentInlineSummary.vue",
      "DocumentSettlePanel.vue"
    ].forEach((name) => {
      const source = readComponent(name);
      assert.match(source, /document-/);
    });
  });

  it("keeps warehouse wrappers as compatibility adapters over generic document components", () => {
    assert.match(readComponent("WarehouseDocumentFormShell.vue"), /import DocumentFormShell/);
    assert.match(readComponent("WarehouseDocumentSection.vue"), /import DocumentSection/);
    assert.match(readComponent("WarehouseDocumentActionBar.vue"), /import DocumentActionBar/);
  });

  it("exposes a generic action host without removing the existing warehouse host", () => {
    const paper = readComponent("DocumentPaper.vue");
    assert.match(paper, /data-document-action-host/);
    assert.match(paper, /data-warehouse-document-action-host/);
  });

  it("defines generic document paper controls and keeps warehouse aliases compatible", () => {
    assert.match(mainCss, /\.document-form-shell/);
    assert.match(mainCss, /\.document-section-grid/);
    assert.match(mainCss, /\.document-paper-field/);
    assert.match(mainCss, /\.document-selector-panel/);
    assert.match(mainCss, /\.document-inline-summary/);
    assert.match(mainCss, /\.document-settle-panel/);
    assert.match(mainCss, /\.warehouse-paper-control-grid/);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
cd frontend
node --test src/components/documentForms/documentForms.test.js
```

Expected: FAIL because `DocumentFormShell.vue`, `DocumentSection.vue`, `DocumentActionBar.vue`, `DocumentField.vue`, `DocumentSelectorPanel.vue`, `DocumentInlineSummary.vue`, and `DocumentSettlePanel.vue` do not exist yet.

## Task 2: Add Generic Document Components

**Files:**
- Create: `frontend/src/components/documentForms/DocumentFormShell.vue`
- Create: `frontend/src/components/documentForms/DocumentSection.vue`
- Create: `frontend/src/components/documentForms/DocumentActionBar.vue`
- Create: `frontend/src/components/documentForms/DocumentField.vue`
- Create: `frontend/src/components/documentForms/DocumentSelectorPanel.vue`
- Create: `frontend/src/components/documentForms/DocumentInlineSummary.vue`
- Create: `frontend/src/components/documentForms/DocumentSettlePanel.vue`
- Modify: `frontend/src/components/documentForms/DocumentPaper.vue`

- [ ] **Step 1: Add `DocumentFormShell.vue`**

Create `frontend/src/components/documentForms/DocumentFormShell.vue`:

```vue
<template>
  <form class="document-form-shell" @submit.prevent="emit('submit')">
    <DocumentPaper
      :title="title"
      :document-no="documentNo"
      :company-name="companyName"
      :tone="tone"
      :signature-labels="signatureLabels"
    >
      <div class="document-form-meta" :aria-label="metaAriaLabel">
        <span>{{ metaLabel }}</span>
        <strong>{{ companyName }}</strong>
        <span>{{ dateLabel }}</span>
        <strong>{{ displayBusinessDate }}</strong>
      </div>

      <main class="document-form-body">
        <slot />
      </main>
    </DocumentPaper>
  </form>
</template>

<script setup>
import { computed } from "vue";

import DocumentPaper from "./DocumentPaper.vue";
import { formatChineseDate } from "../../utils/formatters";

const props = defineProps({
  title: {
    type: String,
    required: true
  },
  documentNo: {
    type: String,
    default: ""
  },
  businessDate: {
    type: [String, Date],
    default: ""
  },
  tone: {
    type: String,
    default: "green"
  },
  companyName: {
    type: String,
    default: "宁波嘉恒智能科技有限公司"
  },
  metaLabel: {
    type: String,
    default: "业务联单"
  },
  dateLabel: {
    type: String,
    default: "业务日期"
  },
  signatureLabels: {
    type: Array,
    default: () => ["制单", "经办", "审核", "财务"]
  }
});

const emit = defineEmits(["submit"]);

const displayBusinessDate = computed(() => formatChineseDate(props.businessDate));
const metaAriaLabel = computed(() => `${props.metaLabel}基础信息`);
</script>
```

- [ ] **Step 2: Add `DocumentSection.vue`**

Create `frontend/src/components/documentForms/DocumentSection.vue`:

```vue
<template>
  <section class="document-section">
    <header class="document-section-head">
      <span class="document-section-index">{{ index }}</span>
      <strong>{{ title }}</strong>
    </header>

    <div class="document-section-grid">
      <slot />
    </div>
  </section>
</template>

<script setup>
defineProps({
  index: {
    type: [Number, String],
    required: true
  },
  title: {
    type: String,
    required: true
  }
});
</script>
```

- [ ] **Step 3: Add `DocumentActionBar.vue`**

Create `frontend/src/components/documentForms/DocumentActionBar.vue`:

```vue
<template>
  <span ref="anchorRef" class="document-action-anchor" aria-hidden="true"></span>

  <Teleport :to="actionHost" :disabled="!actionHost">
    <div
      class="document-action-bar"
      :class="{ 'document-action-bar--with-state': $slots.state }"
      @click.capture="handleActionClick"
    >
      <div v-if="$slots.state" class="document-action-state">
        <slot name="state" />
      </div>

      <div class="document-action-buttons">
        <slot />
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { nextTick, onMounted, ref } from "vue";

const anchorRef = ref(null);
const actionHost = ref(null);

onMounted(async () => {
  await nextTick();
  const form = anchorRef.value?.closest?.("form");
  actionHost.value = form?.querySelector?.("[data-document-action-host]") || form?.querySelector?.("[data-warehouse-document-action-host]") || null;
});

function handleActionClick(event) {
  const target = event.target;
  const submitter = target?.closest?.("button[type='submit'], input[type='submit']");
  if (!submitter || submitter.disabled) {
    return;
  }
  const form = anchorRef.value?.closest?.("form");
  if (!form) {
    return;
  }
  event.preventDefault();
  form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
}
</script>
```

- [ ] **Step 4: Add `DocumentField.vue`**

Create `frontend/src/components/documentForms/DocumentField.vue`:

```vue
<template>
  <component
    :is="as"
    class="document-paper-field"
    :class="[
      spanClass,
      {
        'document-paper-field-readonly': readonly,
        'document-paper-field-upload': upload
      }
    ]"
  >
    <span>{{ label }}</span>
    <slot>
      <p class="document-paper-readonly-value">{{ value || "-" }}</p>
    </slot>
  </component>
</template>

<script setup>
import { computed } from "vue";

const props = defineProps({
  label: {
    type: String,
    required: true
  },
  value: {
    type: [String, Number],
    default: ""
  },
  span: {
    type: [Number, String],
    default: 1
  },
  as: {
    type: String,
    default: "label"
  },
  readonly: {
    type: Boolean,
    default: false
  },
  upload: {
    type: Boolean,
    default: false
  }
});

const spanClass = computed(() => {
  if (String(props.span) === "full") return "document-paper-field-full";
  if (Number(props.span) === 2) return "document-paper-field-wide";
  return "";
});
</script>
```

- [ ] **Step 5: Add `DocumentSelectorPanel.vue`**

Create `frontend/src/components/documentForms/DocumentSelectorPanel.vue`:

```vue
<template>
  <section class="document-selector-panel">
    <div class="document-selector-heading">
      <span>{{ label }}</span>
      <strong>{{ hint }}</strong>
    </div>
    <div class="document-selector-body">
      <slot />
    </div>
  </section>
</template>

<script setup>
defineProps({
  label: {
    type: String,
    required: true
  },
  hint: {
    type: String,
    default: "请选择"
  }
});
</script>
```

- [ ] **Step 6: Add `DocumentInlineSummary.vue`**

Create `frontend/src/components/documentForms/DocumentInlineSummary.vue`:

```vue
<template>
  <div class="document-inline-summary" :class="toneClass">
    <slot />
  </div>
</template>

<script setup>
import { computed } from "vue";

const props = defineProps({
  tone: {
    type: String,
    default: "green"
  }
});

const toneClass = computed(() => `document-inline-summary-${props.tone}`);
</script>
```

- [ ] **Step 7: Add `DocumentSettlePanel.vue`**

Create `frontend/src/components/documentForms/DocumentSettlePanel.vue`:

```vue
<template>
  <section class="document-settle-panel">
    <slot />
  </section>
</template>
```

- [ ] **Step 8: Add generic and legacy action hosts to `DocumentPaper.vue`**

Modify the action host near the end of the template:

```vue
<div
  class="document-paper-action-host"
  data-document-action-host
  data-warehouse-document-action-host
></div>
```

- [ ] **Step 9: Run component test**

Run:

```bash
cd frontend
node --test src/components/documentForms/documentForms.test.js
```

Expected: FAIL on CSS assertions because the generic `document-*` styles are not defined yet.

## Task 3: Convert Warehouse Wrappers To Generic Adapters

**Files:**
- Modify: `frontend/src/components/documentForms/WarehouseDocumentFormShell.vue`
- Modify: `frontend/src/components/documentForms/WarehouseDocumentSection.vue`
- Modify: `frontend/src/components/documentForms/WarehouseDocumentActionBar.vue`

- [ ] **Step 1: Replace `WarehouseDocumentFormShell.vue` with adapter**

Use:

```vue
<template>
  <DocumentFormShell
    class="warehouse-document-form"
    :title="title"
    :document-no="documentNo"
    :business-date="businessDate"
    :tone="tone"
    :company-name="companyName"
    meta-label="仓库联单"
    :signature-labels="signatureLabels"
    @submit="emit('submit')"
  >
    <slot />
  </DocumentFormShell>
</template>

<script setup>
import DocumentFormShell from "./DocumentFormShell.vue";

defineProps({
  title: {
    type: String,
    required: true
  },
  documentNo: {
    type: String,
    default: ""
  },
  businessDate: {
    type: [String, Date],
    default: ""
  },
  tone: {
    type: String,
    default: "green"
  },
  companyName: {
    type: String,
    default: "嘉恒仓库"
  },
  signatureLabels: {
    type: Array,
    default: () => ["制单", "审核", "仓库", "财务"]
  }
});

const emit = defineEmits(["submit"]);
</script>
```

- [ ] **Step 2: Replace `WarehouseDocumentSection.vue` with adapter**

Use:

```vue
<template>
  <DocumentSection
    class="warehouse-document-section"
    :index="index"
    :title="title"
  >
    <slot />
  </DocumentSection>
</template>

<script setup>
import DocumentSection from "./DocumentSection.vue";

defineProps({
  index: {
    type: [Number, String],
    required: true
  },
  title: {
    type: String,
    required: true
  }
});
</script>
```

- [ ] **Step 3: Replace `WarehouseDocumentActionBar.vue` with adapter**

Use:

```vue
<template>
  <DocumentActionBar class="warehouse-document-action-bar--shared">
    <template v-if="$slots.state" #state>
      <slot name="state" />
    </template>
    <slot />
  </DocumentActionBar>
</template>

<script setup>
import DocumentActionBar from "./DocumentActionBar.vue";
</script>
```

- [ ] **Step 4: Run component test again**

Run:

```bash
cd frontend
node --test src/components/documentForms/documentForms.test.js
```

Expected: still FAIL on CSS assertions only.

## Task 4: Add Generic Paper Document CSS And Compatibility Aliases

**Files:**
- Modify: `frontend/src/styles/main.css`

- [ ] **Step 1: Add generic style block near existing paper document styles**

Add this block before the existing warehouse-specific paper form overrides:

```css
/* System-wide paper document form primitives */
.document-form-shell {
  display: grid;
  gap: 14px;
  width: 100%;
  min-width: 0;
  padding-bottom: 86px;
}

.document-form-shell .document-paper-shell {
  max-width: 1080px;
  padding: 18px 12px 26px;
}

.document-form-shell .document-paper-sheet {
  min-height: 0;
  padding-bottom: 34px;
}

.document-form-shell .document-paper-title {
  font-size: clamp(34px, 4.2vw, 48px);
}

.document-form-shell .document-number-stamp {
  min-width: 196px;
}

.document-form-meta {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  padding: 9px 12px;
  color: rgba(23, 18, 10, 0.72);
  border: 2px solid rgba(20, 14, 6, 0.82);
  background:
    linear-gradient(90deg, rgba(31, 122, 74, 0.12), transparent 42%),
    rgba(255, 250, 240, 0.54);
  font-family: "Songti SC", "SimSun", var(--ui-font-body);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.08em;
}

.document-form-meta strong {
  display: inline-flex;
  align-items: center;
  min-height: 26px;
  padding: 0 10px;
  color: #145c39;
  border: 1.5px solid rgba(31, 122, 74, 0.64);
  background: rgba(232, 246, 232, 0.62);
}

.document-form-body {
  display: grid;
  gap: 14px;
}

.document-section {
  border: 2px solid rgba(20, 14, 6, 0.88);
  background: rgba(255, 250, 240, 0.38);
}

.document-section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 9px 12px;
  border-bottom: 2px solid rgba(20, 14, 6, 0.88);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.26), transparent),
    rgba(227, 207, 161, 0.72);
}

.document-section-index {
  display: inline-grid;
  place-items: center;
  min-width: 30px;
  height: 30px;
  color: #145c39;
  border: 1.5px solid rgba(31, 122, 74, 0.64);
  background: rgba(232, 246, 232, 0.62);
  font-family: var(--ui-font-display);
  font-size: 12px;
  font-weight: 900;
}

.document-section-head strong {
  color: #17120a;
  font-family: var(--ui-font-display);
  font-size: 16px;
  font-weight: 900;
  letter-spacing: 0.06em;
}

.document-section-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  min-width: 0;
  padding: 14px;
}

.document-section-grid > .document-control-grid,
.document-section-grid > .document-selector-panel,
.document-section-grid > .document-source-table,
.document-section-grid > .document-inline-summary,
.document-section-grid > .document-settle-panel,
.document-section-grid > .document-paper-field {
  grid-column: 1 / -1;
  width: 100%;
  min-width: 0;
}

.document-control-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 2px solid rgba(20, 14, 6, 0.88);
  border-left: 2px solid rgba(20, 14, 6, 0.88);
  background: rgba(255, 250, 240, 0.34);
}

.document-paper-field {
  display: grid;
  grid-template-rows: auto minmax(54px, auto);
  min-width: 0;
  margin: 0;
  border-right: 2px solid rgba(20, 14, 6, 0.88);
  border-bottom: 2px solid rgba(20, 14, 6, 0.88);
  background: rgba(255, 250, 240, 0.44);
}

.document-paper-field-wide {
  grid-column: span 2;
}

.document-paper-field-full {
  grid-column: 1 / -1;
}

.document-paper-field > span {
  display: flex;
  align-items: center;
  min-height: 32px;
  padding: 7px 10px;
  color: rgba(23, 18, 10, 0.78);
  border-bottom: 1px solid rgba(20, 14, 6, 0.56);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.24), transparent),
    rgba(227, 207, 161, 0.62);
  font-family: var(--ui-font-display);
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.08em;
}

.document-paper-field input,
.document-paper-field select,
.document-paper-field textarea {
  width: 100%;
  min-width: 0;
  min-height: 48px;
  padding: 10px 11px;
  color: #17120a;
  border: 0;
  border-radius: 0;
  outline: 0;
  background: transparent;
  box-shadow: none;
  font-family: "Songti SC", "SimSun", var(--ui-font-body);
  font-size: 15px;
  font-weight: 800;
  line-height: 1.45;
}

.document-paper-field textarea {
  min-height: 104px;
  resize: vertical;
}

.document-paper-field input:focus,
.document-paper-field select:focus,
.document-paper-field textarea:focus {
  background:
    linear-gradient(90deg, rgba(31, 122, 74, 0.12), transparent 42%),
    rgba(255, 255, 255, 0.48);
  box-shadow: inset 0 0 0 2px rgba(31, 122, 74, 0.42);
}

.document-paper-readonly-value {
  display: flex;
  align-items: center;
  min-height: 48px;
  margin: 0;
  padding: 10px 11px;
  color: #145c39;
  background:
    repeating-linear-gradient(-45deg, rgba(31, 122, 74, 0.06) 0 5px, transparent 5px 10px),
    rgba(232, 246, 232, 0.32);
  font-family: "Songti SC", "SimSun", var(--ui-font-body);
  font-size: 14px;
  font-weight: 900;
  line-height: 1.5;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.document-selector-panel {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  min-width: 0;
  border-top: 2px solid rgba(20, 14, 6, 0.88);
  border-left: 2px solid rgba(20, 14, 6, 0.88);
  background: rgba(255, 250, 240, 0.44);
}

.document-selector-heading,
.document-selector-body {
  min-width: 0;
  border-right: 2px solid rgba(20, 14, 6, 0.88);
  border-bottom: 2px solid rgba(20, 14, 6, 0.88);
}

.document-selector-heading {
  display: grid;
  grid-template-rows: auto 1fr;
}

.document-selector-heading span {
  display: flex;
  align-items: center;
  min-height: 32px;
  padding: 7px 10px;
  color: rgba(23, 18, 10, 0.78);
  border-bottom: 1px solid rgba(20, 14, 6, 0.56);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.24), transparent),
    rgba(227, 207, 161, 0.62);
  font-family: var(--ui-font-display);
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.08em;
}

.document-selector-heading strong,
.document-selector-body {
  padding: 10px 11px;
  font-family: "Songti SC", "SimSun", var(--ui-font-body);
  font-weight: 900;
}

.document-inline-summary {
  min-width: 0;
  padding: 12px 14px;
  color: #145c39;
  border: 2px solid rgba(20, 14, 6, 0.88);
  background:
    linear-gradient(90deg, rgba(31, 122, 74, 0.12), transparent 42%),
    rgba(232, 246, 232, 0.52);
  font-family: "Songti SC", "SimSun", var(--ui-font-body);
  font-size: 15px;
  font-weight: 900;
}

.document-settle-panel {
  display: grid;
  gap: 0;
  min-width: 0;
  border-top: 2px solid rgba(20, 14, 6, 0.88);
  border-left: 2px solid rgba(20, 14, 6, 0.88);
  background:
    linear-gradient(90deg, rgba(180, 83, 9, 0.08), transparent 48%),
    rgba(255, 250, 240, 0.5);
}

.document-action-anchor {
  display: none;
}

.document-action-bar {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: fit-content;
  max-width: 1080px;
  margin: 0 0 0 auto;
  padding: 10px;
  border: 1px solid rgba(31, 122, 74, 0.24);
  border-radius: 18px;
  background:
    linear-gradient(90deg, rgba(232, 246, 232, 0.92), rgba(255, 250, 240, 0.96)),
    rgba(255, 255, 255, 0.92);
  box-shadow:
    0 -12px 28px rgba(5, 11, 18, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.68);
  backdrop-filter: blur(14px);
}

.document-action-bar--with-state {
  justify-content: space-between;
  width: min(100%, 1080px);
}

.document-action-state {
  min-width: 0;
  color: var(--document-paper-muted, #6a5631);
  font-size: 13px;
  font-weight: 900;
  line-height: 1.45;
}

.document-action-buttons {
  display: inline-flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-end;
}

@media (max-width: 960px) {
  .document-form-meta,
  .document-section-grid,
  .document-control-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .document-selector-panel {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 620px) {
  .document-form-shell .document-paper-shell {
    padding-inline: 0;
  }

  .document-form-meta,
  .document-section-grid,
  .document-control-grid {
    grid-template-columns: 1fr;
  }

  .document-paper-field,
  .document-paper-field-wide,
  .document-paper-field-full {
    grid-column: 1 / -1;
  }

  .document-action-bar,
  .document-action-bar--with-state,
  .document-action-state,
  .document-action-buttons {
    width: 100%;
  }
}
```

- [ ] **Step 2: Add warehouse compatibility aliases below generic styles**

Add:

```css
.warehouse-document-operation-form,
.warehouse-document-form {
  display: grid;
  gap: 14px;
  width: 100%;
  min-width: 0;
}

.warehouse-document-form-meta {
  display: contents;
}

.warehouse-document-body {
  display: contents;
}

.warehouse-document-section {
  display: block;
}

.warehouse-document-section-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  min-width: 0;
  padding: 14px;
}

.warehouse-paper-control-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
```

- [ ] **Step 3: Run component test to verify generic foundation passes**

Run:

```bash
cd frontend
node --test src/components/documentForms/documentForms.test.js
```

Expected: PASS.

## Task 5: Add P0 Warehouse Regression Tests

**Files:**
- Modify: `frontend/src/views/InventoryLedgerView.test.js`

- [ ] **Step 1: Add assertions for P0 document conversion**

Append these tests inside the existing `describe("InventoryLedgerView warehouse action layout", () => { })` block:

```js
it("keeps special adjustment documents inside the paper document language", () => {
  assert.match(source, /special-adjustment-paper-grid/);
  assert.match(source, /special-adjustment-paper-warning/);
  assert.match(source, /special-adjustment-paper-lines/);
  assert.match(source, /special-adjustment-paper-line-card/);
  assert.doesNotMatch(source, /<div class="double-field">[\s\S]*?specialAdjustmentForm\.reason/);
  assert.match(mainCss, /\.special-adjustment-paper-grid/);
  assert.match(mainCss, /\.special-adjustment-paper-line-card/);
});

it("keeps production outbound document controls paper-native", () => {
  assert.match(source, /production-out-paper-grid/);
  assert.match(source, /production-out-source-lot-panel/);
  assert.match(source, /production-out-source-lot-table/);
  assert.doesNotMatch(source, /<div class="workflow-tip">选择原材料库存批次出库/);
  assert.match(mainCss, /\.production-out-source-lot-panel/);
});

it("keeps production ledger inbound settlement controls paper-native", () => {
  assert.match(source, /production-ledger-inbound-paper-grid/);
  assert.match(source, /production-ledger-inbound-summary/);
  assert.match(source, /production-ledger-inbound-metrics/);
  assert.doesNotMatch(source, /<div class="triple-field">[\s\S]*?productionWorkOrderInboundForm\.finished_qty/);
  assert.match(mainCss, /\.production-ledger-inbound-metrics/);
});
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
cd frontend
node --test src/views/InventoryLedgerView.test.js
```

Expected: FAIL because the P0 class names are not in `InventoryLedgerView.vue` or CSS yet.

## Task 6: Convert Special Inbound / Special Outbound Paper Form

**Files:**
- Modify: `frontend/src/views/InventoryLedgerView.vue`
- Modify: `frontend/src/styles/main.css`

- [ ] **Step 1: Replace the special adjustment warning and header fields**

In the `specialAdjustmentDrawerOpen` drawer, replace:

```vue
<div class="special-adjustment-warning">
  <strong>特殊调整会直接改变库存明细</strong>
  <span>保存后写入库存流水和特殊调整记录，请仅在补录、纠错、线下已处理等特殊场景使用。</span>
</div>

<div class="double-field">
  <label class="form-field">
    <span>调整说明</span>
    <textarea
      v-model.trim="specialAdjustmentForm.reason"
      rows="3"
      required
      placeholder="必须填写现场原因、审批依据或补录说明"
    ></textarea>
  </label>
  <label class="form-field">
    <span>仓库</span>
    <select v-model.number="specialAdjustmentForm.warehouse_id" required @change="handleSpecialAdjustmentWarehouseChange">
      <option disabled value="">请选择仓库</option>
      <option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
        {{ warehouse.warehouse_name }}
      </option>
    </select>
  </label>
</div>
```

with:

```vue
<div class="document-inline-summary special-adjustment-paper-warning">
  <strong>特殊调整会直接改变库存明细</strong>
  <span>保存后写入库存流水和特殊调整记录，请仅在补录、纠错、线下已处理等特殊场景使用。</span>
</div>

<div class="document-control-grid special-adjustment-paper-grid">
  <label class="document-paper-field document-paper-field-wide">
    <span>调整说明</span>
    <textarea
      v-model.trim="specialAdjustmentForm.reason"
      rows="3"
      required
      placeholder="必须填写现场原因、审批依据或补录说明"
    ></textarea>
  </label>
  <label class="document-paper-field">
    <span>仓库</span>
    <select v-model.number="specialAdjustmentForm.warehouse_id" required @change="handleSpecialAdjustmentWarehouseChange">
      <option disabled value="">请选择仓库</option>
      <option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
        {{ warehouse.warehouse_name }}
      </option>
    </select>
  </label>
</div>
```

- [ ] **Step 2: Replace add-line action area**

Replace:

```vue
<div class="drawer-table-actions">
  <button class="ghost-button" type="button" :disabled="!specialAdjustmentForm.reason.trim()" @click="addSpecialAdjustmentLine">
    添加调整明细
  </button>
  <span v-if="!specialAdjustmentForm.reason.trim()" class="special-adjustment-tip">请先填写调整说明</span>
</div>
```

with:

```vue
<div class="document-inline-summary special-adjustment-paper-toolbar">
  <button class="ghost-button" type="button" :disabled="!specialAdjustmentForm.reason.trim()" @click="addSpecialAdjustmentLine">
    添加调整明细
  </button>
  <span v-if="!specialAdjustmentForm.reason.trim()" class="special-adjustment-tip">请先填写调整说明</span>
  <span v-else>每条明细都会写入库存流水，调整说明会同步进入变更记录。</span>
</div>
```

- [ ] **Step 3: Rename special adjustment line containers**

Replace class names:

```vue
class="special-adjustment-lines"
class="special-adjustment-line-card"
```

with:

```vue
class="special-adjustment-paper-lines special-adjustment-lines"
class="special-adjustment-paper-line-card special-adjustment-line-card"
```

Keep existing child classes such as `special-adjustment-card-grid`, `special-adjustment-card-field`, and `special-adjustment-card-preview` for now because they already use a paper-grid look.

- [ ] **Step 4: Add CSS aliases**

Add near the existing special adjustment styles:

```css
.special-adjustment-paper-warning {
  display: grid;
  gap: 5px;
  color: #78350f;
  background:
    linear-gradient(90deg, rgba(180, 83, 9, 0.13), transparent 50%),
    rgba(255, 250, 240, 0.64);
}

.special-adjustment-paper-warning strong {
  color: #78350f;
  font-size: 15px;
}

.special-adjustment-paper-warning span,
.special-adjustment-paper-toolbar span {
  font-size: 12px;
  font-weight: 900;
}

.special-adjustment-paper-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.special-adjustment-paper-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.special-adjustment-paper-lines {
  display: grid;
  grid-column: 1 / -1;
  gap: 14px;
  min-width: 0;
}

.special-adjustment-paper-line-card {
  position: relative;
  overflow: hidden;
  border: 1.5px solid rgba(20, 14, 6, 0.72);
  background:
    linear-gradient(90deg, rgba(31, 122, 74, 0.06), transparent 44%),
    rgba(255, 250, 240, 0.72);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.5),
    0 8px 18px rgba(52, 39, 15, 0.08);
}
```

- [ ] **Step 5: Run focused test**

Run:

```bash
cd frontend
node --test src/views/InventoryLedgerView.test.js
```

Expected: special adjustment test passes; production out and production ledger inbound tests still fail.

## Task 7: Convert Production Out Paper Form

**Files:**
- Modify: `frontend/src/views/InventoryLedgerView.vue`
- Modify: `frontend/src/styles/main.css`

- [ ] **Step 1: Replace production out tip and product selector**

In the `productionDrawerOpen` drawer, replace:

```vue
<div class="workflow-tip">选择原材料库存批次出库，后续小程序报工按库存批次号推进。</div>

<label class="form-field">
  <span>产品</span>
  <select v-model.number="productionForm.product_item_id" required>
    <option disabled value="">请选择产品</option>
    <option v-for="product in products" :key="product.item_id" :value="product.item_id">
      {{ formatProductLabel(product) }}
    </option>
  </select>
</label>
```

with:

```vue
<div class="document-inline-summary production-out-paper-tip">
  选择原材料库存批次出库，后续小程序报工按库存批次号推进。一次生产出库只能选择一个材料库存批次。
</div>

<div class="document-control-grid production-out-paper-grid">
  <label class="document-paper-field document-paper-field-wide">
    <span>产品</span>
    <select v-model.number="productionForm.product_item_id" required>
      <option disabled value="">请选择产品</option>
      <option v-for="product in products" :key="product.item_id" :value="product.item_id">
        {{ formatProductLabel(product) }}
      </option>
    </select>
  </label>
</div>
```

- [ ] **Step 2: Replace production stock lot selector**

Replace:

```vue
<label class="form-field">
  <span>库存批次号</span>
  <StockLotTagSelect
    v-model="selectedProductionStockLots"
    :options="productionStockLotOptions"
    :max-selections="1"
    placeholder="搜索并选择一个材料库存批次号"
  />
</label>
```

with:

```vue
<div class="document-selector-panel production-out-source-lot-panel">
  <div class="document-selector-heading">
    <span>库存批次号</span>
    <strong>搜索并选择一个材料库存批次号</strong>
  </div>
  <div class="document-selector-body">
    <StockLotTagSelect
      v-model="selectedProductionStockLots"
      :options="productionStockLotOptions"
      :max-selections="1"
      placeholder="搜索并选择一个材料库存批次号"
    />
  </div>
</div>
```

- [ ] **Step 3: Replace selected lot table wrapper**

Change:

```vue
<div v-if="selectedProductionStockLots.length" class="table-wrap">
```

to:

```vue
<div v-if="selectedProductionStockLots.length" class="table-wrap document-source-table production-out-source-lot-table">
```

- [ ] **Step 4: Replace issue weight/reference quantity group**

Replace the production out `double-field` group with:

```vue
<div class="document-control-grid production-out-paper-grid">
  <label class="document-paper-field">
    <span>本次生产出库重量(kg)</span>
    <input
      v-model.number="productionForm.issue_weight_kg"
      type="number"
      min="0.001"
      step="0.001"
      required
      :readonly="selectedProductionStockLots.length > 0"
    />
  </label>
  <label class="document-paper-field">
    <span>参考生产数量</span>
    <input :value="formatQty(referenceProductionQty)" type="text" readonly />
  </label>
</div>
```

- [ ] **Step 5: Add CSS**

Add:

```css
.production-out-paper-tip {
  color: #145c39;
}

.production-out-paper-grid,
.production-out-source-lot-panel,
.production-out-source-lot-table {
  grid-column: 1 / -1;
}

.production-out-source-lot-panel .stock-lot-control {
  min-height: 50px;
  border: 0;
  border-radius: 0;
  background: rgba(255, 255, 255, 0.32);
  box-shadow: inset 0 0 0 1.5px rgba(31, 122, 74, 0.28);
}

.production-out-source-lot-table {
  margin: 0;
  overflow-x: auto;
  border-top: 2px solid rgba(20, 14, 6, 0.88);
  border-left: 2px solid rgba(20, 14, 6, 0.88);
  border-radius: 0;
  background: rgba(255, 250, 240, 0.46);
  box-shadow: none;
}
```

- [ ] **Step 6: Run focused test**

Run:

```bash
cd frontend
node --test src/views/InventoryLedgerView.test.js
```

Expected: special adjustment and production out tests pass; production ledger inbound test still fails.

## Task 8: Convert Production Ledger Inbound Settlement Paper Form

**Files:**
- Modify: `frontend/src/views/InventoryLedgerView.vue`
- Modify: `frontend/src/styles/main.css`

- [ ] **Step 1: Replace settlement workflow tip and ledger select**

In `productionWorkOrderInboundDrawerOpen`, replace:

```vue
<div class="workflow-tip">
  选择生产台账后，系统按领料重量、单件毛重和已入库成品数量填入理论余料、理论废料。理论值只是辅助参考，实际称重可修改；偏差超过上下15%时需要填写说明。
</div>

<label class="form-field">
  <span>生产台账</span>
  <select v-model.number="selectedInboundWorkOrderId" required @change="loadProductionWorkOrderInboundPreview">
    <option disabled value="">请选择在生产的生产台账</option>
    <option v-for="workOrder in openProductionWorkOrders" :key="workOrder.work_order_id" :value="workOrder.work_order_id">
      {{ formatProductionWorkOrderInboundOption(workOrder) }}
    </option>
  </select>
</label>
```

with:

```vue
<div class="document-inline-summary production-ledger-inbound-tip">
  选择生产台账后，系统按领料重量、单件毛重和已入库成品数量填入理论余料、理论废料。理论值只是辅助参考，实际称重可修改；偏差超过上下15%时需要填写说明。
</div>

<div class="document-control-grid production-ledger-inbound-paper-grid">
  <label class="document-paper-field document-paper-field-wide">
    <span>生产台账</span>
    <select v-model.number="selectedInboundWorkOrderId" required @change="loadProductionWorkOrderInboundPreview">
      <option disabled value="">请选择在生产的生产台账</option>
      <option v-for="workOrder in openProductionWorkOrders" :key="workOrder.work_order_id" :value="workOrder.work_order_id">
        {{ formatProductionWorkOrderInboundOption(workOrder) }}
      </option>
    </select>
  </label>
</div>
```

- [ ] **Step 2: Add settlement summary class to preview grid**

Change:

```vue
<div v-if="productionWorkOrderInboundPreview" class="warehouse-paper-summary-grid">
```

to:

```vue
<div v-if="productionWorkOrderInboundPreview" class="warehouse-paper-summary-grid production-ledger-inbound-summary">
```

- [ ] **Step 3: Replace the triple-field input group**

Replace:

```vue
<div class="triple-field">
  <label class="form-field">
    <span>成品入库数量</span>
    <input
      v-model.number="productionWorkOrderInboundForm.finished_qty"
      type="number"
      min="0"
      step="1"
      @input="handleProductionWorkOrderInboundFinishedQtyInput"
    />
  </label>
  <label class="form-field">
    <span>余料入库重量(kg)</span>
    <input v-model.number="productionWorkOrderInboundForm.surplus_weight_kg" type="number" min="0" step="0.001" />
  </label>
  <label class="form-field">
    <span>废料重量(kg)</span>
    <input v-model.number="productionWorkOrderInboundForm.scrap_weight_kg" type="number" min="0" step="0.001" />
  </label>
</div>
```

with:

```vue
<div class="document-control-grid production-ledger-inbound-paper-grid">
  <label class="document-paper-field">
    <span>成品入库数量</span>
    <input
      v-model.number="productionWorkOrderInboundForm.finished_qty"
      type="number"
      min="0"
      step="1"
      @input="handleProductionWorkOrderInboundFinishedQtyInput"
    />
  </label>
  <label class="document-paper-field">
    <span>余料入库重量(kg)</span>
    <input v-model.number="productionWorkOrderInboundForm.surplus_weight_kg" type="number" min="0" step="0.001" />
  </label>
  <label class="document-paper-field">
    <span>废料重量(kg)</span>
    <input v-model.number="productionWorkOrderInboundForm.scrap_weight_kg" type="number" min="0" step="0.001" />
  </label>
</div>
```

- [ ] **Step 4: Replace theoretical metric grid class**

Change:

```vue
<div v-if="productionWorkOrderInboundPreview" class="drawer-row-detail-grid">
```

to:

```vue
<div v-if="productionWorkOrderInboundPreview" class="warehouse-paper-summary-grid production-ledger-inbound-metrics">
```

Then change each nested `<article>` to use:

```vue
<article class="warehouse-paper-summary-card">
```

Keep the labels and computed values unchanged.

- [ ] **Step 5: Add CSS**

Add:

```css
.production-ledger-inbound-tip,
.production-ledger-inbound-paper-grid,
.production-ledger-inbound-summary,
.production-ledger-inbound-metrics {
  grid-column: 1 / -1;
}

.production-ledger-inbound-tip {
  color: #145c39;
}

.production-ledger-inbound-metrics {
  border-top: 2px solid rgba(20, 14, 6, 0.88);
  border-left: 2px solid rgba(20, 14, 6, 0.88);
}
```

- [ ] **Step 6: Run focused test**

Run:

```bash
cd frontend
node --test src/views/InventoryLedgerView.test.js
```

Expected: all `InventoryLedgerView` tests pass.

## Task 9: Full Verification

**Files:**
- Test: `frontend/src/components/documentForms/documentForms.test.js`
- Test: `frontend/src/views/InventoryLedgerView.test.js`
- Build: `frontend/package.json`

- [ ] **Step 1: Run document component tests**

Run:

```bash
cd frontend
node --test src/components/documentForms/documentForms.test.js
```

Expected: PASS.

- [ ] **Step 2: Run warehouse view tests**

Run:

```bash
cd frontend
node --test src/views/InventoryLedgerView.test.js
```

Expected: PASS.

- [ ] **Step 3: Run frontend build**

Run:

```bash
cd frontend
npm run build
```

Expected: build succeeds. Existing Vite chunk-size warning is acceptable.

- [ ] **Step 4: Browser verify P0 documents**

Using the in-app browser at `http://localhost:5173/inventory-ledger`, verify:

- `原材料库 -> 特殊入库`: warning, reason, warehouse, add-line toolbar, and line cards visually belong to the paper document.
- `原材料库 -> 特殊出库`: same as special inbound.
- `原材料库 -> 生产出库`: product selector, source lot selector, selected lot table, and issue weight/reference quantity are paper-native.
- `工具 -> 生产台账入库`: ledger selector, preview summary, finished/surplus/scrap inputs, theoretical metrics, settle checkbox, and abnormal explanation area are paper-native.
- `原材料库 -> 客料入库`: approved template does not regress.
- `采购订单` and `销售订单`: existing paper documents still render.

## Task 10: Completion Review

**Files:**
- Inspect: `git diff --stat`
- Inspect: `git status --short`

- [ ] **Step 1: Check whitespace**

Run:

```bash
git diff --check
```

Expected: no output.

- [ ] **Step 2: Check changed files**

Run:

```bash
git status --short
```

Expected changed files should be limited to:

```text
frontend/src/components/documentForms/DocumentActionBar.vue
frontend/src/components/documentForms/DocumentField.vue
frontend/src/components/documentForms/DocumentFormShell.vue
frontend/src/components/documentForms/DocumentInlineSummary.vue
frontend/src/components/documentForms/DocumentPaper.vue
frontend/src/components/documentForms/DocumentSection.vue
frontend/src/components/documentForms/DocumentSelectorPanel.vue
frontend/src/components/documentForms/DocumentSettlePanel.vue
frontend/src/components/documentForms/WarehouseDocumentActionBar.vue
frontend/src/components/documentForms/WarehouseDocumentFormShell.vue
frontend/src/components/documentForms/WarehouseDocumentSection.vue
frontend/src/components/documentForms/documentForms.test.js
frontend/src/styles/main.css
frontend/src/views/InventoryLedgerView.test.js
frontend/src/views/InventoryLedgerView.vue
```

Plus any already-existing dirty files that predated execution.

- [ ] **Step 3: Summarize residual risks**

Report:

- Which P0 documents were browser-verified.
- Whether build passed.
- Whether any browser verification was limited by narrow viewport.
- That backend APIs and business logic were intentionally not changed.

## Self-Review

- Spec coverage: covers Phase 0 generic foundation and Phase 1 P0 warehouse documents from `2026-06-13-system-wide-paper-document-unification-design.md`.
- Placeholder scan: no placeholder markers or fill-later wording.
- Type consistency: component names are consistently `DocumentFormShell`, `DocumentSection`, `DocumentActionBar`, `DocumentField`, `DocumentSelectorPanel`, `DocumentInlineSummary`, `DocumentSettlePanel`.
- Scope guard: does not alter backend archive routes, inventory calculation, production calculation, or database schema.
