[CLAUDE] FE-User+FE-Admin: action buttons (Mở chi tiết + Xóa) trên row Panel 1 Thao tác
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m35s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m35s
User feedback: thêm nút edit/action ở mỗi row trong list Panel 1 trang
Thao tác. Hiện absolute positioned ở góc phải-trên row, opacity-0 → 100
khi hover (group-hover). Sibling không nested để click không trigger
row select propagation.
## 2 button per row
- ⤴ ExternalLink → navigate /contracts/{id} (fullpage detail với
Workflow + History, khác Panel 2 chỉ có Edit form)
- 🗑 Trash2 → confirm() + DELETE /contracts/{id} (soft delete,
blocked sau DangInKy ở BE). Nếu xóa HĐ đang select → clear ?id=
## Implementation details
- pr-16 cho row button để chừa khoảng cho action group
- group-hover:opacity-100 transition (smooth fade in)
- Mutation invalidate ['my-contracts'] sau xóa thành công
- Toast success + getErrorMessage cho fail case (vd xóa HĐ đã qua DangInKy)
Build: tsc + vite pass cả 2 app (fe-user 515ms, fe-admin 937ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -12,8 +12,8 @@
|
||||
// hiển thị Chi tiết section.
|
||||
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 } from 'lucide-react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { FileText, Plus, Search, Save, ExternalLink, Trash2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab'
|
||||
import { PhaseBadge } from '@/components/PhaseBadge'
|
||||
@ -39,6 +39,8 @@ import {
|
||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||
|
||||
export function ContractCreatePage() {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : 2
|
||||
const selectedId = searchParams.get('id')
|
||||
@ -51,6 +53,23 @@ export function ContractCreatePage() {
|
||||
(await api.get<Paged<ContractListItem>>('/contracts', { params: { page: 1, pageSize: 100 } })).data,
|
||||
})
|
||||
|
||||
const deleteContract = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await api.delete(`/contracts/${id}`)
|
||||
},
|
||||
onSuccess: (_, deletedId) => {
|
||||
toast.success('Đã xóa HĐ')
|
||||
qc.invalidateQueries({ queryKey: ['my-contracts'] })
|
||||
// Nếu đang edit HĐ vừa xóa → clear selection
|
||||
if (selectedId === deletedId) {
|
||||
const next = new URLSearchParams(searchParams)
|
||||
next.delete('id')
|
||||
setSearchParams(next, { replace: true })
|
||||
}
|
||||
},
|
||||
onError: err => toast.error(getErrorMessage(err)),
|
||||
})
|
||||
|
||||
const detail = useQuery({
|
||||
queryKey: ['contract', selectedId],
|
||||
queryFn: async () => (await api.get<ContractDetail>(`/contracts/${selectedId}`)).data,
|
||||
@ -141,11 +160,11 @@ export function ContractCreatePage() {
|
||||
)}
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{rows.map(c => (
|
||||
<li key={c.id}>
|
||||
<li key={c.id} className="group relative">
|
||||
<button
|
||||
onClick={() => selectContract(c.id)}
|
||||
className={cn(
|
||||
'block w-full px-3 py-2.5 text-left transition hover:bg-slate-50',
|
||||
'block w-full px-3 py-2.5 pr-16 text-left transition hover:bg-slate-50',
|
||||
selectedId === c.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
||||
)}
|
||||
>
|
||||
@ -167,6 +186,29 @@ export function ContractCreatePage() {
|
||||
<SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Action buttons — hover-show, sibling không nested để click không trigger row select */}
|
||||
<div className="absolute right-2 top-2 z-10 flex gap-0.5 opacity-0 transition group-hover:opacity-100">
|
||||
<button
|
||||
onClick={() => navigate(`/contracts/${c.id}`)}
|
||||
title="Mở chi tiết (fullpage)"
|
||||
className="rounded p-1 text-slate-500 hover:bg-white hover:text-brand-600 hover:shadow-sm"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Xóa HĐ "${c.tenHopDong ?? c.maHopDong ?? 'này'}"?`)) {
|
||||
deleteContract.mutate(c.id)
|
||||
}
|
||||
}}
|
||||
title="Xóa HĐ"
|
||||
className="rounded p-1 text-slate-500 hover:bg-white hover:text-red-600 hover:shadow-sm"
|
||||
disabled={deleteContract.isPending}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user