# Drawer Table Readability 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:** Make all ERP drawers easier to read by eliminating ugly horizontal scrolling, vertical table headers, random wrapping, and overloaded drawer tables.

**Architecture:** First add global drawer/table readability guardrails so existing drawers immediately stop collapsing columns. Then introduce a reusable `DrawerDataTable` component for drawer tables with fixed layout, compact labels, ellipsis cells, optional expanded detail rows, and consistent column types. Finally migrate the highest-impact warehouse product overview and inventory ledger drawer tables, leaving the component ready for the rest of the ERP.

**Tech Stack:** Vue 3 single-file components, existing `FormDrawer`, existing `TableControls`, existing `PaginationBar`, global CSS in `frontend/src/styles/main.css`, Vite build.

---

## File Structure

- Modify: `frontend/src/components/FormDrawer.vue`
  - Add a `size` prop while keeping the old `wide` prop working.
  - Supported sizes: `normal`, `wide`, `xl`, `fullscreen`.

- Create: `frontend/src/components/DrawerDataTable.vue`
  - Reusable drawer table renderer.
  - Accepts rows, columns, row key, empty text, and optional expandable row details.
  - Provides named cell slots like `cell-lot_no` and `cell-status`.

- Modify: `frontend/src/styles/main.css`
  - Add drawer width classes.
  - Add drawer table readability CSS.
  - Prevent vertical Chinese headers.
  - Add cell helpers for ellipsis, numeric alignment, chip-like long codes, and detail grids.

- Modify: `frontend/src/views/InventoryLedgerView.vue`
  - Use `size="fullscreen"` or `size="xl"` for complex inventory detail drawers.
  - Convert the product overview lot table, inventory batch detail table, and inventory transaction table to `DrawerDataTable`.
  - Reduce visible columns to the core fields and move secondary fields to expandable detail.

- Test: `frontend/scripts/test-drawer-data-table.mjs`
  - Static test that confirms the new component exists and includes required slot/column/detail behavior.

- Test: existing `frontend/scripts/test-inventory-ledger-options.mjs`
  - Must still pass.

---

### Task 1: Add Drawer Size Levels

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

- [ ] **Step 1: Update `FormDrawer.vue` props and class binding**

Replace the current aside class binding:

```vue
<aside class="workflow-drawer form-drawer" :class="{ 'form-drawer-wide': wide }">
```

with:

```vue
<aside class="workflow-drawer form-drawer" :class="drawerClass">
```

Add this script code after `const emit = defineEmits(["close"]);`:

```js
const drawerClass = computed(() => {
  const normalizedSize = ["normal", "wide", "xl", "fullscreen"].includes(props.size) ? props.size : "normal";
  return {
    "form-drawer-wide": props.wide || normalizedSize === "wide",
    "form-drawer-xl": normalizedSize === "xl",
    "form-drawer-fullscreen": normalizedSize === "fullscreen"
  };
});
```

Update the import:

```js
import { computed, toRef } from "vue";
```

Add this prop:

```js
size: {
  type: String,
  default: "normal"
}
```

- [ ] **Step 2: Add drawer width CSS**

Append near the existing drawer/form-drawer CSS in `frontend/src/styles/main.css`:

```css
.form-drawer-xl {
  width: min(1280px, calc(100vw - 40px));
  max-width: calc(100vw - 40px);
}

.form-drawer-fullscreen {
  width: min(1680px, calc(100vw - 28px));
  max-width: calc(100vw - 28px);
  height: min(940px, calc(100vh - 28px));
}

.form-drawer-fullscreen .form-drawer-body,
.form-drawer-xl .form-drawer-body {
  min-width: 0;
}
```

- [ ] **Step 3: Build check**

Run:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
npm run build
```

Expected: build succeeds.

---

### Task 2: Add Global Drawer Table Guardrails

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

- [ ] **Step 1: Add drawer table readability CSS**

Append after the existing `.data-table` styles:

```css
.workflow-drawer .table-wrap {
  max-width: 100%;
  overflow-x: hidden;
}

.workflow-drawer .data-table {
  min-width: 0;
  table-layout: fixed;
}

.workflow-drawer .data-table th {
  writing-mode: horizontal-tb;
  white-space: normal;
  word-break: keep-all;
  overflow-wrap: normal;
  line-height: 1.35;
  vertical-align: middle;
}

.workflow-drawer .data-table td {
  min-width: 0;
  vertical-align: middle;
  word-break: normal;
  overflow-wrap: anywhere;
}

