Files
Stirling-PDF/frontend/src/proprietary/components/workflow/ParticipantView.tsx
T
Anthony StirlingGitHubConnorYohConnor YohEthanHealy01Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
28613caf8a fileshare (#5414)
Co-authored-by: ConnorYoh <[email protected]>
Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: EthanHealy01 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-25 11:00:40 +00:00

280 lines
8.4 KiB
TypeScript

import React, { useState } from 'react';
import {
Stack,
Card,
Text,
Badge,
Group,
Button,
Loader,
Alert,
TextInput,
FileInput,
Select,
} from '@mantine/core';
import { useParticipantSession } from '@app/hooks/workflow/useParticipantSession';
import InfoIcon from '@mui/icons-material/Info';
import DownloadIcon from '@mui/icons-material/Download';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
interface ParticipantViewProps {
token: string;
}
const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
const { session, participant, loading, error, submitSignature, decline, downloadDocument } =
useParticipantSession(token);
const [certType, setCertType] = useState<string>('P12');
const [password, setPassword] = useState<string>('');
const [certFile, setCertFile] = useState<File | null>(null);
const [location, setLocation] = useState<string>('');
const [reason, setReason] = useState<string>('Document Signing');
const [showSignature, _setShowSignature] = useState<boolean>(true);
const [pageNumber, setPageNumber] = useState<number>(1);
const [declineReason, _setDeclineReason] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const handleSubmitSignature = async () => {
if (!certFile && certType !== 'SERVER') {
setNotification({ type: 'error', message: 'Please select a certificate file' });
return;
}
setIsSubmitting(true);
setNotification(null);
try {
await submitSignature({
participantToken: token,
certType,
password,
p12File: certType === 'P12' ? certFile || undefined : undefined,
jksFile: certType === 'JKS' ? certFile || undefined : undefined,
showSignature,
pageNumber,
location,
reason,
showLogo: true,
});
setNotification({ type: 'success', message: 'Signature submitted successfully!' });
} catch (err: any) {
setNotification({ type: 'error', message: `Failed to submit signature: ${err.message}` });
} finally {
setIsSubmitting(false);
}
};
const handleDecline = async () => {
if (window.confirm('Are you sure you want to decline signing this document?')) {
setNotification(null);
try {
await decline(token, declineReason || 'Declined by participant');
setNotification({ type: 'success', message: 'You have declined this signing request.' });
} catch (err: any) {
setNotification({ type: 'error', message: `Failed to decline: ${err.message}` });
}
}
};
if (loading && !session) {
return (
<Stack align="center" justify="center" p="xl">
<Loader size="lg" />
<Text c="dimmed">Loading session...</Text>
</Stack>
);
}
if (error) {
return (
<Alert icon={<InfoIcon fontSize="small" />} color="red" title="Error">
{error}
</Alert>
);
}
if (!session || !participant) {
return (
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
Session not found or access denied.
</Alert>
);
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'SIGNED':
return <Badge color="green">Signed</Badge>;
case 'DECLINED':
return <Badge color="red">Declined</Badge>;
case 'VIEWED':
return <Badge color="blue">Viewed</Badge>;
case 'NOTIFIED':
return <Badge color="yellow">Notified</Badge>;
case 'PENDING':
return <Badge color="gray">Pending</Badge>;
default:
return <Badge>{status}</Badge>;
}
};
const canSign = !participant.hasCompleted && !participant.isExpired && session.status === 'IN_PROGRESS';
return (
<Stack gap="md">
{notification && (
<Alert
icon={notification.type === 'success' ? <CheckCircleIcon fontSize="small" /> : <InfoIcon fontSize="small" />}
color={notification.type === 'success' ? 'green' : 'red'}
withCloseButton
onClose={() => setNotification(null)}
>
{notification.message}
</Alert>
)}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Stack gap="sm">
<Group justify="space-between">
<div>
<Text fw={500} size="lg">
{session.documentName}
</Text>
<Text size="sm" c="dimmed">
From: {session.ownerUsername}
</Text>
</div>
{getStatusBadge(participant.status)}
</Group>
{session.message && (
<Alert icon={<InfoIcon fontSize="small" />} color="blue" variant="light">
<Text size="sm">{session.message}</Text>
</Alert>
)}
{session.dueDate && (
<Text size="sm" c="dimmed">
Due Date: {session.dueDate}
</Text>
)}
<Group gap="xs" mt="sm">
<Button
size="sm"
leftSection={<DownloadIcon fontSize="small" />}
onClick={() => downloadDocument(token)}
variant="light"
>
Download Document
</Button>
</Group>
</Stack>
</Card>
{canSign && (
<Card shadow="sm" padding="md" radius="md" withBorder>
<Stack gap="md">
<Text fw={500} size="lg">
Sign Document
</Text>
<Select
label="Certificate Type"
value={certType}
onChange={(value) => setCertType(value || 'P12')}
data={[
{ value: 'P12', label: 'P12/PKCS12 Certificate' },
{ value: 'JKS', label: 'JKS Keystore' },
{ value: 'SERVER', label: 'Server Certificate (if available)' },
]}
size="sm"
/>
{certType !== 'SERVER' && (
<>
<FileInput
label="Certificate File"
placeholder="Select certificate file"
value={certFile}
onChange={setCertFile}
accept=".p12,.pfx,.jks"
size="sm"
/>
<TextInput
label="Certificate Password"
type="password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
size="sm"
/>
</>
)}
<TextInput
label="Location"
placeholder="e.g., San Francisco, CA"
value={location}
onChange={(e) => setLocation(e.currentTarget.value)}
size="sm"
/>
<TextInput
label="Reason"
placeholder="e.g., Document approval"
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
size="sm"
/>
<TextInput
label="Page Number (optional)"
type="number"
value={pageNumber}
onChange={(e) => setPageNumber(parseInt(e.currentTarget.value) || 1)}
size="sm"
min={1}
/>
<Group gap="xs">
<Button
leftSection={<CheckCircleIcon fontSize="small" />}
onClick={handleSubmitSignature}
loading={isSubmitting}
color="green"
>
Submit Signature
</Button>
<Button
leftSection={<CancelIcon fontSize="small" />}
onClick={handleDecline}
color="red"
variant="light"
>
Decline
</Button>
</Group>
</Stack>
</Card>
)}
{participant.hasCompleted && (
<Alert icon={<CheckCircleIcon fontSize="small" />} color="green">
You have {participant.status === 'SIGNED' ? 'signed' : 'declined'} this document.
</Alert>
)}
{participant.isExpired && (
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
Your access to this document has expired.
</Alert>
)}
</Stack>
);
};
export default ParticipantView;