From 2bba851135e98056de6c43f471626c80dadc3c6a Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 11 May 2026 10:04:49 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-PE:=20Chunk=20B=20=E2=80=94=20NCC?= =?UTF-8?q?=20nested=20expand=20d=C6=B0=E1=BB=9Bi=20H=E1=BA=A1ng=20m?= =?UTF-8?q?=E1=BB=A5c,=20b=E1=BB=8F=20Section=204=20ri=C3=AAng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure ItemsTab → nested cards (tầng 1 = hạng mục, tầng 2 = NCC tham gia + báo giá inline). Replace bảng matrix grid (hạng mục × NCC) cũ — user Session 20 yêu cầu UX nested cho 1 hạng mục demo. FE (mirror fe-admin + fe-user): - ItemsTab giờ render list HangMucCard (1 card / 1 hạng mục) - HangMucCard mới: header info hạng mục + expand panel default OPEN - Expand panel: NCC inline table columns: NCC | Liên hệ | Điều khoản TT | File báo giá | ĐG chưa VAT | ĐG có VAT | Thành tiền | Action - Quote click cell → QuoteDialog cũ (reuse) - NCC button: + Thêm NCC (AddSupplierDialog) / ✏ Sửa (EditSupplierDialog) / ✓ Winner / 🗑 Xóa - Budget Δ compute per-card (KHÔNG tfoot tổng — 1 hạng mục) - Bỏ Section 4 "NCC tham gia" cũ trong main render — gộp vào Section 2 - Drop function SuppliersTab (dead code) — giữ AddSupplierDialog + EditSupplierDialog + SupplierAttachmentsCell cho HangMucCard reuse Section layout mới (4 section): 1. Thông tin gói thầu 2. Hạng mục + Báo giá NCC (nested) 3. Chọn NCC / TP thắng thầu 4. Ý kiến cấp duyệt Verify: - npm run build × fe-admin pass (warning chunk size, không liên quan) - npm run build × fe-user pass - Test pass mặc định skip (Phase 9 UAT iteration, Q4 user public luôn) Pending Chunk C: Section 5 (rename 4) gộp đồng cấp cùng Phòng — 1 box / Step Pending Chunk D: Docs S20 changelog + STATUS + HANDOFF Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 580 ++++++++++---------- fe-user/src/components/pe/PeDetailTabs.tsx | 580 ++++++++++---------- 2 files changed, 570 insertions(+), 590 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 2d1689e..bae82d4 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' -import { Check, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' +import { Check, ChevronDown, ChevronRight, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' import { Input } from '@/components/ui/Input' @@ -161,23 +161,20 @@ export function PeDetailTabs({
- {/* Section order (Session 20): Hạng mục lên đầu sau Thông tin gói thầu. - BE auto-tạo 1 hạng mục mặc định (tên = TenGoiThau, giá trị = ngân sách) - khi Create. NCC tham gia tạm giữ riêng — Chunk B sẽ gộp NCC nested - expand dưới mỗi hạng mục. */} + {/* Section layout (Session 20 Chunk B): Hạng mục nested expand chứa NCC + (tầng 1 = hạng mục, tầng 2 = NCC tham gia + báo giá inline). NCC + tham gia section riêng bỏ — gộp vào Section 2 expand panel. Tên + hạng mục + giá trị auto từ gói thầu (Chunk A BE seed). */}
-
+
-
+
-
- -
-
+
{mode === 'workspace' && (
Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu “Duyệt” để ký. @@ -1037,139 +1034,9 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun ) } -// ===== Tab: NCC ===== -function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { - const qc = useQueryClient() - const [open, setOpen] = useState(false) - const [editRow, setEditRow] = useState(null) - - const remove = useMutation({ - mutationFn: async (rowId: string) => api.delete(`/purchase-evaluations/${ev.id}/suppliers/${rowId}`), - onSuccess: () => { toast.success('Đã xóa NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - const setWinner = useMutation({ - mutationFn: async (supplierId: string) => - api.post(`/purchase-evaluations/${ev.id}/select-winner`, { supplierId }), - onSuccess: () => { toast.success('Đã chọn NCC thắng.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - - return ( -
- {!readOnly && ( -
- -
- )} - {ev.suppliers.length === 0 ? ( -

- {readOnly ? 'Chưa có NCC.' : 'Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.'} -

- ) : ( -
- - - - - - - - {!readOnly && } - - - - {ev.suppliers.map(s => ( - - - - - - {!readOnly && (() => { - // User 2026-05-07: NCC đã được chọn (winner) → KHÔNG cho - // sửa/xóa (tránh thay đổi NCC đã chốt). Chỉ hiển thị - // checkmark active state. - // User 2026-05-07 (B11+): NCC đã có hạng mục báo giá (quotes - // entered in Section 4) → KHÔNG cho xóa (tránh mất báo giá đã nhập). - const isWinner = ev.selectedSupplierId === s.supplierId - const hasQuotes = ev.details.some(d => d.quotes.some(q => q.purchaseEvaluationSupplierId === s.id)) - const canDelete = !isWinner && !hasQuotes - return ( - - ) - })()} - - ))} - -
NCCLiên hệĐiều khoản TTFile đính kèm
-
{s.supplierName}
- {s.displayName &&
{s.displayName}
} - {s.note &&
{s.note}
} - {readOnly && ev.selectedSupplierId === s.supplierId && ( -
✓ NCC được chọn
- )} -
- {s.contactName &&
{s.contactName}
} - {s.contactPhone &&
{s.contactPhone}
} - {s.contactEmail &&
{s.contactEmail}
} -
{s.paymentTermText ?? '—'} - a.purchaseEvaluationSupplierId === s.id)} - readOnly={readOnly} - /> - -
- - {!isWinner && ( - - )} - {canDelete ? ( - - ) : !isWinner && hasQuotes && ( - - - - )} -
-
-
- )} - - {open && setOpen(false)} />} - {editRow && setEditRow(null)} />} -
- ) -} +// Session 20 Chunk B: SuppliersTab function bỏ — NCC list giờ render nested +// trong HangMucCard (expand panel mỗi hạng mục). 2 dialog Add/Edit Supplier +// vẫn giữ vì HangMucCard call lại. function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; onClose: () => void }) { const qc = useQueryClient() @@ -1263,24 +1130,14 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri ) } -// ===== Tab: Hạng mục + Báo giá (matrix) ===== +// ===== Tab: Hạng mục + Báo giá (Session 20 — nested cards layout) ===== +// Mỗi hạng mục = 1 card với expand panel chứa NCC tham gia inline grid. +// Replace bảng matrix grid (hạng mục × NCC) cũ — user demo 1 hạng mục. function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { - const qc = useQueryClient() const [addOpen, setAddOpen] = useState(false) const [editDetail, setEditDetail] = useState(null) - const [quoteEdit, setQuoteEdit] = useState<{ detail: PeDetailRow; supplier: PeSupplier; existing: PeQuote | null } | null>(null) - - const removeDetail = useMutation({ - mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${ev.id}/details/${id}`), - onSuccess: () => { toast.success('Đã xóa hạng mục.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - - const quoteKey = (detailId: string, supplierRowId: string) => - ev.details.find(d => d.id === detailId)?.quotes.find(q => q.purchaseEvaluationSupplierId === supplierRowId) ?? null // Budget comparison — fetch full Budget bundle nếu có link để so sánh per-row. - // Match key: groupCode|itemCode (case-sensitive match; itemCode null cho phép). const budgetBundle = useQuery({ queryKey: ['budget-detail-for-pe', ev.budgetId], queryFn: async () => (await api.get<{ details: { groupCode: string; itemCode: string | null; thanhTien: number }[]; tongNganSach: number }>( @@ -1295,18 +1152,13 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo return m })() const showBudgetCol = !!ev.budgetId - const totalPeNganSach = ev.details.reduce((sum, d) => sum + d.thanhTienNganSach, 0) - const totalBudget = budgetBundle.data?.tongNganSach ?? 0 return (

- {ev.suppliers.length === 0 - ? (readOnly ? 'Chưa có NCC tham gia.' : 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.') - : readOnly - ? `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC` - : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`} + {ev.details.length} hạng mục · {ev.suppliers.length} NCC tham gia + {!readOnly && ' — mở hạng mục để thêm NCC + nhập báo giá.'}

{!readOnly && ( +
+
+ {detail.groupCode} + {detail.noiDung} +
+
+ {detail.groupName}{detail.donViTinh ? ` · ĐVT: ${detail.donViTinh}` : ''} +
+
+
+
+
KL
+
{detail.khoiLuongNganSach}
+
+
+
ĐG ngân sách
+
{fmtMoney(detail.donGiaNganSach)}
+
+
+
Thành tiền NS
+
{fmtMoney(detail.thanhTienNganSach)}
+
+ {showBudgetCol && bgValue != null && ( +
+
NS link
+
{fmtMoney(bgValue)}
+
0 && 'text-red-600', + delta! < 0 && 'text-emerald-600', + delta === 0 && 'text-slate-500', + )}> + Δ {delta! > 0 ? '+' : ''}{fmtMoney(delta!)} +
+
+ )} +
+ {!readOnly && ( +
+ + +
+ )} +
+ + {/* Expand panel — NCC tham gia + báo giá inline */} + {expanded && ( +
+
+
+ NCC tham gia ({ev.suppliers.length}) +
+ {!readOnly && ( + + )} +
+ + {ev.suppliers.length === 0 ? ( +

+ {readOnly ? 'Chưa có NCC tham gia.' : 'Chưa có NCC. Thêm NCC để nhập báo giá.'} +

+ ) : ( +
+ + + + + + + + + + + {!readOnly && } + + + + {ev.suppliers.map(s => { + const q = detail.quotes.find(x => x.purchaseEvaluationSupplierId === s.id) ?? null + const isWinner = ev.selectedSupplierId === s.supplierId + const hasQuotes = ev.details.some(dd => dd.quotes.some(qq => qq.purchaseEvaluationSupplierId === s.id)) + const canDelete = !isWinner && !hasQuotes + const openQuote = () => setQuoteEdit({ supplier: s, existing: q }) + const cellHover = !readOnly && 'cursor-pointer hover:bg-brand-50' + return ( + + + + + + + + + {!readOnly && ( + + )} + + ) + })} + +
NCCLiên hệĐiều khoản TTFile báo giáĐG chưa VATĐG có VATThành tiền
+
+ {isWinner && }{s.supplierName} +
+ {s.displayName &&
{s.displayName}
} + {s.note &&
{s.note}
} +
+ {s.contactName &&
{s.contactName}
} + {s.contactPhone &&
{s.contactPhone}
} + {s.contactEmail &&
{s.contactEmail}
} + {!s.contactName && !s.contactPhone && !s.contactEmail && } +
+ {s.paymentTermText ?? } + + a.purchaseEvaluationSupplierId === s.id)} + readOnly={readOnly} + /> + + {q ? fmtMoney(q.chuaVat) : } + + {q ? fmtMoney(q.bgVat) : } + + {q ? fmtMoney(q.thanhTien) : } + +
+ + {!isWinner && ( + + )} + {canDelete ? ( + + ) : !isWinner && hasQuotes && ( + + + + )} +
+
+
+ )} +
+ )} + + {addNccOpen && setAddNccOpen(false)} />} + {editNccRow && setEditNccRow(null)} />} {quoteEdit && ( setQuoteEdit(null)} /> diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 2d1689e..bae82d4 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' -import { Check, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' +import { Check, ChevronDown, ChevronRight, Paperclip, Pencil, Plus, Trash2, Upload } from 'lucide-react' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' import { Input } from '@/components/ui/Input' @@ -161,23 +161,20 @@ export function PeDetailTabs({
- {/* Section order (Session 20): Hạng mục lên đầu sau Thông tin gói thầu. - BE auto-tạo 1 hạng mục mặc định (tên = TenGoiThau, giá trị = ngân sách) - khi Create. NCC tham gia tạm giữ riêng — Chunk B sẽ gộp NCC nested - expand dưới mỗi hạng mục. */} + {/* Section layout (Session 20 Chunk B): Hạng mục nested expand chứa NCC + (tầng 1 = hạng mục, tầng 2 = NCC tham gia + báo giá inline). NCC + tham gia section riêng bỏ — gộp vào Section 2 expand panel. Tên + hạng mục + giá trị auto từ gói thầu (Chunk A BE seed). */}
-
+
-
+
-
- -
-
+
{mode === 'workspace' && (
Ý kiến + chữ ký auto đồng bộ khi NV duyệt phiếu — vào menu “Duyệt” để ký. @@ -1037,139 +1034,9 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun ) } -// ===== Tab: NCC ===== -function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { - const qc = useQueryClient() - const [open, setOpen] = useState(false) - const [editRow, setEditRow] = useState(null) - - const remove = useMutation({ - mutationFn: async (rowId: string) => api.delete(`/purchase-evaluations/${ev.id}/suppliers/${rowId}`), - onSuccess: () => { toast.success('Đã xóa NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - const setWinner = useMutation({ - mutationFn: async (supplierId: string) => - api.post(`/purchase-evaluations/${ev.id}/select-winner`, { supplierId }), - onSuccess: () => { toast.success('Đã chọn NCC thắng.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - - return ( -
- {!readOnly && ( -
- -
- )} - {ev.suppliers.length === 0 ? ( -

- {readOnly ? 'Chưa có NCC.' : 'Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.'} -

- ) : ( -
- - - - - - - - {!readOnly && } - - - - {ev.suppliers.map(s => ( - - - - - - {!readOnly && (() => { - // User 2026-05-07: NCC đã được chọn (winner) → KHÔNG cho - // sửa/xóa (tránh thay đổi NCC đã chốt). Chỉ hiển thị - // checkmark active state. - // User 2026-05-07 (B11+): NCC đã có hạng mục báo giá (quotes - // entered in Section 4) → KHÔNG cho xóa (tránh mất báo giá đã nhập). - const isWinner = ev.selectedSupplierId === s.supplierId - const hasQuotes = ev.details.some(d => d.quotes.some(q => q.purchaseEvaluationSupplierId === s.id)) - const canDelete = !isWinner && !hasQuotes - return ( - - ) - })()} - - ))} - -
NCCLiên hệĐiều khoản TTFile đính kèm
-
{s.supplierName}
- {s.displayName &&
{s.displayName}
} - {s.note &&
{s.note}
} - {readOnly && ev.selectedSupplierId === s.supplierId && ( -
✓ NCC được chọn
- )} -
- {s.contactName &&
{s.contactName}
} - {s.contactPhone &&
{s.contactPhone}
} - {s.contactEmail &&
{s.contactEmail}
} -
{s.paymentTermText ?? '—'} - a.purchaseEvaluationSupplierId === s.id)} - readOnly={readOnly} - /> - -
- - {!isWinner && ( - - )} - {canDelete ? ( - - ) : !isWinner && hasQuotes && ( - - - - )} -
-
-
- )} - - {open && setOpen(false)} />} - {editRow && setEditRow(null)} />} -
- ) -} +// Session 20 Chunk B: SuppliersTab function bỏ — NCC list giờ render nested +// trong HangMucCard (expand panel mỗi hạng mục). 2 dialog Add/Edit Supplier +// vẫn giữ vì HangMucCard call lại. function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; onClose: () => void }) { const qc = useQueryClient() @@ -1263,24 +1130,14 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri ) } -// ===== Tab: Hạng mục + Báo giá (matrix) ===== +// ===== Tab: Hạng mục + Báo giá (Session 20 — nested cards layout) ===== +// Mỗi hạng mục = 1 card với expand panel chứa NCC tham gia inline grid. +// Replace bảng matrix grid (hạng mục × NCC) cũ — user demo 1 hạng mục. function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { - const qc = useQueryClient() const [addOpen, setAddOpen] = useState(false) const [editDetail, setEditDetail] = useState(null) - const [quoteEdit, setQuoteEdit] = useState<{ detail: PeDetailRow; supplier: PeSupplier; existing: PeQuote | null } | null>(null) - - const removeDetail = useMutation({ - mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${ev.id}/details/${id}`), - onSuccess: () => { toast.success('Đã xóa hạng mục.'); qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) }, - onError: e => toast.error(getErrorMessage(e)), - }) - - const quoteKey = (detailId: string, supplierRowId: string) => - ev.details.find(d => d.id === detailId)?.quotes.find(q => q.purchaseEvaluationSupplierId === supplierRowId) ?? null // Budget comparison — fetch full Budget bundle nếu có link để so sánh per-row. - // Match key: groupCode|itemCode (case-sensitive match; itemCode null cho phép). const budgetBundle = useQuery({ queryKey: ['budget-detail-for-pe', ev.budgetId], queryFn: async () => (await api.get<{ details: { groupCode: string; itemCode: string | null; thanhTien: number }[]; tongNganSach: number }>( @@ -1295,18 +1152,13 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo return m })() const showBudgetCol = !!ev.budgetId - const totalPeNganSach = ev.details.reduce((sum, d) => sum + d.thanhTienNganSach, 0) - const totalBudget = budgetBundle.data?.tongNganSach ?? 0 return (

- {ev.suppliers.length === 0 - ? (readOnly ? 'Chưa có NCC tham gia.' : 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.') - : readOnly - ? `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC` - : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`} + {ev.details.length} hạng mục · {ev.suppliers.length} NCC tham gia + {!readOnly && ' — mở hạng mục để thêm NCC + nhập báo giá.'}

{!readOnly && ( +
+
+ {detail.groupCode} + {detail.noiDung} +
+
+ {detail.groupName}{detail.donViTinh ? ` · ĐVT: ${detail.donViTinh}` : ''} +
+
+
+
+
KL
+
{detail.khoiLuongNganSach}
+
+
+
ĐG ngân sách
+
{fmtMoney(detail.donGiaNganSach)}
+
+
+
Thành tiền NS
+
{fmtMoney(detail.thanhTienNganSach)}
+
+ {showBudgetCol && bgValue != null && ( +
+
NS link
+
{fmtMoney(bgValue)}
+
0 && 'text-red-600', + delta! < 0 && 'text-emerald-600', + delta === 0 && 'text-slate-500', + )}> + Δ {delta! > 0 ? '+' : ''}{fmtMoney(delta!)} +
+
+ )} +
+ {!readOnly && ( +
+ + +
+ )} +
+ + {/* Expand panel — NCC tham gia + báo giá inline */} + {expanded && ( +
+
+
+ NCC tham gia ({ev.suppliers.length}) +
+ {!readOnly && ( + + )} +
+ + {ev.suppliers.length === 0 ? ( +

+ {readOnly ? 'Chưa có NCC tham gia.' : 'Chưa có NCC. Thêm NCC để nhập báo giá.'} +

+ ) : ( +
+ + + + + + + + + + + {!readOnly && } + + + + {ev.suppliers.map(s => { + const q = detail.quotes.find(x => x.purchaseEvaluationSupplierId === s.id) ?? null + const isWinner = ev.selectedSupplierId === s.supplierId + const hasQuotes = ev.details.some(dd => dd.quotes.some(qq => qq.purchaseEvaluationSupplierId === s.id)) + const canDelete = !isWinner && !hasQuotes + const openQuote = () => setQuoteEdit({ supplier: s, existing: q }) + const cellHover = !readOnly && 'cursor-pointer hover:bg-brand-50' + return ( + + + + + + + + + {!readOnly && ( + + )} + + ) + })} + +
NCCLiên hệĐiều khoản TTFile báo giáĐG chưa VATĐG có VATThành tiền
+
+ {isWinner && }{s.supplierName} +
+ {s.displayName &&
{s.displayName}
} + {s.note &&
{s.note}
} +
+ {s.contactName &&
{s.contactName}
} + {s.contactPhone &&
{s.contactPhone}
} + {s.contactEmail &&
{s.contactEmail}
} + {!s.contactName && !s.contactPhone && !s.contactEmail && } +
+ {s.paymentTermText ?? } + + a.purchaseEvaluationSupplierId === s.id)} + readOnly={readOnly} + /> + + {q ? fmtMoney(q.chuaVat) : } + + {q ? fmtMoney(q.bgVat) : } + + {q ? fmtMoney(q.thanhTien) : } + +
+ + {!isWinner && ( + + )} + {canDelete ? ( + + ) : !isWinner && hasQuotes && ( + + + + )} +
+
+
+ )} +
+ )} + + {addNccOpen && setAddNccOpen(false)} />} + {editNccRow && setEditNccRow(null)} />} {quoteEdit && ( setQuoteEdit(null)} />