Merge pull request 'sz/contract-mgmt' (#6) from sz/contract-mgmt into dev

Reviewed-on: #6
This commit is contained in:
Seazn 2025-11-08 14:58:56 +00:00
commit 05cbe87d60
4 changed files with 242 additions and 14 deletions

View File

@ -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">

View File

@ -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 CompanySignContractPage() {
const router = useRouter()
@ -23,11 +24,43 @@ export default function CompanySignContractPage() {
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 company user
useEffect(() => {
const loadPreview = async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
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('CompanySignContractPage.loadPreview error:', e)
setPreviewError(e?.message || 'Failed to load contract preview')
setPreviewHtml(null)
} finally {
setPreviewLoading(false)
}
}
loadPreview()
}, [accessToken])
const valid = () => {
const companyValid = companyName.trim().length >= 3 // Min 3 characters for company name
const repNameValid = repName.trim().length >= 3 // Min 3 characters for representative name
@ -183,15 +216,64 @@ export default function CompanySignContractPage() {
</div>
</div>
<div>
<div className="rounded-lg border border-gray-200 h-64 sm:h-72 bg-white flex items-center justify-center relative overflow-hidden">
<p className="text-xs text-gray-500 text-center px-6">
(Vertragsvorschau / PDF Platzhalter)
<br/>Company Contract PDF would render here.
</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 (Unternehmen)</h3>
<div className="flex items-center gap-2">
<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 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>
<button
type="button"
onClick={async () => {
if (!accessToken) return
setPreviewLoading(true)
setPreviewError(null)
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 reload preview')
}
const html = await res.text()
setPreviewHtml(html)
} catch (e: any) {
setPreviewError(e?.message || 'Failed to reload preview')
} finally {
setPreviewLoading(false)
}
}}
disabled={previewLoading}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-2.5 py-1.5 text-xs disabled:opacity-60"
>
{previewLoading ? 'Lade…' : 'Refresh'}
</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="Company 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>

View File

@ -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>

View File

@ -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