.drawer-cell-ellipsis {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-cell-code {
  display: inline-flex;
  max-width: 100%;
  padding: 4px 8px;
  border-radius: 999px;
  background: #eef6ff;
  color: #1f5fa8;
  font-weight: 650;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-cell-number {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
```

- [ ] **Step 2: Verify screenshots manually**

Run the frontend, open an existing drawer, and confirm:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
npm run dev
```

Expected:
- Headers no longer display one Chinese character per line.
- Tables inside drawers no longer force immediate horizontal scroll for normal-width content.
- Long batch numbers are allowed to truncate rather than deform the whole table.

---

### Task 3: Create `DrawerDataTable`

**Files:**
- Create: `frontend/src/components/DrawerDataTable.vue`
- Create: `frontend/scripts/test-drawer-data-table.mjs`

- [ ] **Step 1: Write the static component test**

Create `frontend/scripts/test-drawer-data-table.mjs`:

```js
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

const componentPath = resolve(process.cwd(), "src/components/DrawerDataTable.vue");
const source = readFileSync(componentPath, "utf8");

const requiredSnippets = [
  "defineProps",
  "columns",
  "rows",
  "rowKey",
  "expandedRows",
  "cellClass",
  "slot :name=\"`cell-${column.key}`\"",
  "drawer-data-table",
  "drawer-row-detail"
];

for (const snippet of requiredSnippets) {
  if (!source.includes(snippet)) {
    throw new Error(`DrawerDataTable missing required snippet: ${snippet}`);
  }
}

console.log("drawer data table static tests passed");
```

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

Run:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
node scripts/test-drawer-data-table.mjs
```

Expected: FAIL because `src/components/DrawerDataTable.vue` does not exist.

- [ ] **Step 3: Create the component**

Create `frontend/src/components/DrawerDataTable.vue`:

```vue
<template>
  <div class="drawer-data-table-wrap">
    <table class="data-table compact-table drawer-data-table">
      <colgroup>
        <col v-for="column in visibleColumns" :key="column.key" :style="{ width: column.width || 'auto' }" />
        <col v-if="expandable" style="width: 72px" />
      </colgroup>
      <thead>
        <tr>
          <th v-for="column in visibleColumns" :key="column.key" :title="column.title || column.label">
            <span class="drawer-table-head-label">{{ column.compactLabel || column.label }}</span>
          </th>
          <th v-if="expandable">详情</th>
        </tr>
      </thead>
      <tbody>
        <template v-for="row in rows" :key="getRowKey(row)">
          <tr>
            <td v-for="column in visibleColumns" :key="column.key" :class="cellClass(column)">
              <slot :name="`cell-${column.key}`" :row="row" :column="column" :value="row[column.key]">
                <span :class="fallbackClass(column)" :title="formatFallback(row, column)">
                  {{ formatFallback(row, column) }}
                </span>
              </slot>
            </td>
            <td v-if="expandable" class="drawer-cell-actions">
              <button class="mini-icon-button" type="button" @click="toggleRow(row)">
                {{ isExpanded(row) ? "收起" : "展开" }}
              </button>
            </td>
          </tr>
          <tr v-if="expandable && isExpanded(row)" class="drawer-row-detail">
            <td :colspan="visibleColumns.length + 1">
              <slot name="detail" :row="row">
                <div class="drawer-row-detail-grid">
                  <article v-for="column in detailColumns" :key="column.key">
                    <span>{{ column.label }}</span>
                    <strong>{{ formatFallback(row, column) }}</strong>
                  </article>
                </div>
              </slot>
            </td>
          </tr>
        </template>
        <tr v-if="!rows.length">
          <td :colspan="visibleColumns.length + (expandable ? 1 : 0)" class="empty-row">{{ emptyText }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

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

const props = defineProps({
  rows: {
    type: Array,
    default: () => []
  },
  columns: {
    type: Array,
    default: () => []
  },
  rowKey: {
    type: [String, Function],
    default: "id"
  },
  expandedRows: {
    type: Array,
    default: () => []
  },
  emptyText: {
    type: String,
    default: "暂无数据"
  }
});

const emit = defineEmits(["update:expandedRows"]);

const visibleColumns = computed(() => props.columns.filter((column) => !column.hidden && !column.detailOnly));
const detailColumns = computed(() => props.columns.filter((column) => column.detailOnly));
const expandable = computed(() => detailColumns.value.length > 0);

function getRowKey(row) {
  return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
}

function isExpanded(row) {
  return props.expandedRows.includes(getRowKey(row));
}

function toggleRow(row) {
  const key = getRowKey(row);
  const nextRows = isExpanded(row) ? props.expandedRows.filter((item) => item !== key) : [...props.expandedRows, key];
  emit("update:expandedRows", nextRows);
}

function cellClass(column) {
  return {
    "drawer-cell-number": column.type === "number" || column.align === "right",
    "drawer-cell-status": column.type === "status",
    "drawer-cell-long": column.type === "code" || column.type === "longText"
  };
}

function fallbackClass(column) {
  return {
    "drawer-cell-code": column.type === "code",
    "drawer-cell-ellipsis": column.type === "longText" || column.type === "code"
  };
}

function formatFallback(row, column) {
  if (typeof column.format === "function") {
    return column.format(row[column.key], row);
  }
  const value = row?.[column.key];
  return value === null || value === undefined || value === "" ? "-" : String(value);
}
</script>
```

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

Append to `frontend/src/styles/main.css`:

```css
.drawer-data-table-wrap {
  width: 100%;
  max-width: 100%;
  overflow: hidden;
}

.drawer-data-table {
  width: 100%;
  min-width: 0;
  table-layout: fixed;
}

.drawer-data-table th,
.drawer-data-table td {
  min-width: 0;
}

.drawer-table-head-label {
  display: inline-block;
  max-width: 100%;
  line-height: 1.35;
  white-space: normal;
  word-break: keep-all;
}

.drawer-cell-actions {
  text-align: center;
}

.mini-icon-button {
  border: 1px solid #d9e6f5;
  border-radius: 999px;
  background: #f5f9ff;
  color: #2f74c8;
  font-size: 12px;
  padding: 4px 9px;
  cursor: pointer;
}

.drawer-row-detail td {
  background: #f8fbff;
}

.drawer-row-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}

.drawer-row-detail-grid article {
  display: grid;
  gap: 4px;
  padding: 10px 12px;
  border: 1px solid #e4edf7;
  border-radius: 12px;
  background: #ffffff;
}

.drawer-row-detail-grid span {
  color: #7a8699;
  font-size: 12px;
}

.drawer-row-detail-grid strong {
  color: #1f2d3d;
  font-size: 13px;
  overflow-wrap: anywhere;
}
```

- [ ] **Step 5: Run static component test**

Run:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
node scripts/test-drawer-data-table.mjs
```

Expected: PASS with `drawer data table static tests passed`.

---

### Task 4: Migrate Warehouse Drawer Tables

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

- [ ] **Step 1: Import `DrawerDataTable` and add expansion state**

Add import:

```js
import DrawerDataTable from "../components/DrawerDataTable.vue";
```

Add refs near the other table page refs:

```js
const expandedLotRows = ref([]);
const expandedTxnRows = ref([]);
```

- [ ] **Step 2: Add column definitions**

Add computed columns near the table-related computed values:

```js
const lotDrawerColumns = computed(() => [
  { key: "lot_no", label: "库存批次号", compactLabel: "批次号", type: "code", width: "220px" },
  ...(isSelectedFinishedWarehouse.value ? [{ key: "inbound_qty", label: "入库数量", compactLabel: "入库数", type: "number", width: "96px", format: (_value, row) => formatQty(row.inbound_qty) }] : []),
  ...(selectedWeightOnly.value ? [] : [{ key: "remaining_qty", label: isSelectedFinishedWarehouse.value ? "剩余数量" : "变动数量", compactLabel: isSelectedFinishedWarehouse.value ? "剩余数" : "变动数", type: "number", width: "96px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_qty, "PIECE") : formatQty(row.remaining_qty) }]),
  { key: "inbound_weight_kg", label: "入库重量(kg)", compactLabel: "入库重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.inbound_weight_kg, "WEIGHT") : formatWeight(row.inbound_weight_kg) },
  { key: "remaining_weight_kg", label: "剩余重量(kg)", compactLabel: "剩余重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_weight_kg, "WEIGHT") : formatWeight(row.remaining_weight_kg) },
  { key: "unit_cost", label: selectedUnitCostColumnLabel.value, compactLabel: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
  { key: "status", label: "状态", type: "status", width: "90px" },
  { key: "warehouse_name", label: "仓库", detailOnly: true },
  { key: "source_purchase_order_no", label: "采购单号", detailOnly: true },
  { key: "source_receipt_no", label: "入库单号", detailOnly: true },
  { key: "source_material_summary", label: "来源库存批次", detailOnly: true },
  { key: "logistics_waybill_no", label: "运单号", detailOnly: true },
  { key: "logistics_freight_amount", label: "运费", detailOnly: true, format: (_value, row) => formatFreight(row.logistics_freight_amount) },
  { key: "quality_release", label: "质检放行人", detailOnly: true, format: (_value, row) => row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }
]);

const txnDrawerColumns = computed(() => [
  { key: "biz_time", label: "时间", type: "longText", width: "128px", format: (_value, row) => formatDateTime(row.biz_time) },
  { key: "txn_type", label: "类型", width: "94px", format: (_value, row) => formatInventoryTxnTypeLabel(row.txn_type) },
  { key: "lot_no", label: "库存批次号", compactLabel: "批次号", type: "code", width: "220px" },
  ...(selectedWeightOnly.value ? [] : [{ key: "qty_change", label: "变动数量", compactLabel: "变动数", type: "number", width: "96px", format: (_value, row) => formatQty(row.qty_change) }]),
  { key: "weight_change_kg", label: "变动重量(kg)", compactLabel: "变动重量", type: "number", width: "110px", format: (_value, row) => formatWeight(row.weight_change_kg) },
  { key: "unit_cost", label: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
  { key: "amount", label: "金额", type: "number", width: "104px", format: (_value, row) => `¥${formatAmount(row.amount)}` },
  { key: "source", label: "来源", detailOnly: true, format: (_value, row) => `${formatSourceDocTypeLabel(row.source_doc_type)} #${row.source_doc_id || "-"}` },
  { key: "remark", label: "备注", detailOnly: true }
]);
```

- [ ] **Step 3: Replace the lot detail table**

Replace the current `库存批次明细` table markup with:

```vue
<DrawerDataTable
  v-model:expanded-rows="expandedLotRows"
  :rows="paginatedLots"
  :columns="lotDrawerColumns"
  row-key="lot_id"
  :empty-text="`暂无该${selectedDetailEntityLabel}库存批次`"
>
  <template #cell-status="{ row }">
    <span class="status-pill" :class="lotStatusClass(row)">{{ formatStatusLabel(row.status) }}</span>
  </template>
  <template #cell-lot_no="{ row }">
    <span class="drawer-cell-code" :title="row.lot_no">{{ row.lot_no || "-" }}</span>
  </template>
  <template #detail="{ row }">
    <div class="drawer-row-detail-grid">
      <article><span>仓库</span><strong>{{ row.warehouse_name || "-" }}</strong></article>
      <article><span>采购单号</span><strong>{{ row.source_purchase_order_no || "-" }}</strong></article>
      <article><span>入库单号</span><strong>{{ row.source_receipt_no || "-" }}</strong></article>
      <article><span>来源库存批次</span><strong>{{ row.source_material_summary || formatLotSourceLots(row) }}</strong></article>
      <article><span>运单号</span><strong>{{ row.logistics_waybill_no || "-" }}</strong></article>
      <article><span>运费</span><strong>{{ formatFreight(row.logistics_freight_amount) }}</strong></article>
      <article><span>质检放行人</span><strong>{{ row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }}</strong></article>
    </div>
  </template>
</DrawerDataTable>
```

- [ ] **Step 4: Replace the transaction table**

Replace the current `库存流水` table markup with:

```vue
<DrawerDataTable
  v-model:expanded-rows="expandedTxnRows"
  :rows="paginatedTransactions"
  :columns="txnDrawerColumns"
  row-key="inventory_txn_id"
  :empty-text="`暂无该${selectedDetailEntityLabel}库存流水`"
>
  <template #cell-lot_no="{ row }">
    <span class="drawer-cell-code" :title="row.lot_no || ''">{{ row.lot_no || "-" }}</span>
  </template>
  <template #detail="{ row }">
    <div class="drawer-row-detail-grid">
      <article><span>来源</span><strong>{{ formatSourceDocTypeLabel(row.source_doc_type) }} #{{ row.source_doc_id || "-" }}</strong></article>
      <article><span>备注</span><strong>{{ row.remark || "-" }}</strong></article>
    </div>
  </template>
</DrawerDataTable>
```

- [ ] **Step 5: Use a larger drawer size for complex inventory detail**

Find the warehouse detail drawer container in `InventoryLedgerView.vue`. If it uses a custom dual drawer mask, add a class that enables fullscreen width:

```vue
<div v-if="detailDrawerOpen" class="workflow-drawer-mask dual-drawer-mask inventory-ledger-detail-mask" @click.self="closeDetailDrawer">
```

Add CSS:

```css
.inventory-ledger-detail-mask .dual-drawer-layout {
  width: min(1760px, calc(100vw - 28px));
  max-width: calc(100vw - 28px);
}

.inventory-ledger-detail-mask .workflow-drawer {
  min-width: 0;
}
```

- [ ] **Step 6: Run focused frontend tests**

Run:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
node scripts/test-drawer-data-table.mjs
node scripts/test-inventory-ledger-options.mjs
npm run build
```

Expected:
- Static drawer table test passes.
- Existing inventory option test passes.
- Vite build passes.

---

### Task 5: Apply the Same Pattern to Other Drawers

**Files:**
- Modify high-impact drawer pages after warehouse migration:
  - `frontend/src/views/PurchaseOrderView.vue`
  - `frontend/src/views/DeliveryManagementView.vue`
  - `frontend/src/views/WorkOrderManagementView.vue`
  - `frontend/src/views/CompletionReceiptView.vue`
  - `frontend/src/components/ProductWorkflowDrawer.vue`

- [ ] **Step 1: Identify drawer tables that exceed 7 columns**

Run:

```bash
cd /Volumes/ForgeFlow-ERP
rg -n "<table class=\"data-table|<table class=\"data-table compact-table" frontend/src/views frontend/src/components
```

Expected: list all drawer tables. Prioritize tables inside `FormDrawer`, `.workflow-drawer`, or custom drawer masks.

- [ ] **Step 2: For each drawer table, classify columns**

Use this rule:

```text
Main table: 5-7 business-critical columns.
Detail row: long text, source documents, remarks, logistics, photos, secondary IDs.
Status: always visible as colored pill.
Money/quantity/weight: visible only when it is part of the user's immediate decision.
```

- [ ] **Step 3: Migrate one page at a time**

For each target page:

1. Import `DrawerDataTable`.
2. Add `expandedRows` ref.
3. Add `columns` computed.
4. Replace raw drawer table with `DrawerDataTable`.
5. Keep existing `TableControls` and `PaginationBar`.
6. Run `npm run build`.

Example import and state:

```js
import DrawerDataTable from "../components/DrawerDataTable.vue";

const expandedRows = ref([]);
```

- [ ] **Step 4: Stop when all common drawers follow the new pattern**

Acceptance criteria:
- No drawer table header displays vertically.
- No drawer table requires horizontal scrolling for the default visible columns.
- Long codes are shown as chips or ellipsis with `title`.
- Secondary details are visible by expanding the row.

---

### Task 6: Verification

**Files:**
- Test: `frontend/scripts/test-drawer-data-table.mjs`
- Test: `frontend/scripts/test-inventory-ledger-options.mjs`

- [ ] **Step 1: Run static and build checks**

Run:

```bash
cd /Volumes/ForgeFlow-ERP/frontend
node scripts/test-drawer-data-table.mjs
node scripts/test-inventory-ledger-options.mjs
npm run build
```

Expected: all pass.

- [ ] **Step 2: Manual browser verification**

Open the ERP in the browser and check:

```text
1. 嘉恒仓库 -> 成品库 -> 产品明细抽屉
2. 库存批次明细表
3. 库存流水表
4. 采购订单详情抽屉
5. 发货/销售出库详情抽屉
```

Expected:
- No vertical table headers.
- No ugly horizontal scrollbar for normal data.
- Long batch numbers do not deform the table.
- Row expand details reveal hidden fields clearly.
- Pagination bar remains visually consistent.

---

## Self-Review

- Spec coverage: The plan covers drawer width, table header readability, horizontal-scroll reduction, long-code handling, detail expansion, and migration of warehouse plus other high-impact drawers.
- Placeholder scan: No `TBD`, `TODO`, or unspecified “handle later” steps remain.
- Type consistency: `DrawerDataTable` props are consistently named `rows`, `columns`, `rowKey`, `expandedRows`, and `emptyText`; slot names use `cell-${column.key}` and `detail`.
- Risk note: The current project directory is mounted at `/Volumes/ForgeFlow-ERP` and is not detected as a git repository, so commit steps are intentionally omitted.
