diff --git a/.github/workflows/f6-1-apply-cancel-ux.yml b/.github/workflows/f6-1-apply-cancel-ux.yml new file mode 100644 index 0000000..7bdda4a --- /dev/null +++ b/.github/workflows/f6-1-apply-cancel-ux.yml @@ -0,0 +1,113 @@ +name: F6.1 apply cancellation UX + +on: + push: + branches: + - 'hotfix/f6-1-operator-cancel-ux' + paths: + - '.github/workflows/f6-1-apply-cancel-ux.yml' + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: hotfix/f6-1-operator-cancel-ux + fetch-depth: 0 + + - name: Replace prompt cancellation with explicit audited form + run: | + python - <<'PY' + from pathlib import Path + import re + + path = Path('web-v2/src/pages/InspectionVisitEditorF4Page.tsx') + text = path.read_text() + + state_before = " const [success, setSuccess] = useState('');\n" + state_after = """ const [success, setSuccess] = useState(''); + const [showCancelForm, setShowCancelForm] = useState(false); + const [cancelReason, setCancelReason] = useState(''); +""" + if text.count(state_before) != 1: + raise SystemExit('Could not locate success state exactly once') + text = text.replace(state_before, state_after) + + lifecycle_pattern = re.compile( + r" const lifecycleAction = async \(action: 'PLAN' \| 'UNPLAN' \| 'CANCEL'\) => \{.*?\n \};\n\n if \(loading\)", + re.S, + ) + lifecycle_replacement = """ const lifecycleAction = async (action: 'PLAN' | 'UNPLAN') => { + if (!id) return; + setBusy(true); setError(''); setSuccess(''); + try { + let updated: InspectionVisit; + if (action === 'PLAN') { + updated = await planInspectionVisit(id); + setSuccess('Inspección planificada. Ya puede iniciarse desde la APK.'); + } else { + updated = await unplanInspectionVisit(id); + setSuccess('La Inspección volvió a borrador para corregir la planificación.'); + } + applyVisit(updated); + } catch (requestError) { setError(errorMessage(requestError)); } + finally { setBusy(false); } + }; + + const cancelCurrentVisit = async (event: FormEvent) => { + event.preventDefault(); + if (!id) return; + const reason = cancelReason.trim(); + if (reason.length < 10) { + setError('El motivo de cancelación debe tener al menos 10 caracteres.'); + return; + } + if (reason.length > 500) { + setError('El motivo de cancelación no puede superar los 500 caracteres.'); + return; + } + setBusy(true); setError(''); setSuccess(''); + try { + const updated = await cancelInspectionVisit(id, reason); + applyVisit(updated); + setCancelReason(''); + setShowCancelForm(false); + setSuccess('Inspección cancelada. El motivo quedó registrado y auditado.'); + } catch (requestError) { setError(errorMessage(requestError)); } + finally { setBusy(false); } + }; + + if (loading)""" + text, count = lifecycle_pattern.subn(lifecycle_replacement, text, count=1) + if count != 1: + raise SystemExit(f'Lifecycle block replacements: {count}') + + actions_pattern = re.compile( + r" \{visit &&
.*?
\}\n", + re.S, + ) + actions_replacement = """ {visit &&
Flujo de la Inspección

La oficina planifica. El inicio y cierre operativo se realizan desde la APK. Una Inspección no puede cerrarse hasta que todas sus Actas estén selladas.

{canManage && visit.status === 'DRAFT' && }{canManage && visit.status === 'PLANNED' && }{canManage && ['DRAFT', 'PLANNED', 'IN_PROGRESS'].includes(visit.status) && }
} + + {visit && showCancelForm && ['DRAFT', 'PLANNED', 'IN_PROGRESS'].includes(visit.status) &&
CANCELACIÓN

Cancelar inspección

La cancelación es definitiva y queda auditada. No se permite si la Inspección ya tiene un Acta sellada.