[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 { useState, useMemo, type FormEvent, useEffect } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
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 { toast } from 'sonner'
|
||||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
@ -379,13 +379,75 @@ function ContractHeaderForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-6 text-center text-sm text-slate-400">
|
<ContractDetailsPreview type={type} />
|
||||||
Chi tiết HĐ (line items) sẽ hiện ở đây sau khi tạo Header xong.
|
|
||||||
</div>
|
|
||||||
</form>
|
</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({
|
function ContractEditForm({
|
||||||
contract,
|
contract,
|
||||||
onSaved,
|
onSaved,
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
import { useState, useMemo, type FormEvent, useEffect } from 'react'
|
import { useState, useMemo, type FormEvent, useEffect } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
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 { toast } from 'sonner'
|
||||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||||
@ -379,13 +379,75 @@ function ContractHeaderForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-6 text-center text-sm text-slate-400">
|
<ContractDetailsPreview type={type} />
|
||||||
Chi tiết HĐ (line items) sẽ hiện ở đây sau khi tạo Header xong.
|
|
||||||
</div>
|
|
||||||
</form>
|
</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({
|
function ContractEditForm({
|
||||||
contract,
|
contract,
|
||||||
onSaved,
|
onSaved,
|
||||||
|
|||||||
Reference in New Issue
Block a user