feat: Add contract preview functionality in UserDetailModal and PersonalSignContractPage
This commit is contained in:
parent
6f8573fe16
commit
0d225cb0ac
@ -46,6 +46,11 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
|
||||
const token = useAuthStore(state => state.accessToken)
|
||||
|
||||
// Contract preview state (lazy-loaded)
|
||||
const [previewLoading, setPreviewLoading] = useState(false)
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && userId && token) {
|
||||
fetchUserDetails()
|
||||
@ -133,6 +138,22 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
}
|
||||
}
|
||||
|
||||
const loadContractPreview = async () => {
|
||||
if (!userId || !token || !userDetails) return
|
||||
setPreviewLoading(true)
|
||||
setPreviewError(null)
|
||||
try {
|
||||
const html = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type)
|
||||
setPreviewHtml(html)
|
||||
} catch (e: any) {
|
||||
console.error('UserDetailModal.loadContractPreview error:', e)
|
||||
setPreviewError(e?.message || 'Failed to load contract preview')
|
||||
setPreviewHtml(null)
|
||||
} finally {
|
||||
setPreviewLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | undefined | null) => {
|
||||
if (!dateString) return 'N/A'
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
@ -367,6 +388,63 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contract Preview (admin verify flow) */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<DocumentTextIcon className="h-5 w-5 text-gray-600" />
|
||||
Contract Preview
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadContractPreview}
|
||||
disabled={previewLoading}
|
||||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
{previewLoading ? 'Loading…' : (previewHtml ? 'Refresh Preview' : 'Load Preview')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!previewHtml) return
|
||||
const blob = new Blob([previewHtml], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}}
|
||||
disabled={!previewHtml}
|
||||
className="inline-flex items-center justify-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
Open in new tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-5">
|
||||
{previewError && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 mb-4">
|
||||
{previewError}
|
||||
</div>
|
||||
)}
|
||||
{previewLoading && (
|
||||
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
|
||||
Loading preview…
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && previewHtml && (
|
||||
<div className="rounded-md border border-gray-200 overflow-hidden">
|
||||
<iframe
|
||||
title="Contract Preview"
|
||||
className="w-full h-[600px] bg-white"
|
||||
srcDoc={previewHtml}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!previewLoading && !previewHtml && !previewError && (
|
||||
<p className="text-sm text-gray-500">Click "Load Preview" to render the latest active contract template for this user.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Information */}
|
||||
{userDetails.user.user_type === 'personal' && userDetails.personalProfile && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { API_BASE_URL } from '../../../utils/api'
|
||||
|
||||
export default function PersonalSignContractPage() {
|
||||
const router = useRouter()
|
||||
@ -21,11 +22,42 @@ export default function PersonalSignContractPage() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [previewLoading, setPreviewLoading] = useState(false)
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setDate(new Date().toISOString().slice(0, 10))
|
||||
}, [])
|
||||
|
||||
// Load latest contract preview for personal user
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
if (!accessToken) return
|
||||
setPreviewLoading(true)
|
||||
setPreviewError('')
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/contracts/preview/latest`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
credentials: 'include'
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(text || 'Failed to load contract preview')
|
||||
}
|
||||
const html = await res.text()
|
||||
setPreviewHtml(html)
|
||||
} catch (e: any) {
|
||||
console.error('PersonalSignContractPage.loadPreview error:', e)
|
||||
setPreviewError(e?.message || 'Failed to load contract preview')
|
||||
} finally {
|
||||
setPreviewLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [accessToken])
|
||||
|
||||
const valid = () => {
|
||||
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
|
||||
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
||||
@ -175,15 +207,35 @@ export default function PersonalSignContractPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rounded-lg border border-gray-200 h-64 sm:h-72 bg-white relative flex items-center justify-center overflow-hidden">
|
||||
<p className="text-xs text-gray-500 text-center px-6">
|
||||
(Vertragsvorschau / PDF Platzhalter)
|
||||
<br/>Der vollständige Vertrag wird hier als PDF gerendert.
|
||||
</p>
|
||||
<div className="absolute inset-x-0 bottom-0 p-3 bg-gradient-to-t from-white via-white/90 to-transparent text-[11px] text-gray-500 text-center">
|
||||
Scroll preview (disabled in mock)
|
||||
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Vertragsvorschau</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!previewHtml) return
|
||||
const blob = new Blob([previewHtml], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}}
|
||||
disabled={!previewHtml}
|
||||
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-2.5 py-1.5 text-xs disabled:opacity-60"
|
||||
>
|
||||
Open in new tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{previewLoading ? (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Lade Vorschau…</div>
|
||||
) : previewError ? (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-red-600 px-4 text-center">{previewError}</div>
|
||||
) : previewHtml ? (
|
||||
<iframe title="Contract Preview" className="w-full h-72" srcDoc={previewHtml} />
|
||||
) : (
|
||||
<div className="h-72 flex items-center justify-center text-xs text-gray-500">Keine Vorschau verfügbar.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ export const API_ENDPOINTS = {
|
||||
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
||||
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
||||
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
|
||||
ADMIN_CONTRACT_PREVIEW: '/api/admin/contracts/:id/preview',
|
||||
}
|
||||
|
||||
// API Helper Functions
|
||||
@ -344,6 +345,21 @@ export class AdminAPI {
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company') {
|
||||
let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_PREVIEW.replace(':id', userId)
|
||||
if (userType) {
|
||||
const qs = new URLSearchParams({ userType }).toString()
|
||||
endpoint += `?${qs}`
|
||||
}
|
||||
const response = await ApiClient.get(endpoint, token)
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Failed to fetch contract preview' }))
|
||||
throw new Error(error.message || 'Failed to fetch contract preview')
|
||||
}
|
||||
// Return HTML string
|
||||
return response.text()
|
||||
}
|
||||
}
|
||||
|
||||
// Response Types
|
||||
|
||||
Loading…
Reference in New Issue
Block a user