6.15.0: Драфты авто-выполнения в статистике
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
poignatov
2026-03-13 14:43:19 +03:00
parent 4ce8ba66cc
commit c8a47ff408
4 changed files with 296 additions and 73 deletions

View File

@@ -5,10 +5,10 @@ import { useAuth } from './auth/AuthContext'
// Функция для форматирования скорa (аналогично formatScore из TaskDetail)
const formatScore = (num) => {
if (num === 0) return '0'
let str = num.toPrecision(4)
str = str.replace(/\.?0+$/, '')
if (str.includes('e+') || str.includes('e-')) {
const numValue = parseFloat(str)
if (Math.abs(numValue) >= 10000) {
@@ -16,7 +16,7 @@ const formatScore = (num) => {
}
return numValue.toString().replace(/\.?0+$/, '')
}
return str
}
@@ -54,7 +54,7 @@ const formatEntryText = (text, nodes) => {
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
if (nodesMap[i] && currentText.includes(placeholder)) {
const node = nodesMap[i]
const scoreStr = node.score >= 0
const scoreStr = node.score >= 0
? `${node.project_name}+${formatScore(node.score)}`
: `${node.project_name}-${formatScore(Math.abs(node.score))}`
currentText = currentText.replace(regex, `__NODE_${i}__`)
@@ -69,7 +69,7 @@ const formatEntryText = (text, nodes) => {
for (let i = 99; i >= 0; i--) {
if (nodesMap[i]) {
const node = nodesMap[i]
const scoreStr = node.score >= 0
const scoreStr = node.score >= 0
? `${node.project_name}+${formatScore(node.score)}`
: `${node.project_name}-${formatScore(Math.abs(node.score))}`
const regex = new RegExp(`\\$${i}(?!\\d)`, 'g')
@@ -85,12 +85,12 @@ const formatEntryText = (text, nodes) => {
// Разбиваем текст на части и создаем React элементы
const result = []
let searchIndex = 0
while (searchIndex < currentText.length) {
// Ищем следующий маркер
let foundMarker = null
let markerIndex = currentText.length
// Ищем все маркеры
for (const marker in escapedMarkers) {
const index = currentText.indexOf(marker, searchIndex)
@@ -99,14 +99,14 @@ const formatEntryText = (text, nodes) => {
foundMarker = marker
}
}
// Если нашли маркер
if (foundMarker) {
// Добавляем текст до маркера
if (markerIndex > searchIndex) {
result.push(currentText.substring(searchIndex, markerIndex))
}
// Добавляем элемент для маркера
const markerData = escapedMarkers[foundMarker]
if (markerData && markerData.type === 'node') {
@@ -117,7 +117,7 @@ const formatEntryText = (text, nodes) => {
// Это экранированный плейсхолдер
result.push(markerData)
}
searchIndex = markerIndex + foundMarker.length
} else {
// Больше маркеров нет, добавляем оставшийся текст
@@ -134,6 +134,7 @@ const formatEntryText = (text, nodes) => {
function TodayEntriesList({ data, loading, error, onRetry, onDelete }) {
const { authFetch } = useAuth()
const [deletingIds, setDeletingIds] = useState(new Set())
const [clearingAutoCompleteIds, setClearingAutoCompleteIds] = useState(new Set())
const handleDelete = async (entryId) => {
if (deletingIds.has(entryId)) return
@@ -174,6 +175,45 @@ function TodayEntriesList({ data, loading, error, onRetry, onDelete }) {
}
}
const handleClearAutoComplete = async (taskId) => {
if (clearingAutoCompleteIds.has(taskId)) return
if (!window.confirm('Снять автовыполнение в конце дня? Задача не будет выполнена автоматически.')) {
return
}
setClearingAutoCompleteIds(prev => new Set(prev).add(taskId))
try {
const response = await authFetch(`/api/tasks/${taskId}/draft`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ auto_complete: false }),
})
if (!response.ok) {
const errorText = await response.text()
console.error('Clear auto_complete error:', response.status, errorText)
throw new Error(`Ошибка: ${response.status}`)
}
if (onDelete) {
onDelete()
}
} catch (err) {
console.error('Clear auto_complete failed:', err)
alert(err.message || 'Не удалось снять автовыполнение')
} finally {
setClearingAutoCompleteIds(prev => {
const next = new Set(prev)
next.delete(taskId)
return next
})
}
}
if (loading) {
return (
<div className="flex justify-center items-center py-8">
@@ -197,71 +237,120 @@ function TodayEntriesList({ data, loading, error, onRetry, onDelete }) {
return (
<div className="mt-2 mb-6">
<div className="space-y-3">
{data.map((entry) => (
<div
key={entry.id}
className="bg-white rounded-lg p-4 shadow-sm border border-gray-200 relative group"
>
<button
onClick={() => handleDelete(entry.id)}
disabled={deletingIds.has(entry.id)}
className="absolute top-4 right-4 disabled:opacity-50 disabled:cursor-not-allowed"
style={{
color: '#6b7280',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
borderRadius: '0.25rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '24px',
height: '24px',
transition: 'all 0.2s',
opacity: deletingIds.has(entry.id) ? 0.5 : 1,
zIndex: 10
}}
onMouseEnter={(e) => {
if (!deletingIds.has(entry.id)) {
e.currentTarget.style.backgroundColor = '#f3f4f6'
e.currentTarget.style.color = '#1f2937'
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = '#6b7280'
}}
title="Удалить запись"
{data.map((entry) => {
const isDraft = entry.is_draft === true
return (
<div
key={isDraft ? `draft-${entry.task_id}` : entry.id}
className={`bg-white rounded-lg p-4 shadow-sm border relative group ${
isDraft ? 'border-blue-400' : 'border-gray-200'
}`}
>
{deletingIds.has(entry.id) ? (
<svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{isDraft ? (
<button
onClick={() => handleClearAutoComplete(entry.task_id)}
disabled={clearingAutoCompleteIds.has(entry.task_id)}
className="absolute top-4 right-4 disabled:opacity-50 disabled:cursor-not-allowed"
style={{
color: '#3b82f6',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
borderRadius: '0.25rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '24px',
height: '24px',
transition: 'all 0.2s',
opacity: clearingAutoCompleteIds.has(entry.task_id) ? 0.5 : 1,
zIndex: 10
}}
onMouseEnter={(e) => {
if (!clearingAutoCompleteIds.has(entry.task_id)) {
e.currentTarget.style.backgroundColor = '#eff6ff'
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
}}
title="Снять автовыполнение в конце дня"
>
{clearingAutoCompleteIds.has(entry.task_id) ? (
<svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" title="Автовыполнение в конце дня">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
</svg>
)}
</button>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
<button
onClick={() => handleDelete(entry.id)}
disabled={deletingIds.has(entry.id)}
className="absolute top-4 right-4 disabled:opacity-50 disabled:cursor-not-allowed"
style={{
color: '#6b7280',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
borderRadius: '0.25rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '24px',
height: '24px',
transition: 'all 0.2s',
opacity: deletingIds.has(entry.id) ? 0.5 : 1,
zIndex: 10
}}
onMouseEnter={(e) => {
if (!deletingIds.has(entry.id)) {
e.currentTarget.style.backgroundColor = '#f3f4f6'
e.currentTarget.style.color = '#1f2937'
}
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = '#6b7280'
}}
title="Удалить запись"
>
{deletingIds.has(entry.id) ? (
<svg className="w-5 h-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
)}
</button>
)}
</button>
<div className="text-gray-800 whitespace-pre-wrap pr-8">
{formatEntryText(entry.text, entry.nodes)}
</div>
{entry.created_date && (
<div className="text-xs text-gray-500 mt-2">
{new Date(entry.created_date).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
})}
<div className="text-gray-800 whitespace-pre-wrap pr-8">
{formatEntryText(entry.text, entry.nodes)}
</div>
)}
</div>
))}
<div className="text-xs text-gray-500 mt-2">
{isDraft ? 'в конце дня' : (
entry.created_date && new Date(entry.created_date).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
})
)}
</div>
</div>
)
})}
</div>
</div>
)