[CLAUDE] FE: ContractDetailsPreview cho create mode — table headers + disabled add
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m36s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m36s
User feedback: thay vì placeholder dashed nhỏ "Chi tiết sẽ hiện sau khi
tạo Header", show structure thật của Chi tiết section ngay từ đầu nhưng
disabled. User thấy trước layout columns + button add → trải nghiệm
liên tục, không bất ngờ khi switch sang edit mode.
## Component mới: ContractDetailsPreview
- Section title "Chi tiết ({TypeLabel})" + amber pill "🔒 Cần tạo Header trước"
- Table opacity-60 với:
- thead column headers per type (sync với HEADERS_BY_TYPE config)
- tbody empty state: Lock icon + "Tạo Header xong sẽ thêm được hạng mục"
- Disabled "+ Thêm dòng" button (cursor-not-allowed, slate-400 text)
## HEADERS_BY_TYPE config
7 type × column headers — duplicate nhỏ với ContractDetailsTab.tsx renderers
(acceptable: chỉ là labels visual, không logic).
## Reactive theo type
User đổi dropdown "Loại HĐ" → preview headers update tương ứng (state-driven).
## Build
- fe-user: tsc + vite pass (586ms)
- fe-admin: tsc + vite pass (709ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -13,7 +13,7 @@
|
||||
import { useState, useMemo, type FormEvent, useEffect } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { FileText, Plus, Search, Save, Pencil, Trash2 } from 'lucide-react'
|
||||
import { FileText, Plus, Search, Save, Pencil, Trash2, Lock } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
@ -379,13 +379,75 @@ function ContractHeaderForm({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-6 text-center text-sm text-slate-400">
|
||||
Chi tiết HĐ (line items) sẽ hiện ở đây sau khi tạo Header xong.
|
||||
</div>
|
||||
<ContractDetailsPreview type={type} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Preview section cho create mode — render structure thật của Chi tiết
|
||||
// (table headers + add button) nhưng disabled, để user thấy trước phần
|
||||
// nhập liệu sẽ unlock sau khi tạo Header xong.
|
||||
function ContractDetailsPreview({ type }: { type: number }) {
|
||||
const headers = HEADERS_BY_TYPE[type] ?? []
|
||||
const typeLabel = ContractTypeLabel[type] ?? '—'
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<FileText className="h-4 w-4" />
|
||||
Chi tiết ({typeLabel})
|
||||
<span className="ml-2 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-700">
|
||||
🔒 Cần tạo Header trước
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-200 opacity-60">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-slate-50 text-[11px] uppercase tracking-wider text-slate-500">
|
||||
<tr>
|
||||
<th className="w-10 px-2 py-2 text-left">#</th>
|
||||
{headers.map(h => <th key={h} className="px-2 py-2 text-left">{h}</th>)}
|
||||
<th className="w-10 px-2 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={headers.length + 2} className="px-3 py-12 text-center">
|
||||
<div className="flex flex-col items-center gap-2 text-slate-400">
|
||||
<Lock className="h-5 w-5" />
|
||||
<span className="text-sm">Tạo Header xong sẽ thêm được hạng mục chi tiết</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="mt-3 flex w-full cursor-not-allowed items-center justify-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-400"
|
||||
title="Tạo Header trước"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Thêm dòng
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Headers per ContractType — sync với ContractDetailsTab.tsx để preview
|
||||
// trùng với Chi tiết thật khi unlock.
|
||||
const HEADERS_BY_TYPE: Record<number, string[]> = {
|
||||
1: ['Hạng mục', 'ĐVT', 'Khối lượng', 'Đơn giá', 'Thành tiền', 'Hoàn thành', 'Ghi chú'],
|
||||
2: ['Mã CV', 'Tên công việc', 'ĐVT', 'KL', 'Đơn giá', 'Thành tiền', 'Hoàn thành'],
|
||||
3: ['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'Thành tiền', 'Giao hàng'],
|
||||
4: ['Mã DV', 'Tên DV', 'ĐVT', 'Thời gian', 'Đơn giá', 'Thành tiền'],
|
||||
5: ['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'VAT (%)', 'Thành tiền'],
|
||||
6: ['Nhóm SP', 'Tên SP', 'ĐVT', 'Giá min', 'Giá max', 'Điều kiện thanh toán'],
|
||||
7: ['Loại DV', 'Tên DV', 'ĐVT', 'Giá min', 'Giá max', 'SLA'],
|
||||
}
|
||||
|
||||
function ContractEditForm({
|
||||
contract,
|
||||
onSaved,
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
import { useState, useMemo, type FormEvent, useEffect } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { FileText, Plus, Search, Save, Pencil, Trash2 } from 'lucide-react'
|
||||
import { FileText, Plus, Search, Save, Pencil, Trash2, Lock } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
@ -379,13 +379,75 @@ function ContractHeaderForm({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-6 text-center text-sm text-slate-400">
|
||||
Chi tiết HĐ (line items) sẽ hiện ở đây sau khi tạo Header xong.
|
||||
</div>
|
||||
<ContractDetailsPreview type={type} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Preview section cho create mode — render structure thật của Chi tiết
|
||||
// (table headers + add button) nhưng disabled, để user thấy trước phần
|
||||
// nhập liệu sẽ unlock sau khi tạo Header xong.
|
||||
function ContractDetailsPreview({ type }: { type: number }) {
|
||||
const headers = HEADERS_BY_TYPE[type] ?? []
|
||||
const typeLabel = ContractTypeLabel[type] ?? '—'
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-5">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<FileText className="h-4 w-4" />
|
||||
Chi tiết ({typeLabel})
|
||||
<span className="ml-2 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-700">
|
||||
🔒 Cần tạo Header trước
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-200 opacity-60">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-slate-50 text-[11px] uppercase tracking-wider text-slate-500">
|
||||
<tr>
|
||||
<th className="w-10 px-2 py-2 text-left">#</th>
|
||||
{headers.map(h => <th key={h} className="px-2 py-2 text-left">{h}</th>)}
|
||||
<th className="w-10 px-2 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={headers.length + 2} className="px-3 py-12 text-center">
|
||||
<div className="flex flex-col items-center gap-2 text-slate-400">
|
||||
<Lock className="h-5 w-5" />
|
||||
<span className="text-sm">Tạo Header xong sẽ thêm được hạng mục chi tiết</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="mt-3 flex w-full cursor-not-allowed items-center justify-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-400"
|
||||
title="Tạo Header trước"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Thêm dòng
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Headers per ContractType — sync với ContractDetailsTab.tsx để preview
|
||||
// trùng với Chi tiết thật khi unlock.
|
||||
const HEADERS_BY_TYPE: Record<number, string[]> = {
|
||||
1: ['Hạng mục', 'ĐVT', 'Khối lượng', 'Đơn giá', 'Thành tiền', 'Hoàn thành', 'Ghi chú'],
|
||||
2: ['Mã CV', 'Tên công việc', 'ĐVT', 'KL', 'Đơn giá', 'Thành tiền', 'Hoàn thành'],
|
||||
3: ['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'Thành tiền', 'Giao hàng'],
|
||||
4: ['Mã DV', 'Tên DV', 'ĐVT', 'Thời gian', 'Đơn giá', 'Thành tiền'],
|
||||
5: ['Mã SP', 'Tên SP', 'ĐVT', 'SL', 'Đơn giá', 'VAT (%)', 'Thành tiền'],
|
||||
6: ['Nhóm SP', 'Tên SP', 'ĐVT', 'Giá min', 'Giá max', 'Điều kiện thanh toán'],
|
||||
7: ['Loại DV', 'Tên DV', 'ĐVT', 'Giá min', 'Giá max', 'SLA'],
|
||||
}
|
||||
|
||||
function ContractEditForm({
|
||||
contract,
|
||||
onSaved,
|
||||
|
||||
Reference in New Issue
Block a user