[CLAUDE] FE-Admin+FE-User: PE workspace "new" — lock Loại quy trình + Select preset payment terms
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m36s
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m36s
User feedback 2026-05-07 (annotation screenshot):
1. "Gán cứng Duyệt NCC hoặc Duyệt NCC và Giải pháp theo đúng Menu" — Loại quy
trình lock theo URL ?type=N (user vào menu nào → loại đó, không chọn lại).
2. "Chỗ này vẫn hiểu code sửa lại thành select" — Điều khoản thanh toán đổi từ
Textarea (JSON code-style placeholder) → Select preset options + "Khác".
Implementation:
~ PeWorkspaceCreateView.tsx (× 2 app)
- Loại quy trình: <Select> editable → <Input disabled> hiển thị
PurchaseEvaluationTypeLabel[type] với bg-slate-100. Label đổi sang
"Loại quy trình (theo menu — khóa)" rõ ý đồ.
- Điều khoản thanh toán: <Textarea> JSON → <Select> với 8 preset:
"100% sau khi nghiệm thu" / "Tạm ứng 30% / 70%" / "Tạm ứng 50% / 50%" /
"TGN-30 ngày" / "TGN-45" / "TGN-60" / "Tiến độ theo đợt" / "Bảo hành 5%"
+ last option "Khác (nhập tay)" → khi chọn show Input text custom.
- Bỏ import Textarea (không dùng nữa).
- paymentMode local state điều khiển select; form.paymentTerms vẫn save text.
UAT mode: skip verify, push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -14,7 +14,6 @@ import { Button } from '@/components/ui/Button'
|
|||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Label } from '@/components/ui/Label'
|
import { Label } from '@/components/ui/Label'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
@ -24,6 +23,22 @@ import {
|
|||||||
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
||||||
import type { Paged, Project } from '@/types/master'
|
import type { Paged, Project } from '@/types/master'
|
||||||
|
|
||||||
|
// Preset điều khoản thanh toán phổ biến — user chọn 1 trong list, hoặc "Khác"
|
||||||
|
// để nhập tay. Save as plain text (không JSON như cũ — code-style không phù
|
||||||
|
// hợp UI cho end-user). User 2026-05-07 chỉnh.
|
||||||
|
const PAYMENT_PRESETS = [
|
||||||
|
'100% sau khi nghiệm thu',
|
||||||
|
'Tạm ứng 30% / Thanh toán 70% sau nghiệm thu',
|
||||||
|
'Tạm ứng 50% / Thanh toán 50% sau nghiệm thu',
|
||||||
|
'TGN-30 ngày (Thanh toán giao nhận 30 ngày)',
|
||||||
|
'TGN-45 ngày',
|
||||||
|
'TGN-60 ngày',
|
||||||
|
'Tiến độ theo từng đợt',
|
||||||
|
'Bảo hành 5% trong 12 tháng',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const PAYMENT_CUSTOM = '__custom__'
|
||||||
|
|
||||||
export function PeWorkspaceCreateView({
|
export function PeWorkspaceCreateView({
|
||||||
defaultType,
|
defaultType,
|
||||||
onSaved,
|
onSaved,
|
||||||
@ -48,6 +63,9 @@ export function PeWorkspaceCreateView({
|
|||||||
budgetManualName: '',
|
budgetManualName: '',
|
||||||
budgetManualAmount: 0,
|
budgetManualAmount: 0,
|
||||||
})
|
})
|
||||||
|
// Payment terms: select preset OR "Khác" → text input
|
||||||
|
const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__
|
||||||
|
const isPaymentCustom = paymentMode === PAYMENT_CUSTOM
|
||||||
|
|
||||||
const projects = useQuery({
|
const projects = useQuery({
|
||||||
queryKey: ['all-projects'],
|
queryKey: ['all-projects'],
|
||||||
@ -112,12 +130,12 @@ export function PeWorkspaceCreateView({
|
|||||||
<Section title="1. Thông tin gói thầu">
|
<Section title="1. Thông tin gói thầu">
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-[11px]">Loại quy trình *</Label>
|
<Label className="text-[11px]">Loại quy trình (theo menu — khóa)</Label>
|
||||||
<Select value={form.type} onChange={e => setForm({ ...form, type: Number(e.target.value) })}>
|
<Input
|
||||||
{Object.values(PurchaseEvaluationType).map(t => (
|
value={PurchaseEvaluationTypeLabel[form.type]}
|
||||||
<option key={t} value={t}>{PurchaseEvaluationTypeLabel[t]}</option>
|
disabled
|
||||||
))}
|
className="bg-slate-100 font-medium"
|
||||||
</Select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-1">
|
<div className="md:col-span-1">
|
||||||
<Label className="text-[11px]">a. Tên gói thầu *</Label>
|
<Label className="text-[11px]">a. Tên gói thầu *</Label>
|
||||||
@ -157,12 +175,32 @@ export function PeWorkspaceCreateView({
|
|||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label className="text-[11px]">Điều khoản thanh toán</Label>
|
<Label className="text-[11px]">Điều khoản thanh toán</Label>
|
||||||
<Textarea
|
<Select
|
||||||
rows={2}
|
value={paymentMode}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value
|
||||||
|
setPaymentMode(v)
|
||||||
|
if (v === '' || v === PAYMENT_CUSTOM) {
|
||||||
|
setForm(f => ({ ...f, paymentTerms: '' }))
|
||||||
|
} else {
|
||||||
|
setForm(f => ({ ...f, paymentTerms: v }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— Chọn điều khoản —</option>
|
||||||
|
{PAYMENT_PRESETS.map(p => (
|
||||||
|
<option key={p} value={p}>{p}</option>
|
||||||
|
))}
|
||||||
|
<option value={PAYMENT_CUSTOM}>Khác (nhập tay)</option>
|
||||||
|
</Select>
|
||||||
|
{isPaymentCustom && (
|
||||||
|
<Input
|
||||||
value={form.paymentTerms}
|
value={form.paymentTerms}
|
||||||
onChange={e => setForm({ ...form, paymentTerms: e.target.value })}
|
onChange={e => setForm({ ...form, paymentTerms: e.target.value })}
|
||||||
placeholder='{"tamUng":"10%","thanhToanTam":"100% W.done","quyetToan":"Final Account","baoHanh":"5%"}'
|
placeholder="Nhập điều khoản tùy chỉnh"
|
||||||
|
className="mt-2"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import { Button } from '@/components/ui/Button'
|
|||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Label } from '@/components/ui/Label'
|
import { Label } from '@/components/ui/Label'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
@ -24,6 +23,22 @@ import {
|
|||||||
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
||||||
import type { Paged, Project } from '@/types/master'
|
import type { Paged, Project } from '@/types/master'
|
||||||
|
|
||||||
|
// Preset điều khoản thanh toán phổ biến — user chọn 1 trong list, hoặc "Khác"
|
||||||
|
// để nhập tay. Save as plain text (không JSON như cũ — code-style không phù
|
||||||
|
// hợp UI cho end-user). User 2026-05-07 chỉnh.
|
||||||
|
const PAYMENT_PRESETS = [
|
||||||
|
'100% sau khi nghiệm thu',
|
||||||
|
'Tạm ứng 30% / Thanh toán 70% sau nghiệm thu',
|
||||||
|
'Tạm ứng 50% / Thanh toán 50% sau nghiệm thu',
|
||||||
|
'TGN-30 ngày (Thanh toán giao nhận 30 ngày)',
|
||||||
|
'TGN-45 ngày',
|
||||||
|
'TGN-60 ngày',
|
||||||
|
'Tiến độ theo từng đợt',
|
||||||
|
'Bảo hành 5% trong 12 tháng',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const PAYMENT_CUSTOM = '__custom__'
|
||||||
|
|
||||||
export function PeWorkspaceCreateView({
|
export function PeWorkspaceCreateView({
|
||||||
defaultType,
|
defaultType,
|
||||||
onSaved,
|
onSaved,
|
||||||
@ -48,6 +63,9 @@ export function PeWorkspaceCreateView({
|
|||||||
budgetManualName: '',
|
budgetManualName: '',
|
||||||
budgetManualAmount: 0,
|
budgetManualAmount: 0,
|
||||||
})
|
})
|
||||||
|
// Payment terms: select preset OR "Khác" → text input
|
||||||
|
const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__
|
||||||
|
const isPaymentCustom = paymentMode === PAYMENT_CUSTOM
|
||||||
|
|
||||||
const projects = useQuery({
|
const projects = useQuery({
|
||||||
queryKey: ['all-projects'],
|
queryKey: ['all-projects'],
|
||||||
@ -112,12 +130,12 @@ export function PeWorkspaceCreateView({
|
|||||||
<Section title="1. Thông tin gói thầu">
|
<Section title="1. Thông tin gói thầu">
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-[11px]">Loại quy trình *</Label>
|
<Label className="text-[11px]">Loại quy trình (theo menu — khóa)</Label>
|
||||||
<Select value={form.type} onChange={e => setForm({ ...form, type: Number(e.target.value) })}>
|
<Input
|
||||||
{Object.values(PurchaseEvaluationType).map(t => (
|
value={PurchaseEvaluationTypeLabel[form.type]}
|
||||||
<option key={t} value={t}>{PurchaseEvaluationTypeLabel[t]}</option>
|
disabled
|
||||||
))}
|
className="bg-slate-100 font-medium"
|
||||||
</Select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-1">
|
<div className="md:col-span-1">
|
||||||
<Label className="text-[11px]">a. Tên gói thầu *</Label>
|
<Label className="text-[11px]">a. Tên gói thầu *</Label>
|
||||||
@ -157,12 +175,32 @@ export function PeWorkspaceCreateView({
|
|||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label className="text-[11px]">Điều khoản thanh toán</Label>
|
<Label className="text-[11px]">Điều khoản thanh toán</Label>
|
||||||
<Textarea
|
<Select
|
||||||
rows={2}
|
value={paymentMode}
|
||||||
|
onChange={e => {
|
||||||
|
const v = e.target.value
|
||||||
|
setPaymentMode(v)
|
||||||
|
if (v === '' || v === PAYMENT_CUSTOM) {
|
||||||
|
setForm(f => ({ ...f, paymentTerms: '' }))
|
||||||
|
} else {
|
||||||
|
setForm(f => ({ ...f, paymentTerms: v }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— Chọn điều khoản —</option>
|
||||||
|
{PAYMENT_PRESETS.map(p => (
|
||||||
|
<option key={p} value={p}>{p}</option>
|
||||||
|
))}
|
||||||
|
<option value={PAYMENT_CUSTOM}>Khác (nhập tay)</option>
|
||||||
|
</Select>
|
||||||
|
{isPaymentCustom && (
|
||||||
|
<Input
|
||||||
value={form.paymentTerms}
|
value={form.paymentTerms}
|
||||||
onChange={e => setForm({ ...form, paymentTerms: e.target.value })}
|
onChange={e => setForm({ ...form, paymentTerms: e.target.value })}
|
||||||
placeholder='{"tamUng":"10%","thanhToanTam":"100% W.done","quyetToan":"Final Account","baoHanh":"5%"}'
|
placeholder="Nhập điều khoản tùy chỉnh"
|
||||||
|
className="mt-2"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
Reference in New Issue
Block a user