diff --git a/.gitignore b/.gitignore index 226d429..be34a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,9 @@ src/Backend/SolutionErp.Api/wwwroot/exports/ # Sub-agent output dumps (JSON/HTML scratch) tmp/ + +# [S76] Guard cwd-misland (feedback_agent_cwd_relative_memory_misland): sub-agent `cd` +# vào fe-user/fe-admin chạy npm build rồi ghi MEMORY.md relative-path → stray +# `fe-*/.claude/agent-memory/...`. Canonical agent-memory ở ROOT `.claude/` vẫn tracked +# (negation `!.claude/**` trên). Pattern AFTER negation → last-match-wins cho path FE-app. +fe-*/.claude/ diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 3288152..949d679 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -3,7 +3,7 @@ // Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel // → PeApprovalsSection + PeHistorySection). import { useEffect, useMemo, useRef, useState } from 'react' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useIsFetching, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' import { Check, ChevronDown, ChevronRight, Download, Eye, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' @@ -1057,9 +1057,106 @@ function BudgetBlockHeader({ children }: { children: React.ReactNode }) { ) } +// [S76] Ô tiền compact cho ma trận 3 cột — editable (input + nút Lưu) hoặc display. +// allowNegative cho "hiệu chỉnh tăng giảm" (nút ± đảo dấu). onSave nhận number|null. +function BudgetCell({ value, editable, allowNegative = false, saving, onSave }: { + value: number | null + editable: boolean + allowNegative?: boolean + saving: boolean + onSave: (v: number | null) => void +}) { + const [text, setText] = useState(value != null ? Math.abs(value).toLocaleString('vi-VN') : '') + const [neg, setNeg] = useState((value ?? 0) < 0) + useEffect(() => { + setText(value != null ? Math.abs(value).toLocaleString('vi-VN') : '') + setNeg((value ?? 0) < 0) + }, [value]) + if (!editable) { + return value != null + ? {fmtVndSigned(value)} + : + } + const parse = (): number | null => { + const n = parseVnd(text) + if (n === 0 && text.trim() === '') return null + return allowNegative && neg ? -n : n + } + const dirty = parse() !== value + return ( +
+ {allowNegative && ( + + )} + setText(e.target.value.replace(/[^\d.]/g, ''))} + placeholder="0" + className="h-7 min-w-0 px-1.5 text-right font-mono text-[12px]" + /> + +
+ ) +} + +// [S76] Dòng ghi chú phòng (Ghi chú từ PRO / từ CCM) — Textarea editable hoặc text display. +function BudgetNoteRow({ label, editable, value, setValue, savedValue, saving, onSave }: { + label: string + editable: boolean + value: string + setValue: (v: string) => void + savedValue: string | null + saving: boolean + onSave: () => void +}) { + return ( +
+
{label}
+
+ {editable ? ( +
+