docs(fusion_plating_shopfloor): implementation plan - reuse saved Plating Signature
4 tasks: backend (load payload + sign_off persist/drop-attachment + HttpCase tests) -> FpSignatureConfirm component + manifest -> job_workspace confirm-vs-draw wiring -> entech clone-verify. Isolated worktree off main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
# Shop-Floor Sign-Off: Reuse Saved Plating Signature — 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 shop-floor step sign-off reuse the operator's saved Plating Signature (one-tap confirm) instead of redrawing every time; capture-and-persist it the first time.
|
||||
|
||||
**Architecture:** The `/fp/workspace/load` payload exposes whether the user has a Plating Signature + the image; `job_workspace.js` shows a confirm-with-preview dialog when they do (new `FpSignatureConfirm`) and the existing `FpSignaturePad` when they don't; `/fp/workspace/sign_off` persists any drawing to `res.users.x_fc_signature_image` and drops the wasted per-step attachment.
|
||||
|
||||
**Tech Stack:** Odoo 19 (`fusion_plating_shopfloor`), OWL components, JSON-RPC controller, `HttpCase` tests.
|
||||
|
||||
---
|
||||
|
||||
## Working location (IMPORTANT — isolated worktree)
|
||||
|
||||
All work happens in the worktree **`K:\Github\Odoo-Modules-signoff-wt`** on branch **`feat/shopfloor-signoff-reuse-signature`** (off `main`). Use absolute paths under that dir for Read/Edit; for git use `git -C "K:\Github\Odoo-Modules-signoff-wt" ...` (tracked prefix `fusion_plating/`). The main checkout is in use by another session — do not touch it.
|
||||
|
||||
## Testing model
|
||||
|
||||
`fusion_plating_shopfloor` can't install on the local Community box — the `HttpCase` tests run on an Enterprise env (entech clone), like the WO-grouping deploy. Local per-task gate:
|
||||
- Python: `python -m pyflakes "<file>"` (host).
|
||||
- XML: `python -c "import xml.etree.ElementTree as ET; ET.parse(r'<file>'); print('XML OK')"`.
|
||||
- JS (ESM): `node --check` rejects `import` on a `.js`; copy to a temp `.mjs` first: `Copy-Item <file> $env:TEMP\x.mjs; node --check $env:TEMP\x.mjs` (skip if `node` absent — the asset-bundle compile during the clone-verify `-u` is the real gate).
|
||||
- SCSS: no local check; Odoo compiles it on `-u` (clone-verify catches errors).
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Module | Responsibility |
|
||||
|------|--------|----------------|
|
||||
| `fusion_plating_shopfloor/controllers/workspace_controller.py` | shopfloor | `load` payload keys; `sign_off` persist + drop attachment. |
|
||||
| `fusion_plating_shopfloor/static/src/js/components/signature_confirm.js` | shopfloor | NEW confirm dialog component. |
|
||||
| `fusion_plating_shopfloor/static/src/xml/components/signature_confirm.xml` | shopfloor | NEW template. |
|
||||
| `fusion_plating_shopfloor/static/src/scss/components/_signature_confirm.scss` | shopfloor | NEW styling. |
|
||||
| `fusion_plating_shopfloor/static/src/js/job_workspace.js` | shopfloor | confirm-vs-draw wiring. |
|
||||
| `fusion_plating_shopfloor/__manifest__.py` | shopfloor | register 3 assets + version bump. |
|
||||
| `fusion_plating_shopfloor/tests/test_workspace_controller.py` | shopfloor | new HttpCase tests. |
|
||||
|
||||
**Build order:** backend (payload + sign_off + tests) → new component + manifest → workspace wiring → version bump + static checks → clone-verify.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend — load payload + sign_off rewrite + tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `fusion_plating_shopfloor/controllers/workspace_controller.py` (load return dict ~line 241; `sign_off` ~line 450-494)
|
||||
- Test: `fusion_plating_shopfloor/tests/test_workspace_controller.py`
|
||||
|
||||
- [ ] **Step 1: Add the load payload keys.** In `workspace_controller.py`, the `load` method's `return {` dict starts with `'ok': True,` (around line 241-242). Insert these two keys immediately after the `'ok': True,` line, at the same indentation:
|
||||
|
||||
```python
|
||||
'user_has_plating_signature': bool(env.user.x_fc_signature_image),
|
||||
'user_plating_signature': (
|
||||
('data:image/png;base64,%s' % env.user.x_fc_signature_image.decode())
|
||||
if env.user.x_fc_signature_image else ''
|
||||
),
|
||||
```
|
||||
|
||||
(`env` is already bound at the top of `load`. `x_fc_signature_image` is in `SELF_READABLE_FIELDS`, so reading `env.user`'s own value is allowed.)
|
||||
|
||||
- [ ] **Step 2: Rewrite `sign_off`.** Replace the entire `sign_off` method (the `@http.route('/fp/workspace/sign_off', ...)` decorator + method, lines ~450-494) with:
|
||||
|
||||
```python
|
||||
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
||||
def sign_off(self, step_id, signature_data_uri=None):
|
||||
env = request.env
|
||||
step = env['fp.job.step'].browse(int(step_id))
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': f'Step {step_id} not found'}
|
||||
|
||||
sig = (signature_data_uri or '').strip()
|
||||
user = env.user
|
||||
if sig:
|
||||
# A drawing was supplied (first-time, or "use a different
|
||||
# signature"). Persist it as the user's Plating Signature so
|
||||
# every future sign-off + report reuses it. x_fc_signature_image
|
||||
# is in SELF_WRITEABLE_FIELDS, so writing one's own is allowed.
|
||||
if ',' in sig and sig.startswith('data:'):
|
||||
sig = sig.split(',', 1)[1]
|
||||
try:
|
||||
user.write({'x_fc_signature_image': sig})
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"workspace/sign_off: persisting Plating Signature failed for uid %s",
|
||||
env.uid,
|
||||
)
|
||||
return {'ok': False, 'error': 'Failed to save your signature.'}
|
||||
elif not user.x_fc_signature_image:
|
||||
# No drawing AND no saved signature — nothing to sign with.
|
||||
return {
|
||||
'ok': False,
|
||||
'error': 'A signature is required. Draw one to continue.',
|
||||
}
|
||||
|
||||
try:
|
||||
step.button_finish()
|
||||
except Exception as exc:
|
||||
_logger.exception("workspace/sign_off: button_finish failed")
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
_logger.info("Step %s signed off by uid %s", step.id, env.uid)
|
||||
return {'ok': True, 'step_id': step.id, 'state': step.state}
|
||||
```
|
||||
|
||||
(Note: `signature_data_uri` is now optional; the per-step `ir.attachment` create is gone.)
|
||||
|
||||
- [ ] **Step 3: Write the tests.** Append to `fusion_plating_shopfloor/tests/test_workspace_controller.py` (the file already defines `_rpc`, `_TINY_PNG_B64`, and the `@tagged` decorator at the top — reuse them):
|
||||
|
||||
```python
|
||||
@tagged('-at_install', 'post_install', 'fp_shopfloor')
|
||||
class TestWorkspaceSignOff(HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.authenticate("admin", "admin")
|
||||
self.partner = self.env['res.partner'].create({'name': 'Sig Cust'})
|
||||
self.product = self.env['product.product'].create({'name': 'Sig Prod'})
|
||||
self.job = self.env['fp.job'].create({
|
||||
'name': 'WH/JOB/SIG001',
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 3,
|
||||
})
|
||||
|
||||
def test_load_exposes_plating_signature_flags(self):
|
||||
self.env.user.x_fc_signature_image = False
|
||||
res = _rpc(self, '/fp/workspace/load', job_id=self.job.id)
|
||||
self.assertFalse(res['user_has_plating_signature'])
|
||||
self.assertEqual(res['user_plating_signature'], '')
|
||||
self.env.user.x_fc_signature_image = _TINY_PNG_B64
|
||||
res2 = _rpc(self, '/fp/workspace/load', job_id=self.job.id)
|
||||
self.assertTrue(res2['user_has_plating_signature'])
|
||||
self.assertTrue(
|
||||
res2['user_plating_signature'].startswith('data:image/png;base64,'))
|
||||
|
||||
def test_sign_off_without_signature_and_no_saved_errors(self):
|
||||
self.env.user.x_fc_signature_image = False
|
||||
step = self.env['fp.job.step'].create({
|
||||
'job_id': self.job.id, 'name': 'Final', 'sequence': 10})
|
||||
res = _rpc(self, '/fp/workspace/sign_off', step_id=step.id)
|
||||
self.assertFalse(res['ok'])
|
||||
self.assertIn('signature', res['error'].lower())
|
||||
|
||||
def test_sign_off_with_drawing_persists_signature_and_no_attachment(self):
|
||||
self.env.user.x_fc_signature_image = False
|
||||
step = self.env['fp.job.step'].create({
|
||||
'job_id': self.job.id, 'name': 'Final', 'sequence': 10})
|
||||
data_uri = 'data:image/png;base64,' + _TINY_PNG_B64
|
||||
# button_finish may fail on this un-started step; we assert the
|
||||
# signature-persist + no-attachment side effects, which happen first.
|
||||
_rpc(self, '/fp/workspace/sign_off',
|
||||
step_id=step.id, signature_data_uri=data_uri)
|
||||
self.env.user.invalidate_recordset(['x_fc_signature_image'])
|
||||
self.assertTrue(
|
||||
self.env.user.x_fc_signature_image,
|
||||
'drawing persisted to the Plating Signature')
|
||||
n = self.env['ir.attachment'].search_count([
|
||||
('res_model', '=', 'fp.job.step'), ('res_id', '=', step.id)])
|
||||
self.assertEqual(n, 0, 'no per-step signature attachment is created')
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Static check.** Run:
|
||||
```
|
||||
python -m pyflakes "K:\Github\Odoo-Modules-signoff-wt\fusion_plating\fusion_plating_shopfloor\controllers\workspace_controller.py" "K:\Github\Odoo-Modules-signoff-wt\fusion_plating\fusion_plating_shopfloor\tests\test_workspace_controller.py"
|
||||
```
|
||||
Expected: clean (ignore pre-existing warnings on lines you didn't touch).
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
```
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" add fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py fusion_plating/fusion_plating_shopfloor/tests/test_workspace_controller.py
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" commit -m "feat(fusion_plating_shopfloor): sign_off reuses+persists Plating Signature; load exposes it"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: New `FpSignatureConfirm` component + manifest registration
|
||||
|
||||
**Files:**
|
||||
- Create: `fusion_plating_shopfloor/static/src/js/components/signature_confirm.js`
|
||||
- Create: `fusion_plating_shopfloor/static/src/xml/components/signature_confirm.xml`
|
||||
- Create: `fusion_plating_shopfloor/static/src/scss/components/_signature_confirm.scss`
|
||||
- Modify: `fusion_plating_shopfloor/__manifest__.py` (assets list, after the `signature_pad.*` block ~line 81; version)
|
||||
|
||||
- [ ] **Step 1: Create the JS component.**
|
||||
|
||||
```js
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — SignatureConfirm
|
||||
//
|
||||
// Confirm dialog shown when the operator already has a saved Plating
|
||||
// Signature: previews it + "Sign & Finish" (props.onConfirm) or "Use a
|
||||
// different signature" (props.onRedraw, opens the draw-pad). No drawing here.
|
||||
// =============================================================================
|
||||
import { Component } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
|
||||
export class FpSignatureConfirm extends Component {
|
||||
static template = "fusion_plating_shopfloor.SignatureConfirm";
|
||||
static components = { Dialog };
|
||||
static props = {
|
||||
close: Function, // dialog service injects
|
||||
title: { type: String, optional: true },
|
||||
contextLabel: { type: String, optional: true },
|
||||
signatureUrl: { type: String }, // data: URI of saved sig
|
||||
onConfirm: { type: Function }, // () => commit (no drawing)
|
||||
onRedraw: { type: Function }, // () => open draw-pad
|
||||
};
|
||||
|
||||
onConfirm() {
|
||||
this.props.onConfirm();
|
||||
this.props.close();
|
||||
}
|
||||
onRedraw() {
|
||||
this.props.onRedraw();
|
||||
this.props.close();
|
||||
}
|
||||
onCancel() {
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the XML template.**
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating_shopfloor.SignatureConfirm">
|
||||
<Dialog title="props.title or 'Confirm signature'" size="'md'">
|
||||
<div class="o_fp_sig_confirm">
|
||||
<div class="o_fp_sig_ctx" t-if="props.contextLabel">
|
||||
<t t-esc="props.contextLabel"/>
|
||||
</div>
|
||||
<div class="o_fp_sig_preview">
|
||||
<img t-att-src="props.signatureUrl" alt="Your saved signature"/>
|
||||
</div>
|
||||
<div class="o_fp_sig_hint">Your saved Plating Signature will be applied.</div>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-link" t-on-click="onRedraw">Use a different signature</button>
|
||||
<button class="btn btn-link" t-on-click="onCancel">Cancel</button>
|
||||
<button class="btn btn-primary" t-on-click="onConfirm">Sign & Finish</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create the SCSS.**
|
||||
|
||||
```scss
|
||||
// Confirm-with-preview dialog for shop-floor sign-off. Explicit hex per the
|
||||
// project card-styling rule (don't rely on var(--bs-border-color)).
|
||||
.o_fp_sig_confirm {
|
||||
.o_fp_sig_ctx {
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.o_fp_sig_preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
padding: 8px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d8dadd;
|
||||
border-radius: 4px;
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 160px;
|
||||
}
|
||||
}
|
||||
.o_fp_sig_hint {
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Register assets + bump version** in `__manifest__.py`. Immediately after the three `signature_pad.*` lines (the `.scss`, `.xml`, `.js` block ending ~line 81), insert:
|
||||
|
||||
```python
|
||||
'fusion_plating_shopfloor/static/src/scss/components/_signature_confirm.scss',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/signature_confirm.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/signature_confirm.js',
|
||||
```
|
||||
|
||||
And change `'version': '19.0.37.1.0',` → `'version': '19.0.37.2.0',`.
|
||||
|
||||
- [ ] **Step 5: Static checks.**
|
||||
```
|
||||
python -c "import xml.etree.ElementTree as ET; ET.parse(r'K:\Github\Odoo-Modules-signoff-wt\fusion_plating\fusion_plating_shopfloor\static\src\xml\components\signature_confirm.xml'); print('XML OK')"
|
||||
```
|
||||
Expected: `XML OK`. (Optional JS check: copy `signature_confirm.js` to `$env:TEMP\x.mjs` and `node --check` it if `node` is present.)
|
||||
|
||||
- [ ] **Step 6: Commit.**
|
||||
```
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" add fusion_plating/fusion_plating_shopfloor/static/src/js/components/signature_confirm.js fusion_plating/fusion_plating_shopfloor/static/src/xml/components/signature_confirm.xml fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_signature_confirm.scss fusion_plating/fusion_plating_shopfloor/__manifest__.py
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" commit -m "feat(fusion_plating_shopfloor): FpSignatureConfirm dialog + asset registration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Wire confirm-vs-draw into `job_workspace.js`
|
||||
|
||||
**Files:**
|
||||
- Modify: `fusion_plating_shopfloor/static/src/js/job_workspace.js` (import ~line 27; `static components` ~line 41; `onFinishStep` ~line 364-392)
|
||||
|
||||
- [ ] **Step 1: Import the new component.** After the existing `import { FpSignaturePad } from "./components/signature_pad";` (line 27), add:
|
||||
|
||||
```js
|
||||
import { FpSignatureConfirm } from "./components/signature_confirm";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register it in `static components`.** In the `static components = { ... };` line (~41), add `FpSignatureConfirm` to the set (e.g. right after `FpSignaturePad`):
|
||||
|
||||
```js
|
||||
static components = { WorkflowChip, GateViz, FpSignaturePad, FpSignatureConfirm, FpHoldComposer, FpTabletLock, FpRackPartsDialog, FpDamageDialog, FpFinishBlockDialog, RackingPanel, FpMovePartsDialog };
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace `onFinishStep` and add two helpers.** Replace the whole `onFinishStep(step)` method (currently lines ~364-392, the `if (step.requires_signoff) { this.dialog.add(FpSignaturePad, {...}); return; } await this._callFinishStep(step, false);`) with:
|
||||
|
||||
```js
|
||||
async onFinishStep(step) {
|
||||
if (step.requires_signoff) {
|
||||
if (this.state.data.user_has_plating_signature) {
|
||||
// One-tap confirm with preview of the saved Plating Signature.
|
||||
this.dialog.add(FpSignatureConfirm, {
|
||||
title: `Sign to finish ${step.name}`,
|
||||
contextLabel: `${this.state.data.job.display_wo_name} · Step ${step.sequence_display}: ${step.name}`,
|
||||
signatureUrl: this.state.data.user_plating_signature,
|
||||
onConfirm: () => this._commitSignOff(step, null), // use saved
|
||||
onRedraw: () => this._openSignaturePad(step), // draw a new one
|
||||
});
|
||||
} else {
|
||||
// First time — draw once; the backend persists it.
|
||||
this._openSignaturePad(step);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Plain finish — routes through /fp/workspace/finish_step which
|
||||
// returns structured errors so we can show the FpFinishBlockDialog.
|
||||
await this._callFinishStep(step, /* bypass */ false);
|
||||
}
|
||||
|
||||
_openSignaturePad(step) {
|
||||
this.dialog.add(FpSignaturePad, {
|
||||
title: `Sign to finish ${step.name}`,
|
||||
contextLabel: `${this.state.data.job.display_wo_name} · Step ${step.sequence_display}: ${step.name}`,
|
||||
onSubmit: (dataUri) => this._commitSignOff(step, dataUri),
|
||||
});
|
||||
}
|
||||
|
||||
async _commitSignOff(step, dataUri) {
|
||||
try {
|
||||
const res = await fpRpc("/fp/workspace/sign_off", {
|
||||
step_id: step.id,
|
||||
signature_data_uri: dataUri, // null -> backend uses the saved signature
|
||||
});
|
||||
if (res && res.ok) {
|
||||
this.notification.add("Step signed off and finished.", { type: "success" });
|
||||
await this.refresh();
|
||||
} else {
|
||||
this.notification.add((res && res.error) || "Sign-off failed", { type: "danger" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(err.message, { type: "danger" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(`fpRpc`, `this.dialog`, `this.notification`, `this.refresh`, `this._callFinishStep` all already exist in this component — verify the imports/usages are unchanged.)
|
||||
|
||||
- [ ] **Step 4: Static check (optional JS).** Copy `job_workspace.js` to `$env:TEMP\x.mjs` and `node --check $env:TEMP\x.mjs` if `node` is present; otherwise rely on the clone-verify asset compile.
|
||||
|
||||
- [ ] **Step 5: Commit.**
|
||||
```
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" add fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js
|
||||
git -C "K:\Github\Odoo-Modules-signoff-wt" commit -m "feat(fusion_plating_shopfloor): workspace sign-off confirms saved signature, draws only when absent"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Verify on an entech clone
|
||||
|
||||
**Files:** none (verification only). Mirror the WO-grouping clone-verify recipe.
|
||||
|
||||
- [ ] **Step 1: Clone + upgrade + tests.** On entech: clone `admin` → throwaway UTF-8 DB (`createdb -O odoo -E UTF8 -T template0 --lc-collate=C --lc-ctype=C`, then `pg_dump admin | psql`), stage this branch's `fusion_plating_shopfloor` files into `/mnt/extra-addons/custom/fusion_plating_shopfloor`, then:
|
||||
```
|
||||
odoo -c /etc/odoo/odoo.conf -d <clone> -u fusion_plating_shopfloor --test-enable \
|
||||
--test-tags /fusion_plating_shopfloor:TestWorkspaceSignOff --stop-after-init \
|
||||
--workers=0 --http-port=0 --gevent-port=0 --log-level=test
|
||||
```
|
||||
Expected: exit 0; the 3 new tests pass. (Run the full `/fusion_plating_shopfloor` suite + a baseline diff if any failures appear, to confirm they're pre-existing — same technique as the WO-grouping deploy.)
|
||||
|
||||
- [ ] **Step 2: Asset compile sanity.** Confirm the `-u` compiled the backend bundle without SCSS/XML errors (no `CRITICAL`/`Failed to load` for `signature_confirm`).
|
||||
|
||||
- [ ] **Step 3: Browser smoke (clone or post-deploy).** As a tech with **no** Plating Signature: finish a `requires_signoff` step → draw-pad appears → draw → their `x_fc_signature_image` is set (query DB). Finish another sign-off step → the **confirm-with-preview** dialog appears (no pad) → Sign & Finish works. Render that job's WO Detail → the saved signature shows.
|
||||
|
||||
- [ ] **Step 4: Mark complete.** Suite green + smoke confirmed → ready to deploy `fusion_plating_shopfloor` to entech (standard recipe: backup, stage, `-u`, cache-bust, restart, gated on exit 0).
|
||||
|
||||
---
|
||||
|
||||
## Self-review (by plan author)
|
||||
|
||||
- **Spec coverage:** load payload keys (Task 1) ✓; sign_off optional URI + persist + drop attachment (Task 1) ✓; `FpSignatureConfirm` (Task 2) ✓; workspace confirm-vs-draw + "use a different signature" replaces saved (Task 3) ✓; manifest assets + version (Task 2) ✓; tablet-only scope, no model/migration ✓.
|
||||
- **Placeholder scan:** no TBD/TODO; every code step has complete code; `<clone>` in Task 4 is an explicit env parameter.
|
||||
- **Type/name consistency:** `signature_data_uri` (optional, default None) consistent across controller + JS; payload keys `user_has_plating_signature` / `user_plating_signature` consistent between controller (Task 1), workspace `this.state.data.*` (Task 3); `FpSignatureConfirm` props (`signatureUrl`, `onConfirm`, `onRedraw`) consistent between the component (Task 2) and its caller (Task 3); `_commitSignOff` / `_openSignaturePad` defined and used in Task 3.
|
||||
Reference in New Issue
Block a user