2025-07-23 17:40:45 +04:00
|
|
|
import {useRef, useState} from 'react'
|
|
|
|
|
import PropTypes from 'prop-types'
|
|
|
|
|
import Box from '@mui/material/Box'
|
|
|
|
|
import Typography from '@mui/material/Typography'
|
|
|
|
|
import Card from '@mui/material/Card'
|
|
|
|
|
import CardContent from '@mui/material/CardContent'
|
|
|
|
|
import CardActions from '@mui/material/CardActions'
|
|
|
|
|
import Button from '@mui/material/Button'
|
|
|
|
|
import Chip from '@mui/material/Chip'
|
|
|
|
|
import Avatar from '@mui/material/Avatar'
|
|
|
|
|
import Divider from '@mui/material/Divider'
|
|
|
|
|
import Timeline from '@mui/lab/Timeline'
|
|
|
|
|
import TimelineItem from '@mui/lab/TimelineItem'
|
|
|
|
|
import TimelineSeparator from '@mui/lab/TimelineSeparator'
|
|
|
|
|
import TimelineConnector from '@mui/lab/TimelineConnector'
|
|
|
|
|
import TimelineContent from '@mui/lab/TimelineContent'
|
|
|
|
|
import TimelineDot from '@mui/lab/TimelineDot'
|
|
|
|
|
import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'
|
|
|
|
|
import AccessTimeIcon from '@mui/icons-material/AccessTime'
|
|
|
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
|
|
|
import ErrorIcon from '@mui/icons-material/Error'
|
|
|
|
|
import EditIcon from '@mui/icons-material/Edit'
|
2025-07-23 18:21:24 +04:00
|
|
|
import Snackbar from '@mui/material/Snackbar'
|
|
|
|
|
import Alert from '@mui/material/Alert'
|
2025-07-23 17:40:45 +04:00
|
|
|
import SessionExpired from '../session/session-expired.js'
|
2025-07-24 11:30:35 +04:00
|
|
|
import MarkdownRenderer from '../markdown-renderer/index.js'
|
2025-07-23 17:40:45 +04:00
|
|
|
import VersionDialog from './version-dialog.js'
|
2025-07-23 18:21:24 +04:00
|
|
|
import VoteButtons from './vote-buttons.js'
|
2025-07-23 19:45:21 +04:00
|
|
|
import CopyButton from './copy-button.js'
|
2025-07-23 19:54:20 +04:00
|
|
|
import ShareButton from './share-button.js'
|
2025-07-23 20:31:55 +04:00
|
|
|
import ExportPdfButton from './export-pdf-button.js'
|
2025-07-24 08:08:15 +04:00
|
|
|
import PrintButton from './print-button.js'
|
2025-07-23 17:40:45 +04:00
|
|
|
import {formatDate} from '@/lib/format.js'
|
|
|
|
|
import {compareVersion} from '@/lib/directus.js'
|
|
|
|
|
|
|
|
|
|
function getVersionStatus(version, index, totalVersions) {
|
|
|
|
|
// Logic to determine version status based on position and data
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
return 'current' // Most recent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (index === totalVersions - 1) {
|
|
|
|
|
return 'initial' // First version
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'archived' // Intermediate versions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getStatusConfig(status) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'current': {
|
|
|
|
|
return {
|
|
|
|
|
color: '#1976D2',
|
|
|
|
|
bgColor: '#E3F2FD',
|
|
|
|
|
icon: <EditIcon />,
|
|
|
|
|
label: 'En cours',
|
|
|
|
|
chipColor: 'primary'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'published': {
|
|
|
|
|
return {
|
|
|
|
|
color: '#2E7D32',
|
|
|
|
|
bgColor: '#E8F5E9',
|
|
|
|
|
icon: <CheckCircleIcon />,
|
|
|
|
|
label: 'Publié',
|
|
|
|
|
chipColor: 'success'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'archived': {
|
|
|
|
|
return {
|
|
|
|
|
color: '#757575',
|
|
|
|
|
bgColor: '#F5F5F5',
|
|
|
|
|
icon: <AccessTimeIcon />,
|
|
|
|
|
label: 'Archivé',
|
|
|
|
|
chipColor: 'default'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'outdated': {
|
|
|
|
|
return {
|
|
|
|
|
color: '#D32F2F',
|
|
|
|
|
bgColor: '#F9E8E8',
|
|
|
|
|
icon: <ErrorIcon />,
|
|
|
|
|
label: 'Obsolète',
|
|
|
|
|
chipColor: 'error'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: {
|
|
|
|
|
return {
|
|
|
|
|
color: '#757575',
|
|
|
|
|
bgColor: '#F5F5F5',
|
|
|
|
|
icon: <AccessTimeIcon />,
|
|
|
|
|
label: 'Archivé',
|
|
|
|
|
chipColor: 'default'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function VersionCard({
|
|
|
|
|
version,
|
|
|
|
|
index,
|
|
|
|
|
totalVersions,
|
|
|
|
|
accessToken,
|
|
|
|
|
userId,
|
|
|
|
|
countdownRef,
|
|
|
|
|
setError,
|
|
|
|
|
setIsErrorAlertOpen,
|
|
|
|
|
setIsOpenComparison,
|
2025-07-23 18:21:24 +04:00
|
|
|
setVersionCompare,
|
|
|
|
|
onVoteResult
|
2025-07-23 17:40:45 +04:00
|
|
|
}) {
|
|
|
|
|
const status = getVersionStatus(version, index, totalVersions)
|
2025-07-23 18:21:24 +04:00
|
|
|
|
2025-07-23 17:40:45 +04:00
|
|
|
const statusConfig = getStatusConfig(status)
|
|
|
|
|
const userDisplayName = version.user_created?.split('-')[0] || 'Système'
|
|
|
|
|
|
2025-07-23 20:39:03 +04:00
|
|
|
// Check if voting is disabled (after 3 days)
|
|
|
|
|
const createdAt = new Date(version.date_created)
|
|
|
|
|
const threeDaysAgo = new Date(Date.now() - (3 * 24 * 60 * 60 * 1000))
|
|
|
|
|
const isVoteDisabled = createdAt < threeDaysAgo
|
|
|
|
|
|
2025-07-23 17:40:45 +04:00
|
|
|
const handleCompareClick = async () => {
|
|
|
|
|
const comparisonData = await compareVersion({
|
|
|
|
|
accessToken,
|
|
|
|
|
userId,
|
|
|
|
|
versionId: version.id,
|
|
|
|
|
countdownRef,
|
|
|
|
|
setError,
|
|
|
|
|
setIsErrorAlertOpen
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (comparisonData) {
|
|
|
|
|
setVersionCompare({...comparisonData, versionId: version.id})
|
|
|
|
|
setIsOpenComparison(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 11:30:35 +04:00
|
|
|
// Create content preview preserving markdown structure
|
|
|
|
|
const createContentPreview = content => {
|
|
|
|
|
if (!content) {
|
|
|
|
|
return 'Contenu non disponible'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If content is short enough, return as is
|
|
|
|
|
if (content.length <= 150) {
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find a good breaking point (end of sentence, paragraph, or word)
|
|
|
|
|
const preview = content.slice(0, 150)
|
|
|
|
|
const lastSentence = Math.max(preview.lastIndexOf('.'), preview.lastIndexOf('!'), preview.lastIndexOf('?'))
|
|
|
|
|
const lastParagraph = preview.lastIndexOf('\n\n')
|
|
|
|
|
const lastWord = preview.lastIndexOf(' ')
|
|
|
|
|
|
|
|
|
|
// Choose the best breaking point
|
|
|
|
|
let breakPoint = 150
|
|
|
|
|
if (lastSentence > 100) {
|
|
|
|
|
breakPoint = lastSentence + 1
|
|
|
|
|
} else if (lastParagraph > 80) {
|
|
|
|
|
breakPoint = lastParagraph
|
|
|
|
|
} else if (lastWord > 100) {
|
|
|
|
|
breakPoint = lastWord
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return content.slice(0, breakPoint) + (content.length > breakPoint ? '...' : '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contentPreview = createContentPreview(version?.delta?.contenu)
|
2025-07-23 17:40:45 +04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
sx={{
|
|
|
|
|
borderLeft: `4px solid ${statusConfig.color}`,
|
|
|
|
|
backgroundColor: statusConfig.bgColor,
|
|
|
|
|
mb: 2,
|
|
|
|
|
transition: 'all 0.2s ease-in-out',
|
|
|
|
|
'&:hover': {
|
|
|
|
|
transform: 'translateY(-2px)',
|
|
|
|
|
boxShadow: 3
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Box sx={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
mb: 2
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
|
|
|
|
<Avatar sx={{bgcolor: statusConfig.color, width: 32, height: 32}}>
|
|
|
|
|
{statusConfig.icon}
|
|
|
|
|
</Avatar>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant='h6' sx={{fontWeight: 'bold', color: statusConfig.color}}>
|
|
|
|
|
{version.name}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant='caption' color='text.secondary'>
|
|
|
|
|
par @{userDisplayName}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
2025-07-23 20:39:03 +04:00
|
|
|
<Box sx={{display: 'flex', gap: 1}}>
|
|
|
|
|
<Chip
|
|
|
|
|
label={statusConfig.label}
|
|
|
|
|
color={statusConfig.chipColor}
|
|
|
|
|
size='small'
|
|
|
|
|
variant='outlined'
|
|
|
|
|
/>
|
|
|
|
|
{isVoteDisabled && (
|
|
|
|
|
<Chip
|
|
|
|
|
label='Vote fermé'
|
|
|
|
|
color='error'
|
|
|
|
|
size='small'
|
|
|
|
|
variant='outlined'
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
2025-07-23 17:40:45 +04:00
|
|
|
</Box>
|
|
|
|
|
|
2025-07-24 11:30:35 +04:00
|
|
|
<Box sx={{mb: 2, '& > div': {fontSize: '0.875rem', fontStyle: 'italic', color: 'text.secondary'}}}>
|
|
|
|
|
<MarkdownRenderer
|
|
|
|
|
content={contentPreview}
|
|
|
|
|
fallbackComponent={({children, ...props}) => (
|
|
|
|
|
<Typography
|
|
|
|
|
variant='body2'
|
|
|
|
|
color='text.secondary'
|
|
|
|
|
sx={{fontStyle: 'italic'}}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Typography>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
2025-07-23 17:40:45 +04:00
|
|
|
|
|
|
|
|
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
|
|
|
|
<Typography variant='caption' color='text.secondary'>
|
|
|
|
|
{formatDate(version.date_created, 'PPpp')}
|
|
|
|
|
</Typography>
|
|
|
|
|
|
2025-07-23 19:45:21 +04:00
|
|
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
|
|
|
|
<CopyButton
|
|
|
|
|
content={version.delta?.contenu || version.name || ''}
|
|
|
|
|
label='Copier le contenu de cette version'
|
|
|
|
|
hasSnackbarVisible={false}
|
|
|
|
|
/>
|
2025-07-23 19:54:20 +04:00
|
|
|
<ShareButton
|
|
|
|
|
versionId={version.id}
|
|
|
|
|
versionName={version.name}
|
|
|
|
|
hasSnackbarVisible={false}
|
|
|
|
|
/>
|
2025-07-23 20:31:55 +04:00
|
|
|
<ExportPdfButton
|
|
|
|
|
versionData={version}
|
|
|
|
|
size='small'
|
|
|
|
|
variant='text'
|
|
|
|
|
/>
|
2025-07-24 08:08:15 +04:00
|
|
|
<PrintButton
|
|
|
|
|
versionData={version}
|
|
|
|
|
size='small'
|
|
|
|
|
variant='text'
|
|
|
|
|
/>
|
2025-07-23 20:39:03 +04:00
|
|
|
<VoteButtons
|
|
|
|
|
hasCountsVisible
|
|
|
|
|
versionId={version.id}
|
|
|
|
|
isDisabled={isVoteDisabled}
|
|
|
|
|
onVoteResult={onVoteResult}
|
|
|
|
|
/>
|
2025-07-23 19:45:21 +04:00
|
|
|
</Box>
|
2025-07-23 17:40:45 +04:00
|
|
|
</Box>
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
<CardActions sx={{justifyContent: 'flex-end'}}>
|
|
|
|
|
<Button
|
|
|
|
|
size='small'
|
|
|
|
|
variant='outlined'
|
|
|
|
|
color='primary'
|
|
|
|
|
onClick={handleCompareClick}
|
|
|
|
|
>
|
|
|
|
|
Comparer
|
|
|
|
|
</Button>
|
|
|
|
|
</CardActions>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function VersionTimeline({
|
|
|
|
|
collection,
|
|
|
|
|
data,
|
|
|
|
|
accessToken,
|
|
|
|
|
userId,
|
|
|
|
|
setError,
|
|
|
|
|
setIsErrorAlertOpen
|
|
|
|
|
}) {
|
|
|
|
|
const countdownRef = useRef()
|
|
|
|
|
const [isOpenComparison, setIsOpenComparison] = useState(false)
|
|
|
|
|
const [versionCompare, setVersionCompare] = useState(null)
|
2025-07-23 18:21:24 +04:00
|
|
|
const [snackbar, setSnackbar] = useState({open: false, message: '', severity: 'success'})
|
2025-07-23 17:40:45 +04:00
|
|
|
|
|
|
|
|
const versionData = data.find(({id}) => id === versionCompare?.versionId)
|
|
|
|
|
|
2025-07-23 18:21:24 +04:00
|
|
|
const handleVoteResult = result => {
|
|
|
|
|
setSnackbar({
|
|
|
|
|
open: true,
|
|
|
|
|
message: result.message,
|
|
|
|
|
severity: result.success ? 'success' : 'error'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseSnackbar = () => {
|
|
|
|
|
setSnackbar(prev => ({...prev, open: false}))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-23 17:40:45 +04:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant='h5' textAlign='center' sx={{mb: 3, fontWeight: 'bold'}}>
|
|
|
|
|
Historique des versions - {collection}
|
|
|
|
|
</Typography>
|
|
|
|
|
|
|
|
|
|
<Timeline position='right'>
|
|
|
|
|
{data.map((version, index) => (
|
|
|
|
|
<TimelineItem key={version.id}>
|
|
|
|
|
<TimelineOppositeContent sx={{flex: 0.3, pr: 2}}>
|
|
|
|
|
<Typography variant='caption' color='text.secondary'>
|
|
|
|
|
{formatDate(version.date_created, 'dd/MM/yyyy')}
|
|
|
|
|
</Typography>
|
|
|
|
|
<br />
|
|
|
|
|
<Typography variant='caption' color='text.secondary'>
|
|
|
|
|
{formatDate(version.date_created, 'HH:mm')}
|
|
|
|
|
</Typography>
|
|
|
|
|
</TimelineOppositeContent>
|
|
|
|
|
|
|
|
|
|
<TimelineSeparator>
|
|
|
|
|
<TimelineDot
|
|
|
|
|
color={index === 0 ? 'primary' : 'grey'}
|
|
|
|
|
variant={index === 0 ? 'filled' : 'outlined'}
|
|
|
|
|
>
|
|
|
|
|
{index === 0 ? <EditIcon /> : <AccessTimeIcon />}
|
|
|
|
|
</TimelineDot>
|
|
|
|
|
{index < data.length - 1 && <TimelineConnector />}
|
|
|
|
|
</TimelineSeparator>
|
|
|
|
|
|
|
|
|
|
<TimelineContent sx={{flex: 1}}>
|
|
|
|
|
<VersionCard
|
|
|
|
|
version={version}
|
|
|
|
|
index={index}
|
|
|
|
|
totalVersions={data.length}
|
|
|
|
|
accessToken={accessToken}
|
|
|
|
|
userId={userId}
|
|
|
|
|
countdownRef={countdownRef}
|
|
|
|
|
setError={setError}
|
|
|
|
|
setIsErrorAlertOpen={setIsErrorAlertOpen}
|
|
|
|
|
setIsOpenComparison={setIsOpenComparison}
|
|
|
|
|
setVersionCompare={setVersionCompare}
|
2025-07-23 18:21:24 +04:00
|
|
|
onVoteResult={handleVoteResult}
|
2025-07-23 17:40:45 +04:00
|
|
|
/>
|
|
|
|
|
</TimelineContent>
|
|
|
|
|
</TimelineItem>
|
|
|
|
|
))}
|
|
|
|
|
</Timeline>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{isOpenComparison && (
|
|
|
|
|
<VersionDialog
|
|
|
|
|
versionData={versionData}
|
|
|
|
|
versionCompare={versionCompare}
|
|
|
|
|
isOpen={isOpenComparison}
|
|
|
|
|
setIsOpen={setIsOpenComparison}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<SessionExpired
|
|
|
|
|
ref={countdownRef}
|
|
|
|
|
setError={setError}
|
|
|
|
|
setIsErrorAlertOpen={setIsErrorAlertOpen}
|
|
|
|
|
/>
|
2025-07-23 18:21:24 +04:00
|
|
|
|
|
|
|
|
<Snackbar
|
|
|
|
|
open={snackbar.open}
|
|
|
|
|
autoHideDuration={6000}
|
|
|
|
|
anchorOrigin={{vertical: 'bottom', horizontal: 'center'}}
|
|
|
|
|
onClose={handleCloseSnackbar}
|
|
|
|
|
>
|
|
|
|
|
<Alert variant='filled' severity={snackbar.severity} sx={{width: '100%'}} onClose={handleCloseSnackbar}>
|
|
|
|
|
{snackbar.message}
|
|
|
|
|
</Alert>
|
|
|
|
|
</Snackbar>
|
2025-07-23 17:40:45 +04:00
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VersionTimeline.propTypes = {
|
|
|
|
|
collection: PropTypes.oneOf(['titres', 'articles']).isRequired,
|
|
|
|
|
data: PropTypes.array.isRequired,
|
|
|
|
|
accessToken: PropTypes.string.isRequired,
|
|
|
|
|
userId: PropTypes.string.isRequired,
|
|
|
|
|
setError: PropTypes.func.isRequired,
|
|
|
|
|
setIsErrorAlertOpen: PropTypes.func.isRequired
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VersionCard.propTypes = {
|
|
|
|
|
version: PropTypes.object.isRequired,
|
|
|
|
|
index: PropTypes.number.isRequired,
|
|
|
|
|
totalVersions: PropTypes.number.isRequired,
|
|
|
|
|
accessToken: PropTypes.string.isRequired,
|
|
|
|
|
userId: PropTypes.string.isRequired,
|
|
|
|
|
countdownRef: PropTypes.object.isRequired,
|
|
|
|
|
setError: PropTypes.func.isRequired,
|
|
|
|
|
setIsErrorAlertOpen: PropTypes.func.isRequired,
|
|
|
|
|
setIsOpenComparison: PropTypes.func.isRequired,
|
2025-07-23 18:21:24 +04:00
|
|
|
setVersionCompare: PropTypes.func.isRequired,
|
|
|
|
|
onVoteResult: PropTypes.func.isRequired
|
2025-07-23 17:40:45 +04:00
|
|
|
}
|