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 [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
|
||||||
const token = useAuthStore(state => state.accessToken)
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen && userId && token) {
|
if (isOpen && userId && token) {
|
||||||
fetchUserDetails()
|
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) => {
|
const formatDate = (dateString: string | undefined | null) => {
|
||||||
if (!dateString) return 'N/A'
|
if (!dateString) return 'N/A'
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
@ -367,6 +388,63 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Profile Information */}
|
||||||
{userDetails.user.user_type === 'personal' && userDetails.personalProfile && (
|
{userDetails.user.user_type === 'personal' && userDetails.personalProfile && (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
<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 PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||||
|
import { API_BASE_URL } from '../../../utils/api'
|
||||||
|
|
||||||
export default function PersonalSignContractPage() {
|
export default function PersonalSignContractPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -21,11 +22,42 @@ export default function PersonalSignContractPage() {
|
|||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [previewLoading, setPreviewLoading] = useState(false)
|
||||||
|
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||||
|
const [previewError, setPreviewError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDate(new Date().toISOString().slice(0, 10))
|
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 valid = () => {
|
||||||
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
|
const nameValid = fullName.trim().length >= 3 // Min 3 characters for name
|
||||||
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
const locationValid = location.trim().length >= 2 // Min 2 characters for location
|
||||||
@ -175,14 +207,34 @@ export default function PersonalSignContractPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div className="rounded-lg border border-gray-200 bg-white relative overflow-hidden">
|
||||||
<p className="text-xs text-gray-500 text-center px-6">
|
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
|
||||||
(Vertragsvorschau / PDF Platzhalter)
|
<h3 className="text-sm font-semibold text-gray-900">Vertragsvorschau</h3>
|
||||||
<br/>Der vollständige Vertrag wird hier als PDF gerendert.
|
<div className="flex items-center gap-2">
|
||||||
</p>
|
<button
|
||||||
<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">
|
type="button"
|
||||||
Scroll preview (disabled in mock)
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export const API_ENDPOINTS = {
|
|||||||
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
||||||
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
||||||
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
|
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
|
||||||
|
ADMIN_CONTRACT_PREVIEW: '/api/admin/contracts/:id/preview',
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Helper Functions
|
// API Helper Functions
|
||||||
@ -344,6 +345,21 @@ export class AdminAPI {
|
|||||||
}
|
}
|
||||||
return response.json()
|
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
|
// Response Types
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user