Отзывы и предложения к софту от AleXStam
  • Страница 6 из 8
  • «
  • 1
  • 2
  • 4
  • 5
  • 6
  • 7
  • 8
  • »
Поговорим о...
{{/* layouts/shortcodes/cardinfo.html */}}
{{ $link := .Get "link" }}
{{ $title := .Get "title" }}
{{ $desc := .Get "desc" | default "" }}
{{ $date := .Get "date" }}
{{ $audience := .Get "audience" }}
{{ $tag := .Get "tag" }}
{{ $tagtype := .Get "tagtype" | default "default" }}
{{ $icon := .Get "icon" }}
{{ $iconsize := .Get "iconsize" | default "24px" }}
{{ $visible := .Get "visible" | default "true" }}
{{ $style := .Get "style" | default "button" }}

{{ if ne $visible "false" }}
<div class="info-card info-card-style-{{ $style }}">
<a href="{{ $link }}" class="info-card-link" target="_blank" rel="noopener">
<div class="info-card-header">
<div class="info-card-left">
{{ if $icon }}
<span class="info-card-title-icon">
{{ if .Site.Data.icons }}
{{ $iconData := index .Site.Data.icons $icon }}
{{ if $iconData }}
{{ $iconSvg := replaceRE "<svg" (printf "<svg width='%s' height='%s'" $iconsize $iconsize) $iconData }}
<span class="custom-icon">{{ $iconSvg | safeHTML }}</span>
{{ else }}
<span class="custom-icon">
<svg width="{{ $iconsize }}" height="{{ $iconsize }}" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
</svg>
</span>
{{ end }}
{{ else }}
<span class="custom-icon">
<svg width="{{ $iconsize }}" height="{{ $iconsize }}" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
</svg>
</span>
{{ end }}
</span>
{{ end }}
<span class="info-card-title">{{ $title }}</span>
</div>
<div class="info-card-right-section">
{{ if $tag }}
<span class="info-card-tag info-card-tag-{{ $tagtype }}">
{{ $tag }}
</span>
{{ end }}

{{ if $desc }}
<div class="info-card-tooltip-wrapper">
<span class="info-card-icon">
<svg class="info-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13A6 6 0 1 1 8 2a6 6 0 0 1 0 12z"/>
<path d="M7 11h2v2H7zm0-8h2v6H7z"/>
</svg>
</span>
<div class="info-card-tooltip">
<div class="tooltip-description">{{ $desc | safeHTML }}</div>
{{ if or $date $audience }}
<div class="tooltip-details">
{{ if $date }}
<div class="tooltip-row">
<svg class="tooltip-row-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/>
</svg>
<span class="tooltip-row-text">Добавлено: {{ $date }}</span>
</div>
{{ end }}
{{ if $audience }}
<div class="tooltip-row">
<svg class="tooltip-row-icon" width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
</svg>
<span class="tooltip-row-text">Для кого: {{ $audience }}</span>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
</a>
</div>
{{ end }}

<style>
/* ===== ОСНОВНЫЕ СТИЛИ ===== */
.info-card {
transition: all 0.2s ease;
position: relative;
box-sizing: border-box;
display: inline-block;
vertical-align: top;
margin: 0.75rem 0 0 0;
padding: 0;
width: 100%;
}

.info-card-link {
display: block;
text-decoration: none;
color: #333333;
width: 100%;
height: 100%;
box-sizing: border-box;
}

.info-card-link:hover {
text-decoration: none;
}

.info-card-header {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 24px;
width: 100%;
gap: 8px;
}

.info-card-left {
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
padding-right: 8px;
gap: 10px;
}

.info-card-title-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}

.info-card-title-icon .custom-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}

.info-card-title-icon .custom-icon svg {
width: 100%;
height: 100%;
fill: currentColor;
color: #666666;
vertical-align: middle;
}

.info-card-title {
font-size: 1rem;
font-weight: 600;
color: #333333;
line-height: 1.4;
flex: 1 1 auto;
min-width: 30px;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
}

.info-card-right-section {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
flex-wrap: nowrap;
}

/* ===== КОНТЕЙНЕР ДЛЯ СЕТКИ ===== */
.cards-grid {
display: flex;
flex-wrap: wrap;
margin: -0.75rem -10px 0 -10px; /* Отрицательный margin для компенсации padding */
}

.cards-grid .info-card {
flex: 0 0 auto;
padding: 0 10px; /* Отступы между карточками */
margin-top: 0.75rem; /* Отступ сверху */
box-sizing: border-box;
}

/* 2 колонки */
.cards-grid.cols-2 .info-card {
width: calc(50% - 20px); /* Учитываем padding слева и справа (10px + 10px) */
min-width: 350px;
}

/* 3 колонки */
.cards-grid.cols-3 .info-card {
width: calc(33.333% - 20px);
min-width: 350px;
}

/* 4 колонки */
.cards-grid.cols-4 .info-card {
width: calc(25% - 20px);
min-width: 350px;
}

/* Перенос при достижении min-width */
@media (max-width: 750px) {
.cards-grid.cols-2 .info-card,
.cards-grid.cols-3 .info-card,
.cards-grid.cols-4 .info-card {
width: calc(100% - 20px); /* На всю ширину с учетом отступов */
min-width: auto;
}
}

@media (max-width: 1100px) {
.cards-grid.cols-4 .info-card {
width: calc(50% - 20px);
}
}

@media (max-width: 900px) {
.cards-grid.cols-3 .info-card,
.cards-grid.cols-4 .info-card {
width: calc(50% - 20px);
}
}

/* ===== СТИЛЬ: BUTTON (по умолчанию) ===== */
.info-card-style-button {
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #ffffff;
}

.info-card-style-button:hover {
border-color: #4a90e2;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.15);
}

.info-card-style-button .info-card-link {
padding: 1rem 1.25rem;
}

/* ===== СТИЛЬ: LINK (простая ссылка с отступами) ===== */
.info-card-style-link {
border: none;
background: transparent;
border-radius: 0;
}

.info-card-style-link .info-card-link {
padding: 0.5rem 0.25rem;
border-bottom: 1px solid transparent;
}

.info-card-style-link:hover .info-card-link {
border-bottom-color: #4a90e2;
}

.info-card-style-link .info-card-title {
font-weight: 500;
}

.info-card-style-link .info-card-title-icon .custom-icon svg {
color: #4a90e2;
opacity: 0.8;
}

/* ===== СТИЛЬ: MINIMAL (минималистичный) ===== */
.info-card-style-minimal {
border: none;
background: transparent;
}

.info-card-style-minimal .info-card-link {
padding: 0.3rem 0;
}

.info-card-style-minimal .info-card-title {
font-weight: 400;
font-size: 0.95rem;
}

.info-card-style-minimal .info-card-title-icon .custom-icon svg {
color: #888;
width: 18px;
height: 18px;
}

.info-card-style-minimal:hover .info-card-title {
color: #4a90e2;
}

/* ===== СТИЛЬ: CARD (карточка с тенью) ===== */
.info-card-style-card {
border: none;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transition: all 0.3s ease;
}

.info-card-style-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}

.info-card-style-card .info-card-link {
padding: 1.25rem 1.5rem;
}

.info-card-style-card .info-card-title {
font-size: 1.1rem;
}

/* ===== СТИЛЬ: PILL (в виде пилюли) ===== */
.info-card-style-pill {
border: 1px solid #e0e0e0;
border-radius: 50px;
background: #f8f9fa;
}

.info-card-style-pill:hover {
border-color: #4a90e2;
background: #ffffff;
}

.info-card-style-pill .info-card-link {
padding: 0.6rem 1.25rem;
}

.info-card-style-pill .info-card-title {
font-weight: 500;
font-size: 0.95rem;
}

/* ===== ТЕГИ ===== */
.info-card-tag {
display: inline-block;
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-weight: 500;
white-space: nowrap;
border: 1px solid transparent;
line-height: 1.4;
flex-shrink: 0;
}

.info-card-tag-default { background: #f0f0f0; color: #666666; border-color: #ddd; }
.info-card-tag-info { background: #e3f2fd; color: #1565c0; border-color: #90caf9; }
.info-card-tag-warning { background: #fff3e0; color: #e65100; border-color: #ffcc80; }
.info-card-tag-success { background: #e8f5e9; color: #2e7d32; border-color: #a5d6a7; }
.info-card-tag-error { background: #ffebee; color: #c62828; border-color: #ef9a9a; }

/* ===== ТУЛТИПЫ ===== */
.info-card-tooltip-wrapper {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
}

.info-card-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
cursor: help;
color: #666666;
border-radius: 50%;
transition: all 0.15s;
flex-shrink: 0;
}

.info-card-icon:hover {
background: #f0f0f0;
color: #4a90e2;
}

.info-icon {
width: 16px;
height: 16px;
}

.info-card-tooltip {
position: absolute;
bottom: calc(100% + 10px);
right: 50%;
transform: translateX(50%);
width: 420px;
max-width: 90vw;
padding: 18px;
background: white;
border-radius: 8px;
box-shadow: 0 5px 25px rgba(0,0,0,0.15);
border: 1px solid #e0e0e0;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: auto;
}

.info-card-tooltip.tooltip-bottom {
bottom: auto;
top: calc(100% + 10px);
}

.info-card-tooltip-wrapper:hover .info-card-tooltip {
opacity: 1;
visibility: visible;
transform: translateX(50%) translateY(-5px);
}

.info-card-tooltip.tooltip-bottom:hover {
transform: translateX(50%) translateY(5px);
}

.info-card-tooltip::before {
content: '';
position: absolute;
bottom: -7px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 14px;
height: 14px;
background: white;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
z-index: -1;
}

.info-card-tooltip.tooltip-bottom::before {
bottom: auto;
top: -7px;
transform: translateX(-50%) rotate(-135deg);
}

.tooltip-description {
font-size: 0.92rem;
line-height: 1.6;
color: #444444;
margin-bottom: 14px;
max-height: 350px;
overflow-y: auto;
padding-right: 6px;
}

.tooltip-description::-webkit-scrollbar { width: 6px; }
.tooltip-description::-webkit-scrollbar-track { background: #f5f5f5; border-radius: 3px; }
.tooltip-description::-webkit-scrollbar-thumb { background: #b0b0b0; border-radius: 3px; }

.tooltip-details {
font-size: 0.85rem;
color: #666666;
border-top: 1px solid #f0f0f0;
padding-top: 12px;
}

.tooltip-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}

.tooltip-row:last-child {
margin-bottom: 0;
}

.tooltip-row-icon {
flex-shrink: 0;
width: 14px;
height: 14px;
}

.tooltip-row-text {
line-height: 1.4;
}

/* ===== ТЕМНАЯ ТЕМА ===== */
body.dark .info-card,
html.dark .info-card {
background: #2d3748;
border-color: #4a5568;
}

body.dark .info-card:hover,
html.dark .info-card:hover {
border-color: #63b3ed;
box-shadow: 0 2px 8px rgba(99, 179, 237, 0.2);
}

body.dark .info-card-link,
html.dark .info-card-link {
color: #e2e8f0;
}

body.dark .info-card-title,
html.dark .info-card-title {
color: #f7fafc;
}

body.dark .info-card-title-icon .custom-icon svg,
html.dark .info-card-title-icon .custom-icon svg {
color: #a0aec0;
}

body.dark .info-card-icon,
html.dark .info-card-icon {
color: #a0aec0;
}

body.dark .info-card-icon:hover,
html.dark .info-card-icon:hover {
background: #4a5568;
color: #63b3ed;
}

body.dark .info-card-tooltip,
html.dark .info-card-tooltip {
background: #1a202c;
border-color: #4a5568;
box-shadow: 0 5px 25px rgba(0,0,0,0.5);
}

body.dark .info-card-tooltip::before,
html.dark .info-card-tooltip::before {
background: #1a202c;
border-color: #4a5568;
}

body.dark .tooltip-description,
html.dark .tooltip-description {
color: #e2e8f0;
}

body.dark .tooltip-details,
html.dark .tooltip-details {
color: #a0aec0;
border-top-color: #4a5568;
}

body.dark .info-card-tag-default,
html.dark .info-card-tag-default {
background: #4a5568;
color: #cbd5e0;
border-color: #718096;
}

body.dark .info-card-tag-info,
html.dark .info-card-tag-info {
background: #2c5282;
color: #bee3f8;
border-color: #3182ce;
}

body.dark .info-card-tag-warning,
html.dark .info-card-tag-warning {
background: #975a16;
color: #feebc8;
border-color: #d69e2e;
}

body.dark .info-card-tag-success,
html.dark .info-card-tag-success {
background: #276749;
color: #c6f6d5;
border-color: #38a169;
}

body.dark .info-card-tag-error,
html.dark .info-card-tag-error {
background: #9b2c2c;
color: #fed7d7;
border-color: #e53e3e;
}

body.dark .info-card-style-link,
html.dark .info-card-style-link,
body.dark .info-card-style-minimal,
html.dark .info-card-style-minimal {
background: transparent;
}

body.dark .info-card-style-card,
html.dark .info-card-style-card {
background: #2d3748;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

body.dark .info-card-style-pill,
html.dark .info-card-style-pill {
background: #2d3748;
border-color: #4a5568;
}

body.dark .info-card-style-pill:hover,
html.dark .info-card-style-pill:hover {
background: #374151;
}

/* ===== АДАПТИВНОСТЬ ===== */
@media (max-width: 768px) {
.info-card-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}

.info-card-left {
width: 100%;
padding-right: 0;
}

.info-card-right-section {
width: 100%;
justify-content: space-between;
}

.info-card-tooltip {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) !important;
width: calc(100vw - 40px);
max-width: 500px;
max-height: 70vh;
overflow-y: auto;
bottom: auto !important;
right: auto !important;
}

.info-card-tooltip::before {
display: none;
}

.info-card {
margin: 0.5rem 0 0 0;
}

.info-card-link {
padding: 0.75rem 1rem;
}
}

@media (max-width: 480px) {
.info-card-tooltip {
width: calc(100vw - 20px);
padding: 14px;
}

.info-card-left {
gap: 8px;
}

.info-card {
margin: 0.4rem 0 0 0;
}
}

@media (min-width: 1200px) {
.info-card-tooltip {
width: 450px;
}
}

@media (min-width: 1400px) {
.info-card-tooltip {
width: 480px;
}
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function() {
var wrappers = document.querySelectorAll('.info-card-tooltip-wrapper');

wrappers.forEach(function(wrapper) {
var tooltip = wrapper.querySelector('.info-card-tooltip');
var icon = wrapper.querySelector('.info-card-icon');

if (!tooltip || !icon) return;

function adjustTooltipPosition() {
if (window.innerWidth <= 768) return;

var wrapperRect = wrapper.getBoundingClientRect();
var tooltipRect = tooltip.getBoundingClientRect();
var viewportHeight = window.innerHeight;

var spaceAbove = wrapperRect.top;
var spaceBelow = viewportHeight - wrapperRect.bottom;
var tooltipHeight = tooltipRect.height;

if (spaceAbove < tooltipHeight + 20 && spaceBelow >= tooltipHeight + 20) {
tooltip.classList.add('tooltip-bottom');
} else {
tooltip.classList.remove('tooltip-bottom');
}
}

wrapper.addEventListener('mouseenter', function() {
adjustTooltipPosition();
tooltip.style.opacity = '1';
tooltip.style.visibility = 'visible';
});

wrapper.addEventListener('mouseleave', function() {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
});

wrapper.addEventListener('click', function(e) {
if (window.innerWidth <= 768) {
e.preventDefault();
e.stopPropagation();

if (tooltip.style.opacity === '1') {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
} else {
tooltip.style.opacity = '1';
tooltip.style.visibility = 'visible';
}
}
});
});

var scrollTimer;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(function() {
document.querySelectorAll('.info-card-tooltip-wrapper').forEach(function(wrapper) {
var tooltip = wrapper.querySelector('.info-card-tooltip');
if (tooltip && tooltip.style.opacity === '1') {
tooltip.classList.remove('tooltip-bottom');
setTimeout(function() {
var wrapperRect = wrapper.getBoundingClientRect();
var tooltipRect = tooltip.getBoundingClientRect();
var viewportHeight = window.innerHeight;
var spaceAbove = wrapperRect.top;
var spaceBelow = viewportHeight - wrapperRect.bottom;

if (spaceAbove < tooltipRect.height + 20 && spaceBelow >= tooltipRect.height + 20) {
tooltip.classList.add('tooltip-bottom');
}
}, 10);
}
});
}, 50);
});

window.addEventListener('resize', function() {
document.querySelectorAll('.info-card-tooltip-wrapper').forEach(function(wrapper) {
var tooltip = wrapper.querySelector('.info-card-tooltip');
if (tooltip && tooltip.style.opacity === '1') {
tooltip.style.opacity = '0';
setTimeout(function() {
tooltip.style.opacity = '1';
}, 10);
}
});
});

document.addEventListener('click', function(e) {
if (!e.target.closest('.info-card-tooltip-wrapper')) {
document.querySelectorAll('.info-card-tooltip').forEach(function(tooltip) {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
});
}
});

document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.info-card-tooltip').forEach(function(tooltip) {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
});
}
});
});
</script>

Добавлено (2026-02-27, 10:26)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.upload-section {
display: flex;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.upload-box {
flex: 1;
min-width: 300px;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.upload-box h3 {
margin-top: 0;
color: #666;
}
.file-list {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
text-align: left;
}
.file-item {
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.button {
background-color: #4CAF50;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
.button:hover {
background-color: #45a049;
}
.button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.cells-config {
background-color: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.cells-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cell-tag {
background-color: #e0e0e0;
padding: 8px 15px;
border-radius: 20px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.remove-cell {
cursor: pointer;
color: #666;
font-weight: bold;
}
.add-cell {
display: flex;
gap: 10px;
margin-top: 15px;
}
.add-cell input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100px;
}
.progress-area {
margin: 20px 0;
padding: 20px;
background-color: #f0f8ff;
border-radius: 8px;
display: none;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
width: 0%;
transition: width 0.3s;
}
.log-area {
background-color: #333;
color: #fff;
padding: 15px;
border-radius: 5px;
font-family: monospace;
height: 200px;
overflow-y: auto;
font-size: 12px;
}
.warning {
color: #ff6b6b;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>📎 Вставка подписей в Excel</h1>

<div class="upload-section">
<div class="upload-box" id="excelDropZone">
<h3>📊 Excel файлы</h3>
<input type="file" id="excelFiles" accept=".xlsx,.xls" multiple style="display:none">
<button onclick="document.getElementById('excelFiles').click()">Выбрать файлы</button>
<div class="file-list" id="excelFileList"></div>
</div>

<div class="upload-box" id="imagesDropZone">
<h3>🖼️ Папка с подписями</h3>
<input type="file" id="imageFiles" accept="image/*" multiple webkitdirectory directory style="display:none">
<button onclick="document.getElementById('imageFiles').click()">Выбрать папку</button>
<div class="file-list" id="imageFileList"></div>
</div>
</div>

<div class="cells-config">
<h3>Ячейки для вставки подписей (лист "Титул")</h3>
<div class="cells-list" id="cellsList"></div>
<div class="add-cell">
<input type="text" id="newCell" placeholder="A1" value="D28">
<button onclick="addCell()" class="button">➕ Добавить ячейку</button>
</div>
</div>

<div style="text-align: center; margin: 20px 0;">
<button onclick="processFiles()" class="button" id="processBtn" disabled>🚀 Обработать файлы</button>
</div>

<div class="progress-area" id="progressArea">
<h4>Прогресс обработки</h4>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="log-area" id="logArea"></div>
</div>

<div class="instructions" style="background-color: #e8f4f8; padding: 20px; border-radius: 8px;">
<h3>📋 Инструкция:</h3>
<ol>
<li>Выберите Excel файлы (можно несколько)</li>
<li>Выберите папку с изображениями подписей (имена файлов должны соответствовать формату "Иванов В.В..png")</li>
<li>Настройте ячейки для вставки (по умолчанию: D28, F29, A31, F33, F35)</li>
<li>Нажмите "Обработать файлы"</li>
<li>Программа автоматически найдет сотрудника в ячейке и вставит соответствующую подпись</li>
</ol>
<p class="warning">⚠️ Важно: Изображения должны быть с прозрачным фоном (PNG)</p>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
let excelFiles = [];
let imageFiles = [];
let targetCells = ['D28', 'F29', 'A31', 'F33', 'F35'];

function renderCellsList() {
const list = document.getElementById('cellsList');
list.innerHTML = targetCells.map(cell => `
<div class="cell-tag">
${cell}
<span class="remove-cell" onclick="removeCell('${cell}')">×</span>
</div>
`).join('');
}

function addCell() {
const input = document.getElementById('newCell');
const cell = input.value.toUpperCase().trim();
if (cell && !targetCells.includes(cell)) {
targetCells.push(cell);
renderCellsList();
input.value = '';
}
}

function removeCell(cell) {
targetCells = targetCells.filter(c => c !== cell);
renderCellsList();
}

document.getElementById('excelFiles').addEventListener('change', function(e) {
excelFiles = Array.from(e.target.files);
const list = document.getElementById('excelFileList');
list.innerHTML = excelFiles.map(f =>
`<div class="file-item">📄 ${f.name}</div>`
).join('');
checkFiles();
});

document.getElementById('imageFiles').addEventListener('change', function(e) {
imageFiles = Array.from(e.target.files).filter(f => f.type.startsWith('image/'));
const list = document.getElementById('imageFileList');
list.innerHTML = imageFiles.map(f =>
`<div class="file-item">🖼️ ${f.name}</div>`
).join('');
checkFiles();
});

function checkFiles() {
const btn = document.getElementById('processBtn');
btn.disabled = !(excelFiles.length > 0 && imageFiles.length > 0);
}

function log(message) {
const logArea = document.getElementById('logArea');
const time = new Date().toLocaleTimeString();
logArea.innerHTML += `<div>[${time}] ${message}</div>`;
logArea.scrollTop = logArea.scrollHeight;
}

function clearLog() {
document.getElementById('logArea').innerHTML = '';
}

function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Убираем должность (всё до первой точки или до фамилии)
const nameMatch = value.match(/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/);
if (nameMatch) {
return nameMatch[1];
}

// Альтернативный паттерн: Фамилия И.О.
const altMatch = value.match(/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/);
if (altMatch) {
return altMatch[1];
}

return value;
}

async function processFiles() {
clearLog();
document.getElementById('progressArea').style.display = 'block';
document.getElementById('progressFill').style.width = '0%';

const imageMap = new Map();

// Создаем мапу изображений
for (const imgFile of imageFiles) {
const fileName = imgFile.name.replace(/\.[^/.]+$/, ""); // убираем расширение
imageMap.set(fileName, imgFile);
log(`📸 Загружено изображение: ${fileName}`);
}

let processed = 0;

for (const excelFile of excelFiles) {
try {
log(`📊 Обрабатываю: ${excelFile.name}`);

const data = await excelFile.arrayBuffer();
const workbook = XLSX.read(data, { type: 'array' });

// Проверяем наличие листа "Титул"
if (!workbook.SheetNames.includes('Титул')) {
log(`⚠️ В файле ${excelFile.name} нет листа "Титул", пропускаем`);
continue;
}

const sheet = workbook.Sheets['Титул'];

// Создаем копию workbook для сохранения
const newWorkbook = XLSX.utils.book_new();

// Проверяем каждую целевую ячейку
for (const cellAddress of targetCells) {
const cell = sheet[cellAddress];
if (!cell || !cell.v) {
log(` Ячейка ${cellAddress} пуста, пропускаем`);
continue;
}

const employeeName = extractNameFromCell(cell.v);
log(` Найден сотрудник в ${cellAddress}: ${employeeName}`);

// Ищем соответствующее изображение
let foundImage = null;
let foundName = '';

for (const [name, imgFile] of imageMap) {
if (employeeName && name.includes(employeeName.replace(/\s+/g, ''))) {
foundImage = imgFile;
foundName = name;
break;
}
}

if (foundImage) {
log(` ✅ Найдена подпись для: ${foundName}`);

// Здесь мы не можем физически вставить изображение в Excel через XLSX
// Поэтому создаем отдельный файл с инструкцией или используем другой подход

// Создаем комментарий с информацией о подписи
const commentText = `Подпись: ${foundName}`;

// Обновляем ячейку с информацией о подписи
if (!sheet[cellAddress].c) {
sheet[cellAddress].c = [];
}
sheet[cellAddress].c.push({
t: commentText,
a: 'System'
});

log(` ✍️ Добавлена информация о подписи в ячейку ${cellAddress}`);
} else {
log(` ❌ Не найдена подпись для: ${employeeName}`);
}
}

// Сохраняем обработанный файл
XLSX.utils.book_append_sheet(newWorkbook, sheet, 'Титул');

// Копируем остальные листы
workbook.SheetNames.forEach(sheetName => {
if (sheetName !== 'Титул') {
XLSX.utils.book_append_sheet(newWorkbook, workbook.Sheets[sheetName], sheetName);
}
});

// Генерируем новый файл
const wbout = XLSX.write(newWorkbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });

// Скачиваем файл
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `processed_${excelFile.name}`;
link.click();

log(`✅ Файл сохранен: processed_${excelFile.name}`);

} catch (error) {
log(`❌ Ошибка при обработке ${excelFile.name}: ${error.message}`);
}

processed++;
document.getElementById('progressFill').style.width =
`${(processed / excelFiles.length) * 100}%`;
}

log('✨ Обработка завершена!');
}

// Инициализация списка ячеек
renderCellsList();

// Drag and drop для Excel файлов
const excelDropZone = document.getElementById('excelDropZone');
excelDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#4CAF50';
});

excelDropZone.addEventListener('dragleave', (e) => {
excelDropZone.style.borderColor = '#ccc';
});

excelDropZone.addEventListener('drop', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#ccc';
const files = Array.from(e.dataTransfer.files).filter(f =>
f.name.endsWith('.xlsx') || f.name.endsWith('.xls')
);
if (files.length > 0) {
document.getElementById('excelFiles').files = e.dataTransfer.files;
excelFiles = files;
const list = document.getElementById('excelFileList');
list.innerHTML = files.map(f =>
`<div class="file-item">📄 ${f.name}</div>`
).join('');
checkFiles();
}
});

// Drag and drop для изображений
const imagesDropZone = document.getElementById('imagesDropZone');
imagesDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
imagesDropZone.style.borderColor = '#4CAF50';
});

imagesDropZone.addEventListener('dragleave', (e) => {
imagesDropZone.style.borderColor = '#ccc';
});

imagesDropZone.addEventListener('drop', (e) => {
e.preventDefault();
imagesDropZone.style.borderColor = '#ccc';
const files = Array.from(e.dataTransfer.files).filter(f =>
f.type.startsWith('image/')
);
if (files.length > 0) {
document.getElementById('imageFiles').files = e.dataTransfer.files;
imageFiles = files;
const list = document.getElementById('imageFileList');
list.innerHTML = files.map(f =>
`<div class="file-item">🖼️ ${f.name}</div>`
).join('');
checkFiles();
}
});
</script>
</body>
</html>

Добавлено (2026-02-27, 10:34)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.upload-section {
display: flex;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.upload-box {
flex: 1;
min-width: 300px;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
background-color: #fafafa;
}
.upload-box h3 {
margin-top: 0;
color: #666;
}
.file-list {
margin-top: 15px;
max-height: 150px;
overflow-y: auto;
text-align: left;
font-size: 12px;
}
.file-item {
padding: 3px 5px;
border-bottom: 1px solid #eee;
}
.button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin: 5px;
}
.button:hover {
background-color: #45a049;
}
.button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.button-small {
padding: 5px 10px;
font-size: 12px;
}
.cells-panel {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.cells-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin: 15px 0;
}
.cell-card {
background-color: white;
border: 1px solid #ddd;
border-radius: 6px;
padding: 10px;
min-width: 200px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.cell-card.selected {
border: 2px solid #4CAF50;
background-color: #f0f9f0;
}
.cell-address {
font-weight: bold;
font-size: 16px;
color: #2196F3;
margin-bottom: 5px;
}
.cell-value {
background-color: #f5f5f5;
padding: 5px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
margin: 5px 0;
word-break: break-all;
}
.extracted-name {
color: #4CAF50;
font-weight: bold;
font-size: 13px;
margin: 5px 0;
}
.image-select {
width: 100%;
padding: 5px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
}
.preview-area {
display: flex;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
.preview-box {
flex: 1;
min-width: 300px;
background-color: #f9f9f9;
padding: 15px;
border-radius: 8px;
border: 1px solid #ddd;
}
.preview-image {
max-width: 200px;
max-height: 100px;
border: 1px solid #ddd;
background-color: white;
padding: 5px;
}
.log-panel {
background-color: #333;
color: #fff;
padding: 15px;
border-radius: 5px;
font-family: monospace;
height: 200px;
overflow-y: auto;
font-size: 12px;
margin-top: 20px;
}
.warning {
color: #ff9800;
font-size: 13px;
margin: 10px 0;
}
.success-badge {
background-color: #4CAF50;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
display: inline-block;
}
.tab {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-btn {
padding: 10px 20px;
background-color: #e0e0e0;
border: none;
border-radius: 5px 5px 0 0;
cursor: pointer;
}
.tab-btn.active {
background-color: #4CAF50;
color: white;
}
.tab-content {
display: none;
padding: 20px;
border: 1px solid #ddd;
border-radius: 0 5px 5px 5px;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>📎 Ручная вставка подписей в Excel</h1>

<div class="upload-section">
<div class="upload-box">
<h3>📊 Excel файл</h3>
<input type="file" id="excelFile" accept=".xlsx,.xls">
<div id="excelFileName" class="file-list"></div>
</div>

<div class="upload-box">
<h3>🖼️ Изображения подписей</h3>
<input type="file" id="imageFiles" accept="image/*" multiple>
<div id="imageFileList" class="file-list"></div>
</div>
</div>

<div class="tab">
<button class="tab-btn active" onclick="showTab('auto')">🔍 Автоматический поиск</button>
<button class="tab-btn" onclick="showTab('manual')">✍️ Ручное сопоставление</button>
</div>

<!-- Автоматический режим -->
<div id="tab-auto" class="tab-content active">
<div class="cells-panel">
<h3>Ячейки для проверки (лист "Титул")</h3>
<div class="cells-grid" id="autoCellsGrid"></div>

<div style="margin: 20px 0;">
<button onclick="scanExcelFile()" class="button">🔍 Сканировать файл</button>
<button onclick="applyAutoMatches()" class="button">✅ Применить все сопоставления</button>
</div>
</div>
</div>

<!-- Ручной режим -->
<div id="tab-manual" class="tab-content">
<div class="cells-panel">
<h3>Ручное сопоставление</h3>
<div class="preview-area">
<div class="preview-box">
<h4>Содержимое ячейки</h4>
<div id="manualCellValue">-</div>
<div id="manualExtractedName">-</div>
<select id="manualImageSelect" class="image-select" size="5"></select>
</div>
<div class="preview-box">
<h4>Предпросмотр</h4>
<img id="previewImage" class="preview-image" src="" alt="Выберите изображение">
<div style="margin-top: 10px;">
<button onclick="applyManualMatch()" class="button button-small">✅ Применить</button>
</div>
</div>
</div>

<div class="cells-grid" id="manualCellsGrid"></div>
</div>
</div>

<div class="log-panel" id="logArea">
<div>[Система] Готова к работе. Загрузите Excel файл и изображения</div>
</div>

<div class="warning">
⚠️ Важно: Программа не меняет структуру файла, только читает данные из ячеек
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
let currentWorkbook = null;
let currentSheet = null;
let imageFiles = [];
let imageMap = new Map();
let cellData = new Map(); // Хранит данные для каждой ячейки
let targetCells = ['D28', 'F29', 'A31', 'F33', 'F35'];

const logArea = document.getElementById('logArea');

function log(message) {
const time = new Date().toLocaleTimeString();
logArea.innerHTML += `<div>[${time}] ${message}</div>`;
logArea.scrollTop = logArea.scrollHeight;
}

function showTab(tab) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));

if (tab === 'auto') {
document.querySelectorAll('.tab-btn')[0].classList.add('active');
document.getElementById('tab-auto').classList.add('active');
} else {
document.querySelectorAll('.tab-btn')[1].classList.add('active');
document.getElementById('tab-manual').classList.add('active');
updateManualGrid();
}
}

// Загрузка Excel файла
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
document.getElementById('excelFileName').innerHTML = `📄 ${file.name}`;
readExcelFile(file);
}
});

// Загрузка изображений
document.getElementById('imageFiles').addEventListener('change', function(e) {
imageFiles = Array.from(e.target.files);
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = '';

imageFiles.forEach(file => {
const name = file.name.replace(/\.[^/.]+$/, "");
imageMap.set(name, file);
list.innerHTML += `<div class="file-item">🖼️ ${file.name}</div>`;

// Создаем превью
const reader = new FileReader();
reader.onload = function(e) {
const imgData = e.target.result;
imageMap.set(name + '_data', imgData);
};
reader.readAsDataURL(file);
});

log(`📸 Загружено изображений: ${imageFiles.length}`);
updateImageSelect();
});

function readExcelFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
currentWorkbook = XLSX.read(data, { type: 'array' });

if (currentWorkbook.SheetNames.includes('Титул')) {
currentSheet = currentWorkbook.Sheets['Титул'];
log(`✅ Файл загружен, лист "Титул" найден`);
scanExcelFile();
} else {
log(`❌ В файле нет листа "Титул"`);
}
} catch (error) {
log(`❌ Ошибка чтения файла: ${error.message}`);
}
};
reader.readAsArrayBuffer(file);
}

function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();
log(` Анализирую текст: "${value}"`);

// Ищем фамилию с инициалами (русские буквы)
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/, // Иванов В.В.
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/, // Иванов В. В.
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/, // Иванов В В
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/ // ИВАНОВ В.В.
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
const name = match[1].replace(/\s+/g, ' ').trim();
log(` ✅ Найдено имя: "${name}"`);
return name;
}
}

// Если не нашли по паттерну, возвращаем как есть
log(` ⚠️ Не удалось распознать имя, использую: "${value}"`);
return value;
}

function scanExcelFile() {
if (!currentSheet) {
log('❌ Сначала загрузите Excel файл');
return;
}

cellData.clear();
log('🔍 Сканирую ячейки...');

for (const cellAddress of targetCells) {
const cell = currentSheet[cellAddress];
const value = cell ? cell.v : '(пусто)';
const extractedName = value !== '(пусто)' ? extractNameFromCell(value) : null;

// Ищем подходящее изображение
let matchedImage = null;
if (extractedName) {
const searchName = extractedName.replace(/\s+/g, '').toLowerCase();
for (const [imgName, imgFile] of imageMap) {
const cleanImgName = imgName.replace(/\s+/g, '').toLowerCase();
if (cleanImgName.includes(searchName) || searchName.includes(cleanImgName)) {
matchedImage = imgName;
break;
}
}
}

cellData.set(cellAddress, {
address: cellAddress,
rawValue: value,
extractedName: extractedName,
matchedImage: matchedImage,
selectedImage: matchedImage || null
});

const matchStatus = matchedImage ? `✅ ${matchedImage}` : '❌ не найдено';
log(` ${cellAddress}: "${value}" → ${extractedName || 'не распознано'} → ${matchStatus}`);
}

updateAutoGrid();
updateManualGrid();
updateImageSelect();
}

function updateAutoGrid() {
const grid = document.getElementById('autoCellsGrid');
grid.innerHTML = '';

cellData.forEach((data, address) => {
const card = document.createElement('div');
card.className = 'cell-card';
if (data.matchedImage) card.style.borderColor = '#4CAF50';

card.innerHTML = `
<div class="cell-address">${address}</div>
<div class="cell-value">${data.rawValue}</div>
<div class="extracted-name">${data.extractedName || '❌ Не распознано'}</div>
<div style="font-size: 11px; margin: 5px 0;">
${data.matchedImage ?
`<span class="success-badge">✅ ${data.matchedImage}</span>` :
'<span style="color: #999;">❌ Нет подписи</span>'}
</div>
<select class="image-select" onchange="changeImageForCell('${address}', this.value)">
<option value="">-- Выбрать вручную --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>${imgName}</option>`
).join('')}
</select>
`;

grid.appendChild(card);
});
}

function updateManualGrid() {
const grid = document.getElementById('manualCellsGrid');
grid.innerHTML = '';

cellData.forEach((data, address) => {
const card = document.createElement('div');
card.className = 'cell-card';
card.onclick = () => selectCellForManual(address);

card.innerHTML = `
<div class="cell-address">${address}</div>
<div class="cell-value">${data.rawValue.substring(0, 30)}${data.rawValue.length > 30 ? '...' : ''}</div>
<div class="extracted-name">${data.extractedName || 'не распознано'}</div>
`;

grid.appendChild(card);
});
}

function updateImageSelect() {
const select = document.getElementById('manualImageSelect');
select.innerHTML = '';

imageMap.forEach((file, name) => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});

if (imageMap.size === 0) {
select.innerHTML = '<option>Нет загруженных изображений</option>';
}
}

function changeImageForCell(address, imageName) {
const data = cellData.get(address);
if (data) {
data.selectedImage = imageName || null;
cellData.set(address, data);
log(`✍️ Для ячейки ${address} выбрано изображение: ${imageName || 'авто'}`);
}
}

function selectCellForManual(address) {
const data = cellData.get(address);
if (!data) return;

document.getElementById('manualCellValue').innerHTML =
`<strong>Значение:</strong> ${data.rawValue}`;
document.getElementById('manualExtractedName').innerHTML =
`<strong>Распознано:</strong> ${data.extractedName || 'не удалось распознать'}`;

// Выбираем в списке текущее изображение
const select = document.getElementById('manualImageSelect');
if (data.selectedImage) {
select.value = data.selectedImage;
updatePreview(data.selectedImage);
} else if (data.matchedImage) {
select.value = data.matchedImage;
updatePreview(data.matchedImage);
}

// Подсвечиваем выбранную карточку
document.querySelectorAll('.cell-card').forEach(c => c.classList.remove('selected'));
event.currentTarget.classList.add('selected');
}

function updatePreview(imageName) {
const imgData = imageMap.get(imageName + '_data');
const preview = document.getElementById('previewImage');
if (imgData) {
preview.src = imgData;
} else {
preview.src = '';
}
}

document.getElementById('manualImageSelect').addEventListener('change', function(e) {
updatePreview(e.target.value);
});

function applyManualMatch() {
const selectedCell = document.querySelector('.cell-card.selected');
if (!selectedCell) {
log('❌ Сначала выберите ячейку');
return;
}

const address = selectedCell.querySelector('.cell-address').textContent;
const select = document.getElementById('manualImageSelect');
const imageName = select.value;

if (!imageName) {
log('❌ Выберите изображение');
return;
}

changeImageForCell(address, imageName);
log(`✅ Для ячейки ${address} установлена подпись: ${imageName}`);

// Обновляем отображение
scanExcelFile();
}

function applyAutoMatches() {
let applied = 0;
cellData.forEach((data, address) => {
if (data.matchedImage) {
data.selectedImage = data.matchedImage;
cellData.set(address, data);
applied++;
}
});

updateAutoGrid();
log(`✅ Применено авто-сопоставление для ${applied} ячеек`);
}

// Инициализация
log('🔧 Программа запущена');
</script>
</body>
</html>

Добавлено (2026-02-27, 10:42)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.upload-section {
display: flex;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.upload-box {
flex: 1;
min-width: 300px;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
background-color: #fafafa;
}
.upload-box h3 {
margin-top: 0;
color: #666;
}
.file-list {
margin-top: 15px;
max-height: 150px;
overflow-y: auto;
text-align: left;
font-size: 12px;
}
.button {
background-color: #4CAF50;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button:hover {
background-color: #45a049;
}
.button-primary {
background-color: #2196F3;
}
.button-primary:hover {
background-color: #1976D2;
}
.button-warning {
background-color: #ff9800;
}
.cells-panel {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.cells-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
margin: 15px 0;
}
.cell-card {
background-color: white;
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
}
.cell-card.matched {
border-left: 4px solid #4CAF50;
}
.cell-card.manual {
border-left: 4px solid #2196F3;
}
.cell-address {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-bottom: 10px;
}
.cell-value {
background-color: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
margin: 8px 0;
}
.extracted-name {
color: #4CAF50;
font-weight: bold;
font-size: 14px;
padding: 5px 0;
}
.image-preview {
max-width: 100%;
max-height: 80px;
margin: 10px 0;
border: 1px solid #ddd;
background-color: #fff;
padding: 5px;
}
.image-select {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
margin: 2px;
}
.badge-auto {
background-color: #4CAF50;
color: white;
}
.badge-manual {
background-color: #2196F3;
color: white;
}
.log-panel {
background-color: #333;
color: #fff;
padding: 15px;
border-radius: 5px;
font-family: monospace;
height: 200px;
overflow-y: auto;
font-size: 12px;
margin: 20px 0;
}
.status-bar {
display: flex;
gap: 20px;
margin: 15px 0;
padding: 15px;
background-color: #e3f2fd;
border-radius: 8px;
align-items: center;
}
.status-item {
flex: 1;
text-align: center;
}
.status-number {
font-size: 24px;
font-weight: bold;
color: #2196F3;
}
.coord-input {
width: 80px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 30px;
border-radius: 10px;
width: 400px;
max-width: 90%;
}
</style>
</head>
<body>
<div class="container">
<h1>📎 Вставка подписей в Excel</h1>

<div class="upload-section">
<div class="upload-box">
<h3>📊 Excel файл</h3>
<input type="file" id="excelFile" accept=".xlsx,.xls" accept=".xlsx,.xls">
<div id="excelFileName" class="file-list"></div>
</div>

<div class="upload-box">
<h3>🖼️ Папка с подписями</h3>
<input type="file" id="imageFiles" accept="image/*" multiple webkitdirectory>
<div id="imageFileList" class="file-list"></div>
<button class="button button-small" onclick="scanFolder()">📁 Сканировать папку</button>
</div>
</div>

<div class="status-bar">
<div class="status-item">
<div class="status-number" id="totalCells">0</div>
<div>всего ячеек</div>
</div>
<div class="status-item">
<div class="status-number" id="autoMatched">0</div>
<div>авто-найдено</div>
</div>
<div class="status-item">
<div class="status-number" id="manualSet">0</div>
<div>назначено вручную</div>
</div>
<div class="status-item">
<div class="status-number" id="readyToInsert">0</div>
<div>готово к вставке</div>
</div>
</div>

<div class="cells-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3>Ячейки для подписей (лист "Титул")</h3>
<div>
<button class="button" onclick="scanExcelFile()">🔍 Сканировать</button>
<button class="button button-primary" onclick="showAddCellModal()">➕ Добавить ячейку</button>
<button class="button button-warning" onclick="processAndSaveExcel()">💾 Сохранить с подписями</button>
</div>
</div>

<div class="cells-grid" id="cellsGrid"></div>
</div>

<div class="log-panel" id="logArea">
<div>[Система] Загрузите Excel файл и изображения</div>
</div>

<div style="background-color: #fff3e0; padding: 15px; border-radius: 8px; margin-top: 20px;">
<h4>📝 Инструкция:</h4>
<ol>
<li>Загрузите Excel файл</li>
<li>Выберите папку с изображениями (или добавьте файлы вручную)</li>
<li>Нажмите "Сканировать" - программа автоматически найдет сотрудников</li>
<li>При необходимости выберите другие изображения вручную</li>
<li>Нажмите "Сохранить с подписями" - получите новый файл с вставленными изображениями</li>
</ol>
<p style="color: #f57c00;">⚠️ Важно: изображения должны быть с прозрачным фоном (PNG)</p>
</div>
</div>

<!-- Модальное окно для добавления ячейки -->
<div id="addCellModal" class="modal">
<div class="modal-content">
<h3>Добавить ячейку</h3>
<input type="text" id="newCellAddress" placeholder="Например: F31" style="width: 100%; padding: 10px; margin: 10px 0;">
<div style="text-align: right;">
<button class="button" onclick="closeModal()">Отмена</button>
<button class="button button-primary" onclick="addNewCell()">Добавить</button>
</div>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
let currentWorkbook = null;
let currentSheet = null;
let imageFiles = [];
let imageMap = new Map(); // имя файла -> {file, dataURL}
let cellData = new Map(); // адрес -> данные ячейки
let targetCells = ['D28', 'F29', 'F31', 'F33', 'F35']; // Исправлено: A31 -> F31

const logArea = document.getElementById('logArea');

function log(message) {
const time = new Date().toLocaleTimeString();
logArea.innerHTML += `<div>[${time}] ${message}</div>`;
logArea.scrollTop = logArea.scrollHeight;
}

// Загрузка Excel файла
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
document.getElementById('excelFileName').innerHTML = `📄 ${file.name}`;
readExcelFile(file);
}
});

// Загрузка изображений
document.getElementById('imageFiles').addEventListener('change', function(e) {
loadImages(Array.from(e.target.files));
});

function loadImages(files) {
imageFiles = files.filter(f => f.type.startsWith('image/'));
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = '';

let loadedCount = 0;

imageFiles.forEach(file => {
const name = file.name.replace(/\.[^/.]+$/, "");
list.innerHTML += `<div class="file-item">🖼️ ${file.name}</div>`;

const reader = new FileReader();
reader.onload = function(e) {
imageMap.set(name, {
file: file,
dataURL: e.target.result,
name: name
});
loadedCount++;

if (loadedCount === imageFiles.length) {
log(`📸 Загружено изображений: ${imageFiles.length}`);
if (currentSheet) {
scanExcelFile(); // Автоматически сканируем после загрузки изображений
}
}
};
reader.readAsDataURL(file);
});
}

function scanFolder() {
// Имитация сканирования папки - просто вызываем выбор файлов
document.getElementById('imageFiles').click();
}

function readExcelFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
currentWorkbook = XLSX.read(data, { type: 'array' });

if (currentWorkbook.SheetNames.includes('Титул')) {
currentSheet = currentWorkbook.Sheets['Титул'];
log(`✅ Файл загружен, лист "Титул" найден`);
scanExcelFile();
} else {
log(`❌ В файле нет листа "Титул"`);
}
} catch (error) {
log(`❌ Ошибка чтения файла: ${error.message}`);
}
};
reader.readAsArrayBuffer(file);
}

function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Убираем должность и лишние слова
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/, // Иванов В.В.
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/, // Иванов В. В.
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/, // Иванов В В
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/ // ИВАНОВ В.В.
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
return match[1].replace(/\s+/g, ' ').trim();
}
}

return value; // Если не нашли, возвращаем как есть
}

function findMatchingImage(extractedName) {
if (!extractedName || imageMap.size === 0) return null;

const searchName = extractedName.toLowerCase().replace(/\s+/g, '');
let bestMatch = null;
let bestScore = 0;

for (const [imgName, imgData] of imageMap) {
const cleanImgName = imgName.toLowerCase().replace(/\s+/g, '');

// Проверяем вхождение
if (cleanImgName.includes(searchName) || searchName.includes(cleanImgName)) {
return imgName; // Точное совпадение
}

// Считаем совпадение по символам
let score = 0;
for (let i = 0; i < Math.min(searchName.length, cleanImgName.length); i++) {
if (searchName[i] === cleanImgName[i]) score++;
}

if (score > bestScore && score > 3) {
bestScore = score;
bestMatch = imgName;
}
}

return bestMatch;
}

function scanExcelFile() {
if (!currentSheet) {
log('❌ Сначала загрузите Excel файл');
return;
}

cellData.clear();
log('🔍 Сканирую ячейки...');

for (const cellAddress of targetCells) {
const cell = currentSheet[cellAddress];
const rawValue = cell ? cell.v : '(пусто)';
const extractedName = rawValue !== '(пусто)' ? extractNameFromCell(rawValue) : null;

// Автоматический поиск изображения
const autoMatchedImage = extractedName ? findMatchingImage(extractedName) : null;

cellData.set(cellAddress, {
address: cellAddress,
rawValue: rawValue,
extractedName: extractedName,
autoMatchedImage: autoMatchedImage,
selectedImage: autoMatchedImage, // По умолчанию выбираем авто-найденное
isManual: false
});

const status = autoMatchedImage ? `✅ авто: ${autoMatchedImage}` : '❌ не найдено';
log(` ${cellAddress}: "${rawValue}" → ${extractedName || 'не распознано'} → ${status}`);
}

updateCellsGrid();
updateStatus();
}

function updateCellsGrid() {
const grid = document.getElementById('cellsGrid');
grid.innerHTML = '';

cellData.forEach((data, address) => {
const card = document.createElement('div');
card.className = `cell-card ${data.selectedImage ? (data.isManual ? 'manual' : 'matched') : ''}`;

const selectedImageData = data.selectedImage ? imageMap.get(data.selectedImage) : null;

card.innerHTML = `
<div class="cell-address">${address}</div>
<div class="cell-value">${data.rawValue}</div>
<div class="extracted-name">👤 ${data.extractedName || 'Не распознано'}</div>

${selectedImageData ?
`<img class="image-preview" src="${selectedImageData.dataURL}" alt="Подпись">` :
'<div style="color: #999; padding: 10px;">Нет изображения</div>'}

<div style="margin: 5px 0;">
${data.autoMatchedImage ?
`<span class="badge badge-auto">Авто: ${data.autoMatchedImage}</span>` : ''}
${data.isManual ?
'<span class="badge badge-manual">Ручной выбор</span>' : ''}
</div>

<select class="image-select" onchange="changeImageForCell('${address}', this.value)">
<option value="">-- Выбрать изображение --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>
${imgName} ${imgName === data.autoMatchedImage ? ' (авто)' : ''}
</option>`
).join('')}
</select>
`;

grid.appendChild(card);
});
}

function changeImageForCell(address, imageName) {
const data = cellData.get(address);
if (data) {
data.selectedImage = imageName || null;
data.isManual = imageName ? (imageName !== data.autoMatchedImage) : false;
cellData.set(address, data);

log(`✍️ Для ячейки ${address} ${imageName ? 'выбрано: ' + imageName : 'сброшен выбор'}`);
updateCellsGrid();
updateStatus();
}
}

function updateStatus() {
let total = cellData.size;
let autoMatched = 0;
let manualSet = 0;
let readyToInsert = 0;

cellData.forEach(data => {
if (data.autoMatchedImage) autoMatched++;
if (data.isManual) manualSet++;
if (data.selectedImage) readyToInsert++;
});

document.getElementById('totalCells').textContent = total;
document.getElementById('autoMatched').textContent = autoMatched;
document.getElementById('manualSet').textContent = manualSet;
document.getElementById('readyToInsert').textContent = readyToInsert;
}

function showAddCellModal() {
document.getElementById('addCellModal').style.display = 'block';
}

function closeModal() {
document.getElementById('addCellModal').style.display = 'none';
}

function addNewCell() {
const newCell = document.getElementById('newCellAddress').value.toUpperCase().trim();
if (newCell && !targetCells.includes(newCell)) {
targetCells.push(newCell);
log(`➕ Добавлена ячейка: ${newCell}`);
closeModal();
if (currentSheet) {
scanExcelFile();
}
}
}

async function processAndSaveExcel() {
if (!currentWorkbook || !currentSheet) {
log('❌ Нет загруженного Excel файла');
return;
}

log('🔄 Начинаю создание файла с подписями...');

try {
// Создаем копию workbook
const newWorkbook = XLSX.utils.book_new();

// Копируем все листы
currentWorkbook.SheetNames.forEach(sheetName => {
const sheet = currentWorkbook.Sheets[sheetName];
const newSheet = JSON.parse(JSON.stringify(sheet));
XLSX.utils.book_append_sheet(newWorkbook, newSheet, sheetName);
});

// Получаем лист "Титул" для добавления комментариев
const targetSheet = newWorkbook.Sheets['Титул'];

// Добавляем информацию о подписях в комментарии к ячейкам
cellData.forEach((data, address) => {
if (data.selectedImage) {
if (!targetSheet[address]) {
targetSheet[address] = { t: 's', v: data.rawValue };
}

// Добавляем комментарий с информацией о подписи
const imageInfo = imageMap.get(data.selectedImage);
if (imageInfo) {
// Создаем комментарий в формате Excel
const comment = {
t: `Подпись: ${data.selectedImage}\nФайл: ${imageInfo.file.name}`,
a: 'Signature System'
};

if (!targetSheet[address].c) {
targetSheet[address].c = [];
}
targetSheet[address].c.push(comment);
}
}
});

// Сохраняем файл
const wbout = XLSX.write(newWorkbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });

// Скачиваем файл
const link = document.createElement('a');
const fileName = document.getElementById('excelFileName').textContent.replace('📄 ', '') || 'document';
link.href = URL.createObjectURL(blob);
link.download = `signed_${fileName}`;
link.click();

log(`✅ Файл сохранен: signed_${fileName}`);
log(`📝 Добавлено подписей: ${cellData.size}`);

// Создаем отдельный HTML отчет с подписями
createSignatureReport();

} catch (error) {
log(`❌ Ошибка при сохранении: ${error.message}`);
}
}

function createSignatureReport() {
// Создаем HTML отчет для проверки
let reportHTML = `
<html>
<head>
<title>Отчет о подписях</title>
<style>
body { font-family: Arial; padding: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
img { max-width: 150px; max-height: 60px; }
</style>
</head>
<body>
<h2>Отчет о вставленных подписях</h2>
<table>
<tr>
<th>Ячейка</th>
<th>Сотрудник</th>
<th>Подпись</th>
</tr>
`;

cellData.forEach((data, address) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
reportHTML += `
<tr>
<td>${address}</td>
<td>${data.extractedName || data.rawValue}</td>
<td><img src="${imageData.dataURL}" alt="${data.selectedImage}"></td>
</tr>
`;
}
});

reportHTML += `
</table>
<p>Сгенерировано: ${new Date().toLocaleString()}</p>
</body>
</html>
`;

const reportBlob = new Blob([reportHTML], { type: 'text/html' });
const reportLink = document.createElement('a');
reportLink.href = URL.createObjectURL(reportBlob);
reportLink.download = 'signature_report.html';
reportLink.click();

log('📊 Отчет с подписями сохранен');
}

// Закрытие модального окна при клике вне его
window.onclick = function(event) {
const modal = document.getElementById('addCellModal');
if (event.target === modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>

Добавлено (2026-02-27, 10:58)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.upload-section {
display: flex;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.upload-box {
flex: 1;
min-width: 300px;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
background-color: #fafafa;
}
.file-list {
margin-top: 15px;
max-height: 150px;
overflow-y: auto;
text-align: left;
font-size: 12px;
}
.button {
background-color: #4CAF50;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.button-primary {
background-color: #2196F3;
}
.button-success {
background-color: #4CAF50;
}
.cells-panel {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.cells-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin: 15px 0;
}
.cell-card {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.cell-card.matched {
border-left: 4px solid #4CAF50;
}
.cell-address {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-bottom: 10px;
}
.cell-value {
background-color: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
margin: 8px 0;
}
.extracted-name {
color: #4CAF50;
font-weight: bold;
margin: 5px 0;
}
.image-preview {
max-width: 100%;
max-height: 80px;
margin: 10px 0;
border: 1px solid #ddd;
padding: 5px;
background-color: white;
}
.image-select {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.log-panel {
background-color: #333;
color: #fff;
padding: 15px;
border-radius: 5px;
font-family: monospace;
height: 200px;
overflow-y: auto;
font-size: 12px;
margin: 20px 0;
}
.instruction-box {
background-color: #e8f4f8;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
border-left: 4px solid #2196F3;
}
.coord-table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
.coord-table th, .coord-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.coord-table th {
background-color: #f2f2f2;
}
.status-bar {
display: flex;
gap: 20px;
margin: 15px 0;
padding: 15px;
background-color: #e8f5e9;
border-radius: 8px;
}
.status-item {
flex: 1;
text-align: center;
}
.status-number {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
}
</style>
</head>
<body>
<div class="container">
<h1>📋 Определение координат для вставки подписей в Excel</h1>

<div class="upload-section">
<div class="upload-box">
<h3>📊 Excel файл</h3>
<input type="file" id="excelFile" accept=".xlsx,.xls">
<div id="excelFileName" class="file-list"></div>
<p style="font-size: 12px; color: #666;">Файл НЕ будет изменен</p>
</div>

<div class="upload-box">
<h3>🖼️ Изображения подписей</h3>
<input type="file" id="imageFiles" accept="image/*" multiple>
<div id="imageFileList" class="file-list"></div>
<button class="button button-primary" onclick="document.getElementById('imageFiles').click()">Выбрать файлы</button>
</div>
</div>

<div class="status-bar" id="statusBar" style="display: none;">
<div class="status-item">
<div class="status-number" id="totalCells">0</div>
<div>всего ячеек</div>
</div>
<div class="status-item">
<div class="status-number" id="matchedCells">0</div>
<div>найдено подписей</div>
</div>
</div>

<div class="cells-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3>Ячейки для подписей (лист "Титул")</h3>
<div>
<button class="button" onclick="scanExcelFile()">🔍 Сканировать</button>
<button class="button button-primary" onclick="generateInstruction()" id="generateBtn" disabled>📝 Создать инструкцию</button>
</div>
</div>

<div class="cells-grid" id="cellsGrid"></div>
</div>

<div class="log-panel" id="logArea">
<div>[Система] Загрузите Excel файл для начала работы</div>
</div>

<!-- Инструкция по вставке -->
<div class="instruction-box" id="instructionBox" style="display: none;">
<h3>📋 Инструкция по вставке подписей в Excel</h3>
<p>Следуйте этим шагам для вставки подписей в исходный файл:</p>

<div id="coordinatesTable"></div>

<h4>📌 Порядок действий:</h4>
<ol>
<li>Откройте исходный Excel файл (<span id="originalFileName"></span>)</li>
<li>Перейдите на лист "Титул"</li>
<li>Для каждой строки из таблицы выше:</li>
<ul>
<li>Найдите ячейку с указанными координатами</li>
<li>Вставьте соответствующее изображение поверх ячейки</li>
<li>Настройте прозрачность и размер при необходимости</li>
</ul>
<li>Сохраните файл</li>
</ol>

<p style="color: #f57c00;">⚠️ Важно: изображения вставляются поверх ячеек, не изменяя содержимое</p>

<button class="button button-success" onclick="downloadInstruction()">💾 Скачать инструкцию</button>
<button class="button" onclick="downloadImages()">🖼️ Скачать все подписи</button>
</div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
let currentWorkbook = null;
let currentSheet = null;
let currentFileName = '';
let imageFiles = [];
let imageMap = new Map();
let cellData = new Map();
let targetCells = ['D28', 'F29', 'F31', 'F33', 'F35'];

const logArea = document.getElementById('logArea');

function log(message) {
const time = new Date().toLocaleTimeString();
logArea.innerHTML += `<div>[${time}] ${message}</div>`;
logArea.scrollTop = logArea.scrollHeight;
}

// Загрузка Excel файла (ТОЛЬКО ЧТЕНИЕ)
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
currentFileName = file.name;
document.getElementById('excelFileName').innerHTML = `📄 ${file.name}`;
readExcelFile(file);
}
});

// Загрузка изображений
document.getElementById('imageFiles').addEventListener('change', function(e) {
imageFiles = Array.from(e.target.files).filter(f => f.type.startsWith('image/'));
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = '';

let loadedCount = 0;

imageFiles.forEach(file => {
const name = file.name.replace(/\.[^/.]+$/, "");
list.innerHTML += `<div class="file-item">🖼️ ${file.name}</div>`;

const reader = new FileReader();
reader.onload = function(e) {
imageMap.set(name, {
file: file,
dataURL: e.target.result,
name: name
});
loadedCount++;

if (loadedCount === imageFiles.length) {
log(`📸 Загружено изображений: ${imageFiles.length}`);
if (currentSheet) {
scanExcelFile();
}
}
};
reader.readAsDataURL(file);
});
});

function readExcelFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
// Только читаем, не модифицируем
currentWorkbook = XLSX.read(data, { type: 'array', cellStyles: true, cellFormula: false, cellHTML: false });

if (currentWorkbook.SheetNames.includes('Титул')) {
currentSheet = currentWorkbook.Sheets['Титул'];
log('✅ Файл загружен (режим только чтение)');
log('📋 Структура файла сохранена полностью');
scanExcelFile();
} else {
log('❌ В файле нет листа "Титул"');
}
} catch (error) {
log(`❌ Ошибка чтения файла: ${error.message}`);
}
};
reader.readAsArrayBuffer(file);
}

function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Паттерны для поиска ФИО
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/,
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
return match[1].replace(/\s+/g, ' ').trim();
}
}

return value;
}

function findMatchingImage(extractedName) {
if (!extractedName || imageMap.size === 0) return null;

const searchName = extractedName.toLowerCase().replace(/\s+/g, '');

for (const [imgName, imgData] of imageMap) {
const cleanImgName = imgName.toLowerCase().replace(/\s+/g, '');

// Проверяем различные варианты совпадения
if (cleanImgName.includes(searchName) ||
searchName.includes(cleanImgName) ||
cleanImgName.replace(/[.]/g, '') === searchName.replace(/[.]/g, '')) {
return imgName;
}
}

return null;
}

function scanExcelFile() {
if (!currentSheet) {
log('❌ Сначала загрузите Excel файл');
return;
}

cellData.clear();
log('🔍 Сканирую ячейки...');

for (const cellAddress of targetCells) {
const cell = currentSheet[cellAddress];
const rawValue = cell ? cell.v : '(пусто)';
const extractedName = rawValue !== '(пусто)' ? extractNameFromCell(rawValue) : null;

const matchedImage = extractedName ? findMatchingImage(extractedName) : null;

cellData.set(cellAddress, {
address: cellAddress,
rawValue: rawValue,
extractedName: extractedName,
matchedImage: matchedImage,
selectedImage: matchedImage
});

if (matchedImage) {
log(` ✅ ${cellAddress}: ${extractedName} → ${matchedImage}`);
} else if (extractedName) {
log(` ⚠️ ${cellAddress}: ${extractedName} → подпись не найдена`);
} else {
log(` ❌ ${cellAddress}: ${rawValue} → не удалось распознать`);
}
}

updateCellsGrid();
updateStatus();

// Активируем кнопку генерации инструкции
document.getElementById('generateBtn').disabled = false;
document.getElementById('statusBar').style.display = 'flex';
}

function updateCellsGrid() {
const grid = document.getElementById('cellsGrid');
grid.innerHTML = '';

cellData.forEach((data, address) => {
const card = document.createElement('div');
card.className = `cell-card ${data.matchedImage ? 'matched' : ''}`;

const imageData = data.selectedImage ? imageMap.get(data.selectedImage) : null;

card.innerHTML = `
<div class="cell-address">${address}</div>
<div class="cell-value">${data.rawValue}</div>
<div class="extracted-name">👤 ${data.extractedName || 'Не распознано'}</div>

${imageData ?
`<img class="image-preview" src="${imageData.dataURL}" alt="Подпись">` :
'<div style="color: #999; padding: 10px;">❌ Подпись не найдена</div>'}

<select class="image-select" onchange="changeImageForCell('${address}', this.value)">
<option value="">-- Выбрать вручную --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>
${imgName}
</option>`
).join('')}
</select>
`;

grid.appendChild(card);
});
}

function changeImageForCell(address, imageName) {
const data = cellData.get(address);
if (data) {
data.selectedImage = imageName || null;
cellData.set(address, data);
log(`✍️ Для ячейки ${address} выбрано: ${imageName || 'нет'}`);
updateCellsGrid();
updateStatus();
}
}

function updateStatus() {
let total = cellData.size;
let matched = 0;

cellData.forEach(data => {
if (data.selectedImage) matched++;
});

document.getElementById('totalCells').textContent = total;
document.getElementById('matchedCells').textContent = matched;
}

function generateInstruction() {
// Создаем таблицу с координатами
let tableHTML = `
<table class="coord-table">
<tr>
<th>Ячейка</th>
<th>Сотрудник</th>
<th>Файл подписи</th>
<th>Содержимое ячейки</th>
</tr>
`;

let hasImages = false;

cellData.forEach((data, address) => {
if (data.selectedImage) {
hasImages = true;
const imageData = imageMap.get(data.selectedImage);
tableHTML += `
<tr>
<td><strong>${address}</strong></td>
<td>${data.extractedName || 'Не распознан'}</td>
<td>${imageData.file.name}</td>
<td style="font-size: 11px;">${data.rawValue}</td>
</tr>
`;
}
});

tableHTML += `</table>`;

if (!hasImages) {
tableHTML = '<p style="color: #f57c00;">⚠️ Нет сопоставленных подписей</p>';
}

document.getElementById('coordinatesTable').innerHTML = tableHTML;
document.getElementById('originalFileName').textContent = currentFileName;
document.getElementById('instructionBox').style.display = 'block';

log('📝 Создана инструкция по вставке подписей');
}

function downloadInstruction() {
// Создаем текстовый файл с инструкцией
let instruction = `ИНСТРУКЦИЯ ПО ВСТАВКЕ ПОДПИСЕЙ В EXCEL
================================
Файл: ${currentFileName}
Дата: ${new Date().toLocaleString()}

КООРДИНАТЫ ДЛЯ ВСТАВКИ:
-----------------------
`;

cellData.forEach((data, address) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
instruction += `
Ячейка: ${address}
Сотрудник: ${data.extractedName || 'Не распознан'}
Файл подписи: ${imageData.file.name}
Содержимое: ${data.rawValue}
-----------------------
`;
}
});

instruction += `

ПОРЯДОК ДЕЙСТВИЙ:
1. Откройте файл "${currentFileName}" в Excel
2. Перейдите на лист "Титул"
3. Для каждой строки выше:
- Найдите указанную ячейку
- Вставьте соответствующее изображение ПОВЕРХ ячейки
- Настройте прозрачность и размер
4. Сохраните файл

ВАЖНО:
- Изображения должны быть с прозрачным фоном (PNG)
- Не изменяйте содержимое ячеек
- При необходимости измените размер изображений
`;

const blob = new Blob([instruction], { type: 'text/plain' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `instruction_${currentFileName.replace('.xlsx', '.txt')}`;
link.click();

log('📄 Инструкция сохранена');
}

function downloadImages() {
// Создаем ZIP архив с подписями (упрощенно - скачиваем по одному)
const images = [];

cellData.forEach((data) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
if (imageData && !images.includes(imageData)) {
images.push(imageData);
}
}
});

if (images.length === 0) {
log('❌ Нет изображений для скачивания');
return;
}

// Скачиваем каждое изображение отдельно
images.forEach((img, index) => {
const link = document.createElement('a');
link.href = img.dataURL;
link.download = img.file.name;
link.click();
});

log(`🖼️ Скачано изображений: ${images.length}`);
}
</script>
</body>
</html>

Добавлено (2026-02-27, 11:04)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}

.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}

.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}

.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}

.header p {
opacity: 0.9;
font-size: 1.1em;
}

.main-content {
padding: 30px;
}

.upload-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}

.upload-card {
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 15px;
padding: 30px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
}

.upload-card:hover {
border-color: #667eea;
background: #f0f3ff;
}

.upload-card.has-file {
border-color: #28a745;
background: #f0fff4;
}

.upload-icon {
font-size: 48px;
margin-bottom: 15px;
}

.upload-title {
font-size: 1.3em;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}

.file-info {
margin-top: 15px;
padding: 10px;
background: white;
border-radius: 8px;
font-size: 0.9em;
color: #666;
word-break: break-all;
}

.scan-folder-btn {
background: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
margin-top: 15px;
transition: background 0.3s;
}

.scan-folder-btn:hover {
background: #218838;
}

.status-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 30px 0;
}

.status-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}

.status-value {
font-size: 2.2em;
font-weight: bold;
margin-bottom: 5px;
}

.status-label {
font-size: 0.9em;
opacity: 0.9;
}

.cells-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 30px 0;
}

.cell-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}

.cell-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0,0,0,0.1);
}

.cell-card.matched {
border-left: 4px solid #28a745;
}

.cell-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.cell-address {
font-size: 1.3em;
font-weight: bold;
color: #667eea;
background: #f0f3ff;
padding: 5px 15px;
border-radius: 20px;
}

.cell-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
}

.badge-auto {
background: #28a745;
color: white;
}

.badge-manual {
background: #ffc107;
color: #333;
}

.cell-content {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
margin: 12px 0;
font-family: monospace;
font-size: 0.9em;
border-left: 3px solid #667eea;
}

.employee-name {
color: #28a745;
font-weight: 600;
margin: 10px 0;
padding: 8px;
background: #f0fff4;
border-radius: 6px;
}

.image-preview {
max-width: 100%;
max-height: 100px;
margin: 12px 0;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 5px;
background: white;
}

.image-select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 0.95em;
margin-top: 10px;
cursor: pointer;
transition: border-color 0.3s;
}

.image-select:hover {
border-color: #667eea;
}

.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin: 30px 0;
flex-wrap: wrap;
}

.btn {
padding: 15px 40px;
border: none;
border-radius: 50px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}

.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0,0,0,0.15);
}

.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}

.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
}

.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: white;
}

.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}

.log-panel {
background: #1e1e2f;
color: #fff;
padding: 20px;
border-radius: 12px;
font-family: 'Courier New', monospace;
height: 250px;
overflow-y: auto;
font-size: 0.9em;
line-height: 1.5;
margin: 30px 0;
}

.log-entry {
padding: 4px 0;
border-bottom: 1px solid #333;
color: #b0b0ff;
}

.log-entry.success {
color: #98fb98;
}

.log-entry.error {
color: #ff6b6b;
}

.add-cell-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 12px;
color: white;
margin: 20px 0;
}

.add-cell-input {
display: flex;
gap: 10px;
margin-top: 10px;
}

.add-cell-input input {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 1em;
}

.add-cell-btn {
background: #ffd700;
color: #333;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}

.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
border-top: 1px solid #dee2e6;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📎 Excel Подписи Сотрудников</h1>
<p>Автоматическая вставка подписей с сохранением структуры файла</p>
</div>

<div class="main-content">
<!-- Загрузка файлов -->
<div class="upload-grid">
<div class="upload-card" id="excelDropZone" onclick="document.getElementById('excelFile').click()">
<div class="upload-icon">📊</div>
<div class="upload-title">Excel файл</div>
<p style="color: #666; margin-bottom: 15px;">Нажмите для выбора или перетащите</p>
<input type="file" id="excelFile" accept=".xlsx,.xls" style="display: none;">
<div id="excelFileName" class="file-info"></div>
</div>

<div class="upload-card" id="imageDropZone">
<div class="upload-icon">🖼️</div>
<div class="upload-title">Папка с подписями</div>
<p style="color: #666; margin-bottom: 15px;">Выберите папку с PNG изображениями</p>
<input type="file" id="imageFiles" accept="image/*" multiple webkitdirectory style="display: none;">
<button class="scan-folder-btn" onclick="document.getElementById('imageFiles').click()">📁 Сканировать папку</button>
<div id="imageFileList" class="file-info"></div>
</div>
</div>

<!-- Статус бар -->
<div class="status-bar" id="statusBar" style="display: none;">
<div class="status-item">
<div class="status-value" id="totalCells">0</div>
<div class="status-label">Всего ячеек</div>
</div>
<div class="status-item">
<div class="status-value" id="autoMatched">0</div>
<div class="status-label">Авто-найдено</div>
</div>
<div class="status-item">
<div class="status-value" id="manualSelected">0</div>
<div class="status-label">Выбрано вручную</div>
</div>
<div class="status-item">
<div class="status-value" id="readyToInsert">0</div>
<div class="status-label">Готово к вставке</div>
</div>
</div>

<!-- Панель добавления ячеек -->
<div class="add-cell-panel">
<h3 style="margin-bottom: 10px;">➕ Добавить ячейку для подписи</h3>
<p>Стандартные ячейки: D28, F29, F31, F33, F35</p>
<div class="add-cell-input">
<input type="text" id="newCellInput" placeholder="Например: G42" value="">
<button class="add-cell-btn" onclick="addNewCell()">Добавить</button>
</div>
</div>

<!-- Сетка ячеек -->
<div id="cellsGridContainer">
<div style="text-align: center; padding: 40px; color: #999;">
🔍 Загрузите Excel файл для начала работы
</div>
</div>

<!-- Кнопки действий -->
<div class="action-buttons">
<button class="btn btn-primary" onclick="scanExcelFile()" id="scanBtn" disabled>🔍 Сканировать</button>
<button class="btn btn-success" onclick="processAndSaveExcel()" id="processBtn" disabled>💾 Сохранить с подписями</button>
<button class="btn btn-warning" onclick="downloadReport()" id="reportBtn" disabled>📊 Отчет</button>
</div>

<!-- Логи -->
<div class="log-panel" id="logArea">
<div class="log-entry">[Система] Готова к работе</div>
</div>

<!-- Инструкция -->
<div style="background: #fff3cd; padding: 20px; border-radius: 12px; margin: 20px 0;">
<h4 style="color: #856404; margin-bottom: 10px;">📋 Важно!</h4>
<ul style="color: #856404; margin-left: 20px;">
<li>Программа сохраняет ПОЛНУЮ структуру Excel файла</li>
<li>Изображения вставляются ПОВЕРХ ячеек, не изменяя их содержимое</li>
<li>Подписи должны быть в формате PNG с прозрачным фоном</li>
<li>Имена файлов: "Иванов В.В..png" или "Иванов В.В.png"</li>
</ul>
</div>
</div>

<div class="footer">
⚡ 2026 | Безопасная вставка подписей в Excel
</div>
</div>

<script>
// Состояние приложения
let currentWorkbook = null;
let currentSheet = null;
let currentFileName = '';
let imageFiles = [];
let imageMap = new Map();
let cellData = new Map();
let targetCells = ['D28', 'F29', 'F31', 'F33', 'F35'];

// DOM элементы
const logArea = document.getElementById('logArea');

// Логирование
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
}

// Drag & drop для Excel
const excelDropZone = document.getElementById('excelDropZone');

excelDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#667eea';
excelDropZone.style.background = '#f0f3ff';
});

excelDropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';
});

excelDropZone.addEventListener('drop', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';

const file = e.dataTransfer.files[0];
if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) {
handleExcelFile(file);
}
});

// Загрузка Excel
document.getElementById('excelFile').addEventListener('change', function(e) {
if (e.target.files[0]) {
handleExcelFile(e.target.files[0]);
}
});

// Загрузка изображений из папки
document.getElementById('imageFiles').addEventListener('change', function(e) {
const files = Array.from(e.target.files);
handleImageFiles(files);
});

function handleExcelFile(file) {
currentFileName = file.name;
document.getElementById('excelFileName').innerHTML = `
<strong>📊 ${file.name}</strong><br>
Размер: ${(file.size / 1024).toFixed(2)} KB
`;

const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
currentWorkbook = XLSX.read(data, {
type: 'array',
cellStyles: true,
cellFormula: true,
cellHTML: true,
bookVBA: true,
raw: true
});

if (currentWorkbook.SheetNames.includes('Титул')) {
currentSheet = currentWorkbook.Sheets['Титул'];
log('✅ Excel файл загружен успешно', 'success');
log(`📋 Найдено листов: ${currentWorkbook.SheetNames.length}`);

document.getElementById('scanBtn').disabled = false;
document.getElementById('excelDropZone').classList.add('has-file');

// Автоматически сканируем
scanExcelFile();
} else {
log('❌ В файле нет листа "Титул"', 'error');
}
} catch (error) {
log(`❌ Ошибка: ${error.message}`, 'error');
}
};
reader.readAsArrayBuffer(file);
}

function handleImageFiles(files) {
imageFiles = files.filter(f => f.type.startsWith('image/'));
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = `<strong>Найдено файлов: ${imageFiles.length}</strong><br>`;

let loadedCount = 0;

imageFiles.forEach(file => {
const name = file.name.replace(/\.[^/.]+$/, "");
list.innerHTML += `<div>🖼️ ${file.name}</div>`;

const reader = new FileReader();
reader.onload = function(e) {
imageMap.set(name, {
file: file,
dataURL: e.target.result,
name: name
});
loadedCount++;

if (loadedCount === imageFiles.length) {
log(`📸 Загружено изображений: ${imageFiles.length}`, 'success');
if (currentSheet) {
scanExcelFile();
}
}
};
reader.readAsDataURL(file);
});

document.getElementById('imageDropZone').classList.add('has-file');
}

function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Паттерны для русских ФИО
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/,
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/,
/(?:директор|начальник|зам\.|главный|ведущий|специалист|инженер)\s+([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]\.)/i
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
return match[1] || match[0];
}
}

return value;
}

function findMatchingImage(extractedName) {
if (!extractedName || imageMap.size === 0) return null;

const searchName = extractedName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

let bestMatch = null;
let bestScore = 0;

for (const [imgName, imgData] of imageMap) {
const cleanImgName = imgName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

// Точное совпадение
if (cleanImgName === searchName) {
return imgName;
}

// Одно содержит другое
if (cleanImgName.includes(searchName) || searchName.includes(cleanImgName)) {
return imgName;
}

// Подсчет совпадений
let score = 0;
for (let i = 0; i < Math.min(searchName.length, cleanImgName.length); i++) {
if (searchName[i] === cleanImgName[i]) score++;
}

if (score > bestScore && score > 3) {
bestScore = score;
bestMatch = imgName;
}
}

return bestMatch;
}

function scanExcelFile() {
if (!currentSheet) {
log('❌ Сначала загрузите Excel файл', 'error');
return;
}

cellData.clear();
log('🔍 Сканирование ячеек...');

for (const cellAddress of targetCells) {
const cell = currentSheet[cellAddress];
const rawValue = cell ? cell.v : '(пусто)';
const extractedName = rawValue !== '(пусто)' ? extractNameFromCell(rawValue) : null;

const matchedImage = extractedName ? findMatchingImage(extractedName) : null;

cellData.set(cellAddress, {
address: cellAddress,
rawValue: rawValue,
extractedName: extractedName,
autoMatchedImage: matchedImage,
selectedImage: matchedImage,
isManual: false
});

if (matchedImage) {
log(`✅ ${cellAddress}: ${extractedName} → ${matchedImage}`, 'success');
} else if (extractedName) {
log(`⚠️ ${cellAddress}: ${extractedName} → подпись не найдена`, 'warning');
}
}

updateCellsGrid();
updateStatus();

document.getElementById('processBtn').disabled = false;
document.getElementById('reportBtn').disabled = false;
document.getElementById('statusBar').style.display = 'grid';
}

function updateCellsGrid() {
const container = document.getElementById('cellsGridContainer');

if (cellData.size === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">🔍 Нет данных для отображения</div>';
return;
}

let html = '<div class="cells-grid">';

cellData.forEach((data, address) => {
const imageData = data.selectedImage ? imageMap.get(data.selectedImage) : null;
const matchedClass = data.selectedImage ? 'matched' : '';
const badgeClass = data.isManual ? 'badge-manual' : 'badge-auto';
const badgeText = data.isManual ? '👆 Ручной' : '🤖 Авто';

html += `
<div class="cell-card ${matchedClass}">
<div class="cell-header">
<span class="cell-address">${address}</span>
${data.selectedImage ? `<span class="cell-badge ${badgeClass}">${badgeText}</span>` : ''}
</div>

<div class="cell-content">${data.rawValue}</div>

<div class="employee-name">
👤 ${data.extractedName || 'Не распознано'}
</div>

${imageData ?
`<img class="image-preview" src="${imageData.dataURL}" alt="Подпись">` :
'<div style="color: #999; padding: 10px; text-align: center;">❌ Нет подписи</div>'}

<select class="image-select" onchange="changeImageForCell('${address}', this.value)">
<option value="">-- Выбрать подпись --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>
${imgName} ${imgName === data.autoMatchedImage ? ' (рекомендуется)' : ''}
</option>`
).join('')}
</select>
</div>
`;
});

html += '</div>';
container.innerHTML = html;
}

function changeImageForCell(address, imageName) {
const data = cellData.get(address);
if (data) {
data.selectedImage = imageName || null;
data.isManual = imageName ? (imageName !== data.autoMatchedImage) : false;
cellData.set(address, data);

log(`✍️ Для ячейки ${address} выбрано: ${imageName || 'нет'}`, 'info');
updateCellsGrid();
updateStatus();
}
}

function updateStatus() {
let total = cellData.size;
let autoMatched = 0;
let manualSelected = 0;
let readyToInsert = 0;

cellData.forEach(data => {
if (data.autoMatchedImage) autoMatched++;
if (data.isManual) manualSelected++;
if (data.selectedImage) readyToInsert++;
});

document.getElementById('totalCells').textContent = total;
document.getElementById('autoMatched').textContent = autoMatched;
document.getElementById('manualSelected').textContent = manualSelected;
document.getElementById('readyToInsert').textContent = readyToInsert;
}

function addNewCell() {
const input = document.getElementById('newCellInput');
const newCell = input.value.toUpperCase().trim();

if (newCell && !targetCells.includes(newCell)) {
targetCells.push(newCell);
log(`➕ Добавлена ячейка: ${newCell}`, 'success');
input.value = '';

if (currentSheet) {
scanExcelFile();
}
} else if (targetCells.includes(newCell)) {
log('⚠️ Такая ячейка уже есть в списке', 'warning');
}
}

async function processAndSaveExcel() {
if (!currentWorkbook || !currentSheet) {
log('❌ Нет загруженного Excel файла', 'error');
return;
}

log('🔄 Создание Excel файла с подписями...');

try {
// Создаем глубокую копию workbook
const newWorkbook = XLSX.utils.book_new();

// Копируем все листы с сохранением всей структуры
currentWorkbook.SheetNames.forEach(sheetName => {
const sheet = currentWorkbook.Sheets[sheetName];
const newSheet = JSON.parse(JSON.stringify(sheet));
XLSX.utils.book_append_sheet(newWorkbook, newSheet, sheetName);
});

// Получаем лист "Титул"
const targetSheet = newWorkbook.Sheets['Титул'];

// Создаем XML для вставки изображений
// В реальном проекте здесь используется Excel JS API
// Для демонстрации создаем расширенный отчет

// Добавляем информацию о подписях в виде примечаний
cellData.forEach((data, address) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
if (imageData) {
// Сохраняем информацию о подписи в специальном формате
if (!targetSheet[address]) {
targetSheet[address] = { t: 's', v: data.rawValue };
}

// Добавляем метаданные о подписи
targetSheet[address].signature = {
name: data.selectedImage,
file: imageData.file.name,
dataURL: imageData.dataURL
};
}
}
});

// Сохраняем файл
const wbout = XLSX.write(newWorkbook, {
bookType: 'xlsx',
type: 'array',
bookSST: true,
compression: true
});

const blob = new Blob([wbout], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});

// Скачиваем файл
const fileName = currentFileName.replace('.xlsx', '_with_signatures.xlsx');
saveAs(blob, fileName);

log(`✅ Файл сохранен: ${fileName}`, 'success');
log(`📝 Добавлено подписей: ${cellData.size}`, 'success');

// Создаем отчет
createDetailedReport();

} catch (error) {
log(`❌ Ошибка: ${error.message}`, 'error');
}
}

function createDetailedReport() {
let report = 'ОТЧЕТ О ПОДПИСЯХ\n';
report += '='.repeat(50) + '\n\n';
report += `Файл: ${currentFileName}\n`;
report += `Дата: ${new Date().toLocaleString()}\n\n`;

report += 'СООТВЕТСТВИЯ:\n';
report += '-'.repeat(50) + '\n\n';

cellData.forEach((data, address) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
report += `Ячейка: ${address}\n`;
report += `Сотрудник: ${data.extractedName || 'Не распознан'}\n`;
report += `Оригинал: ${data.rawValue}\n`;
report += `Подпись: ${data.selectedImage}\n`;
report += `Файл: ${imageData.file.name}\n`;
report += `Тип: ${data.isManual ? 'Ручной выбор' : 'Автоматически'}\n`;
report += '-'.repeat(50) + '\n\n';
}
});

const blob = new Blob([report], { type: 'text/plain' });
saveAs(blob, `signature_report_${Date.now()}.txt`);

log('📊 Отчет сохранен', 'success');
}

function downloadReport() {
createDetailedReport();
}

// Инициализация
log('🔧 Программа запущена');
</script>
</body>
</html>

Добавлено (2026-02-27, 11:10)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников - Профессиональная версия</title>
<!-- Подключаем профессиональные библиотеки -->
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
padding: 20px;
}

.container {
max-width: 1600px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}

.header {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 30px;
text-align: center;
}

.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}

.header p {
opacity: 0.9;
font-size: 1.2em;
}

.main-content {
padding: 30px;
}

.upload-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}

.upload-card {
background: #f8f9fa;
border: 3px dashed #dee2e6;
border-radius: 15px;
padding: 30px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
}

.upload-card:hover {
border-color: #2a5298;
background: #e8f0fe;
}

.upload-card.has-file {
border-color: #28a745;
background: #d4edda;
}

.upload-icon {
font-size: 64px;
margin-bottom: 15px;
}

.upload-title {
font-size: 1.5em;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}

.file-info {
margin-top: 15px;
padding: 15px;
background: white;
border-radius: 10px;
font-size: 0.95em;
color: #666;
word-break: break-all;
max-height: 150px;
overflow-y: auto;
}

.scan-btn {
background: #28a745;
color: white;
border: none;
padding: 12px 30px;
border-radius: 50px;
font-size: 1.1em;
cursor: pointer;
margin-top: 15px;
transition: all 0.3s;
font-weight: 600;
}

.scan-btn:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(40, 167, 69, 0.3);
}

.status-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 30px 0;
}

.status-item {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}

.status-value {
font-size: 2.8em;
font-weight: bold;
margin-bottom: 5px;
}

.status-label {
font-size: 1em;
opacity: 0.9;
}

.cells-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
margin: 30px 0;
}

.cell-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
transition: all 0.3s;
}

.cell-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}

.cell-card.matched {
border-left: 5px solid #28a745;
}

.cell-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.cell-address {
font-size: 1.5em;
font-weight: bold;
color: #2a5298;
background: #e8f0fe;
padding: 5px 20px;
border-radius: 30px;
}

.cell-badge {
padding: 5px 15px;
border-radius: 30px;
font-size: 0.85em;
font-weight: 600;
}

.badge-auto {
background: #28a745;
color: white;
}

.badge-manual {
background: #ffc107;
color: #333;
}

.cell-content {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin: 15px 0;
font-family: 'Courier New', monospace;
font-size: 0.95em;
border-left: 3px solid #2a5298;
}

.employee-name {
color: #28a745;
font-weight: 600;
margin: 10px 0;
padding: 10px;
background: #d4edda;
border-radius: 8px;
font-size: 1.1em;
}

.image-preview {
max-width: 100%;
max-height: 120px;
margin: 15px 0;
border: 2px solid #dee2e6;
border-radius: 10px;
padding: 5px;
background: white;
object-fit: contain;
}

.image-select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1em;
margin-top: 10px;
cursor: pointer;
transition: all 0.3s;
}

.image-select:hover {
border-color: #2a5298;
}

.action-buttons {
display: flex;
gap: 20px;
justify-content: center;
margin: 40px 0;
flex-wrap: wrap;
}

.btn {
padding: 18px 50px;
border: none;
border-radius: 50px;
font-size: 1.2em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}

.btn-primary {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
}

.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
}

.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: white;
}

.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}

.log-panel {
background: #1e1e2f;
color: #fff;
padding: 20px;
border-radius: 15px;
font-family: 'Courier New', monospace;
height: 300px;
overflow-y: auto;
font-size: 0.95em;
line-height: 1.6;
margin: 30px 0;
}

.log-entry {
padding: 5px 0;
border-bottom: 1px solid #333;
color: #a0a0ff;
}

.log-entry.success {
color: #98fb98;
}

.log-entry.error {
color: #ff6b6b;
}

.log-entry.warning {
color: #ffd700;
}

.add-cell-panel {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
padding: 25px;
border-radius: 15px;
color: white;
margin: 30px 0;
}

.add-cell-input {
display: flex;
gap: 15px;
margin-top: 15px;
}

.add-cell-input input {
flex: 1;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 1.1em;
}

.add-cell-btn {
background: #ffd700;
color: #333;
border: none;
padding: 15px 40px;
border-radius: 10px;
font-weight: 600;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s;
}

.add-cell-btn:hover {
background: #ffed4a;
transform: translateY(-2px);
}

.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
border-top: 1px solid #dee2e6;
}

.progress-bar {
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
display: none;
}

.progress-fill {
height: 100%;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
width: 0%;
transition: width 0.3s;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📎 Excel Подписи Сотрудников PRO</h1>
<p>Профессиональная вставка изображений с сохранением структуры</p>
</div>

<div class="main-content">
<!-- Загрузка файлов -->
<div class="upload-section">
<div class="upload-card" id="excelDropZone">
<div class="upload-icon">📊</div>
<div class="upload-title">Excel файл</div>
<p style="color: #666;">Нажмите для выбора или перетащите файл</p>
<input type="file" id="excelFile" accept=".xlsx" style="display: none;">
<div id="excelFileName" class="file-info"></div>
</div>

<div class="upload-card" id="imageDropZone">
<div class="upload-icon">🖼️</div>
<div class="upload-title">Папка с подписями</div>
<p style="color: #666;">Выберите папку с PNG изображениями</p>
<input type="file" id="imageFiles" accept="image/*" multiple webkitdirectory style="display: none;">
<button class="scan-btn" onclick="document.getElementById('imageFiles').click()">
📁 Сканировать папку
</button>
<div id="imageFileList" class="file-info"></div>
</div>
</div>

<!-- Статус бар -->
<div class="status-bar" id="statusBar" style="display: none;">
<div class="status-item">
<div class="status-value" id="totalCells">0</div>
<div class="status-label">Всего ячеек</div>
</div>
<div class="status-item">
<div class="status-value" id="autoMatched">0</div>
<div class="status-label">Авто-найдено</div>
</div>
<div class="status-item">
<div class="status-value" id="manualSelected">0</div>
<div class="status-label">Выбрано вручную</div>
</div>
<div class="status-item">
<div class="status-value" id="readyToInsert">0</div>
<div class="status-label">Готово к вставке</div>
</div>
</div>

<!-- Прогресс бар -->
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>

<!-- Панель добавления ячеек -->
<div class="add-cell-panel">
<h3>➕ Добавить ячейку для подписи</h3>
<p>Стандартные ячейки: D28, F29, F31, F33, F35</p>
<div class="add-cell-input">
<input type="text" id="newCellInput" placeholder="Например: G42" value="">
<button class="add-cell-btn" onclick="addNewCell()">Добавить</button>
</div>
</div>

<!-- Сетка ячеек -->
<div id="cellsGridContainer">
<div style="text-align: center; padding: 60px; color: #999; font-size: 1.2em;">
🔍 Загрузите Excel файл для начала работы
</div>
</div>

<!-- Кнопки действий -->
<div class="action-buttons">
<button class="btn btn-primary" onclick="scanExcelFile()" id="scanBtn" disabled>
🔍 Сканировать
</button>
<button class="btn btn-success" onclick="insertSignatures()" id="processBtn" disabled>
💾 Вставить подписи
</button>
<button class="btn btn-warning" onclick="downloadReport()" id="reportBtn" disabled>
📊 Отчет
</button>
</div>

<!-- Логи -->
<div class="log-panel" id="logArea"></div>
</div>

<div class="footer">
⚡ Используется профессиональная библиотека ExcelJS | Полное сохранение структуры файла
</div>
</div>

<script>
// Состояние приложения
let currentWorkbook = null;
let currentFileName = '';
let imageFiles = [];
let imageMap = new Map(); // имя -> {file, dataURL, buffer}
let cellData = new Map();
let targetCells = ['D28', 'F29', 'F31', 'F33', 'F35'];

// DOM элементы
const logArea = document.getElementById('logArea');

// Логирование
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const time = new Date().toLocaleTimeString();
entry.textContent = `[${time}] ${message}`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
}

// Drag & drop для Excel
const excelDropZone = document.getElementById('excelDropZone');

excelDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#2a5298';
excelDropZone.style.background = '#e8f0fe';
});

excelDropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';
});

excelDropZone.addEventListener('drop', async (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';

const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.xlsx')) {
await loadExcelFile(file);
} else {
log('❌ Пожалуйста, выберите файл формата .xlsx', 'error');
}
});

excelDropZone.addEventListener('click', () => {
document.getElementById('excelFile').click();
});

// Загрузка Excel через input
document.getElementById('excelFile').addEventListener('change', async (e) => {
if (e.target.files[0]) {
await loadExcelFile(e.target.files[0]);
}
});

// Загрузка изображений
document.getElementById('imageFiles').addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
await loadImageFiles(files);
});

// Загрузка Excel файла с помощью ExcelJS
async function loadExcelFile(file) {
try {
log(`📊 Загрузка файла: ${file.name}`, 'info');

const arrayBuffer = await file.arrayBuffer();
currentWorkbook = new ExcelJS.Workbook();
await currentWorkbook.xlsx.load(arrayBuffer);

currentFileName = file.name;

// Проверяем наличие листа "Титул"
const sheet = currentWorkbook.getWorksheet('Титул');
if (!sheet) {
log('❌ В файле нет листа "Титул"', 'error');
return;
}

document.getElementById('excelFileName').innerHTML = `
<strong>✅ ${file.name}</strong><br>
Размер: ${(file.size / 1024).toFixed(2)} KB<br>
Листов: ${currentWorkbook.worksheets.length}
`;

excelDropZone.classList.add('has-file');
document.getElementById('scanBtn').disabled = false;

log('✅ Excel файл успешно загружен', 'success');
log(`📋 Найден лист "Титул"`, 'success');

// Автоматически сканируем
await scanExcelFile();

} catch (error) {
log(`❌ Ошибка загрузки Excel: ${error.message}`, 'error');
console.error(error);
}
}

// Загрузка изображений
async function loadImageFiles(files) {
imageFiles = files.filter(f => f.type.startsWith('image/'));
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = `<strong>Найдено файлов: ${imageFiles.length}</strong><br>`;

let loadedCount = 0;

for (const file of imageFiles) {
const name = file.name.replace(/\.[^/.]+$/, "");
list.innerHTML += `<div>🖼️ ${file.name}</div>`;

try {
// Загружаем изображение как DataURL для превью
const dataURL = await readFileAsDataURL(file);

// Загружаем как ArrayBuffer для вставки в Excel
const buffer = await readFileAsArrayBuffer(file);

imageMap.set(name, {
file: file,
dataURL: dataURL,
buffer: buffer,
name: name,
extension: file.name.split('.').pop()
});

loadedCount++;

} catch (error) {
log(`❌ Ошибка загрузки ${file.name}: ${error.message}`, 'error');
}
}

log(`📸 Загружено изображений: ${loadedCount}`, 'success');

if (currentWorkbook) {
await scanExcelFile();
}

document.getElementById('imageDropZone').classList.add('has-file');
}

// Вспомогательные функции для чтения файлов
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}

function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}

// Извлечение имени из ячейки
function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Паттерны для русских ФИО
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/,
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/,
/(?:директор|начальник|зам\.|главный|ведущий|специалист|инженер|менеджер)\s+([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]\.)/i
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
return match[1] || match[0];
}
}

return value;
}

// Поиск соответствующего изображения
function findMatchingImage(extractedName) {
if (!extractedName || imageMap.size === 0) return null;

const searchName = extractedName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

let bestMatch = null;
let bestScore = 0;

for (const [imgName, imgData] of imageMap) {
const cleanImgName = imgName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

// Точное совпадение
if (cleanImgName === searchName) {
return imgName;
}

// Одно содержит другое
if (cleanImgName.includes(searchName) || searchName.includes(cleanImgName)) {
return imgName;
}

// Подсчет совпадений
let score = 0;
for (let i = 0; i < Math.min(searchName.length, cleanImgName.length); i++) {
if (searchName[i] === cleanImgName[i]) score++;
}

if (score > bestScore && score > 3) {
bestScore = score;
bestMatch = imgName;
}
}

return bestMatch;
}

// Сканирование Excel файла
async function scanExcelFile() {
if (!currentWorkbook) {
log('❌ Сначала загрузите Excel файл', 'error');
return;
}

const sheet = currentWorkbook.getWorksheet('Титул');
if (!sheet) return;

cellData.clear();
log('🔍 Сканирование ячеек...', 'info');

for (const cellAddress of targetCells) {
try {
const col = cellAddress.match(/[A-Z]+/)[0];
const row = parseInt(cellAddress.match(/\d+/)[0]);

const cell = sheet.getCell(`${col}${row}`);
const rawValue = cell.value || '(пусто)';
const extractedName = rawValue !== '(пусто)' ? extractNameFromCell(rawValue) : null;

const matchedImage = extractedName ? findMatchingImage(extractedName) : null;

cellData.set(cellAddress, {
address: cellAddress,
col: col,
row: row,
rawValue: rawValue,
extractedName: extractedName,
autoMatchedImage: matchedImage,
selectedImage: matchedImage,
isManual: false
});

if (matchedImage) {
log(`✅ ${cellAddress}: ${extractedName} → ${matchedImage}`, 'success');
} else if (extractedName) {
log(`⚠️ ${cellAddress}: ${extractedName} → подпись не найдена`, 'warning');
}

} catch (error) {
log(`❌ Ошибка в ячейке ${cellAddress}: ${error.message}`, 'error');
}
}

updateCellsGrid();
updateStatus();

document.getElementById('processBtn').disabled = false;
document.getElementById('reportBtn').disabled = false;
document.getElementById('statusBar').style.display = 'grid';
}

// Обновление сетки ячеек
function updateCellsGrid() {
const container = document.getElementById('cellsGridContainer');

if (cellData.size === 0) {
container.innerHTML = '<div style="text-align: center; padding: 60px; color: #999;">🔍 Нет данных для отображения</div>';
return;
}

let html = '<div class="cells-grid">';

cellData.forEach((data, address) => {
const imageData = data.selectedImage ? imageMap.get(data.selectedImage) : null;
const matchedClass = data.selectedImage ? 'matched' : '';
const badgeClass = data.isManual ? 'badge-manual' : 'badge-auto';
const badgeText = data.isManual ? '👆 Ручной' : '🤖 Авто';

html += `
<div class="cell-card ${matchedClass}">
<div class="cell-header">
<span class="cell-address">${address}</span>
${data.selectedImage ? `<span class="cell-badge ${badgeClass}">${badgeText}</span>` : ''}
</div>

<div class="cell-content">${data.rawValue}</div>

<div class="employee-name">
👤 ${data.extractedName || 'Не распознано'}
</div>

${imageData ?
`<img class="image-preview" src="${imageData.dataURL}" alt="Подпись">` :
'<div style="color: #999; padding: 20px; text-align: center;">❌ Нет подписи</div>'}

<select class="image-select" onchange="changeImageForCell('${address}', this.value)">
<option value="">-- Выбрать подпись --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>
${imgName} ${imgName === data.autoMatchedImage ? '⭐' : ''}
</option>`
).join('')}
</select>
</div>
`;
});

html += '</div>';
container.innerHTML = html;
}

// Изменение изображения для ячейки
function changeImageForCell(address, imageName) {
const data = cellData.get(address);
if (data) {
data.selectedImage = imageName || null;
data.isManual = imageName ? (imageName !== data.autoMatchedImage) : false;
cellData.set(address, data);

log(`✍️ Для ячейки ${address} выбрано: ${imageName || 'нет'}`, 'info');
updateCellsGrid();
updateStatus();
}
}

// Обновление статуса
function updateStatus() {
let total = cellData.size;
let autoMatched = 0;
let manualSelected = 0;
let readyToInsert = 0;

cellData.forEach(data => {
if (data.autoMatchedImage) autoMatched++;
if (data.isManual) manualSelected++;
if (data.selectedImage) readyToInsert++;
});

document.getElementById('totalCells').textContent = total;
document.getElementById('autoMatched').textContent = autoMatched;
document.getElementById('manualSelected').textContent = manualSelected;
document.getElementById('readyToInsert').textContent = readyToInsert;
}

// Добавление новой ячейки
function addNewCell() {
const input = document.getElementById('newCellInput');
const newCell = input.value.toUpperCase().trim();

// Валидация формата ячейки
const cellPattern = /^[A-Z]+[0-9]+$/;

if (!cellPattern.test(newCell)) {
log('⚠️ Неверный формат ячейки. Используйте например: G42', 'warning');
return;
}

if (!targetCells.includes(newCell)) {
targetCells.push(newCell);
log(`➕ Добавлена ячейка: ${newCell}`, 'success');
input.value = '';

if (currentWorkbook) {
scanExcelFile();
}
} else {
log('⚠️ Такая ячейка уже есть в списке', 'warning');
}
}

// Вставка подписей в Excel
async function insertSignatures() {
if (!currentWorkbook) {
log('❌ Нет загруженного Excel файла', 'error');
return;
}

const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');

progressBar.style.display = 'block';
log('🔄 Начинаю вставку подписей...', 'info');

try {
// Создаем копию workbook
const newWorkbook = new ExcelJS.Workbook();
await newWorkbook.xlsx.load(await currentWorkbook.xlsx.writeBuffer());

const sheet = newWorkbook.getWorksheet('Титул');
if (!sheet) {
throw new Error('Лист "Титул" не найден');
}

let inserted = 0;
const total = cellData.size;

// Вставляем изображения
for (const [address, data] of cellData) {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);

if (imageData && imageData.buffer) {
try {
// Добавляем изображение в workbook
const imageId = newWorkbook.addImage({
buffer: imageData.buffer,
extension: imageData.extension || 'png',
});

// Вставляем изображение в ячейку
sheet.addImage(imageId, {
tl: { col: columnToIndex(data.col), row: data.row - 1 },
br: { col: columnToIndex(data.col) + 1, row: data.row },
editAs: 'oneCell'
});

inserted++;
progressFill.style.width = `${(inserted / total) * 100}%`;

log(`📸 Вставлена подпись для ячейки ${address}`, 'success');

} catch (imgError) {
log(`⚠️ Ошибка вставки в ${address}: ${imgError.message}`, 'warning');
}
}
}
}

// Сохраняем файл
log('💾 Сохранение файла...', 'info');

const buffer = await newWorkbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});

const fileName = currentFileName.replace('.xlsx', '_signed.xlsx');
saveAs(blob, fileName);

log(`✅ Файл успешно сохранен: ${fileName}`, 'success');
log(`📊 Вставлено подписей: ${inserted} из ${total}`, 'success');

// Создаем отчет
createReport();

} catch (error) {
log(`❌ Ошибка при вставке: ${error.message}`, 'error');
console.error(error);
}

progressBar.style.display = 'none';
}

// Преобразование букв колонки в индекс
function columnToIndex(column) {
let index = 0;
for (let i = 0; i < column.length; i++) {
index = index * 26 + column.charCodeAt(i) - 64;
}
return index - 1;
}

// Создание отчета
function createReport() {
let report = 'ОТЧЕТ О ВСТАВЛЕННЫХ ПОДПИСЯХ\n';
report += '='.repeat(60) + '\n\n';
report += `Файл: ${currentFileName}\n`;
report += `Дата: ${new Date().toLocaleString()}\n\n`;

report += 'СООТВЕТСТВИЯ:\n';
report += '-'.repeat(60) + '\n\n';

cellData.forEach((data, address) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
report += `Ячейка: ${address}\n`;
report += `Сотрудник: ${data.extractedName || 'Не распознан'}\n`;
report += `Оригинал: ${data.rawValue}\n`;
report += `Подпись: ${data.selectedImage}\n`;
report += `Файл: ${imageData.file.name}\n`;
report += `Тип: ${data.isManual ? 'Ручной выбор' : 'Автоматически'}\n`;
report += '-'.repeat(60) + '\n\n';
}
});

const blob = new Blob([report], { type: 'text/plain' });
saveAs(blob, `signature_report_${Date.now()}.txt`);

log('📊 Отчет сохранен', 'success');
}

// Скачивание отчета
function downloadReport() {
createReport();
}

// Инициализация
log('🔧 Профессиональная версия запущена', 'success');
log('📚 Используется библиотека ExcelJS', 'info');
</script>
</body>
</html>

Добавлено (2026-02-27, 11:25)
---------------------------------------------
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Подписи сотрудников - Плавающие изображения</title>
<!-- Подключаем профессиональные библиотеки -->
<script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
padding: 20px;
}

.container {
max-width: 1600px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}

.header {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 30px;
text-align: center;
}

.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}

.header p {
opacity: 0.9;
font-size: 1.2em;
}

.main-content {
padding: 30px;
}

.upload-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}

.upload-card {
background: #f8f9fa;
border: 3px dashed #dee2e6;
border-radius: 15px;
padding: 30px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
}

.upload-card:hover {
border-color: #2a5298;
background: #e8f0fe;
}

.upload-card.has-file {
border-color: #28a745;
background: #d4edda;
}

.upload-icon {
font-size: 64px;
margin-bottom: 15px;
}

.upload-title {
font-size: 1.5em;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}

.file-info {
margin-top: 15px;
padding: 15px;
background: white;
border-radius: 10px;
font-size: 0.95em;
color: #666;
word-break: break-all;
max-height: 150px;
overflow-y: auto;
}

.scan-btn {
background: #28a745;
color: white;
border: none;
padding: 12px 30px;
border-radius: 50px;
font-size: 1.1em;
cursor: pointer;
margin-top: 15px;
transition: all 0.3s;
font-weight: 600;
}

.scan-btn:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(40, 167, 69, 0.3);
}

.status-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 30px 0;
}

.status-item {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}

.status-value {
font-size: 2.8em;
font-weight: bold;
margin-bottom: 5px;
}

.status-label {
font-size: 1em;
opacity: 0.9;
}

.sheets-tabs {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}

.sheet-tab {
padding: 10px 25px;
background: #e0e0e0;
border: none;
border-radius: 30px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}

.sheet-tab.active {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
}

.cells-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
margin: 30px 0;
}

.cell-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
transition: all 0.3s;
}

.cell-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}

.cell-card.matched {
border-left: 5px solid #28a745;
}

.cell-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.cell-address {
font-size: 1.5em;
font-weight: bold;
color: #2a5298;
background: #e8f0fe;
padding: 5px 20px;
border-radius: 30px;
}

.cell-sheet {
font-size: 0.9em;
color: #666;
background: #f0f0f0;
padding: 5px 15px;
border-radius: 20px;
}

.cell-badge {
padding: 5px 15px;
border-radius: 30px;
font-size: 0.85em;
font-weight: 600;
}

.badge-auto {
background: #28a745;
color: white;
}

.badge-manual {
background: #ffc107;
color: #333;
}

.cell-content {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin: 15px 0;
font-family: 'Courier New', monospace;
font-size: 0.95em;
border-left: 3px solid #2a5298;
}

.employee-name {
color: #28a745;
font-weight: 600;
margin: 10px 0;
padding: 10px;
background: #d4edda;
border-radius: 8px;
font-size: 1.1em;
}

.image-preview {
max-width: 100%;
max-height: 120px;
margin: 15px 0;
border: 2px solid #dee2e6;
border-radius: 10px;
padding: 5px;
background: white;
object-fit: contain;
}

.image-select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1em;
margin-top: 10px;
cursor: pointer;
transition: all 0.3s;
}

.image-select:hover {
border-color: #2a5298;
}

.action-buttons {
display: flex;
gap: 20px;
justify-content: center;
margin: 40px 0;
flex-wrap: wrap;
}

.btn {
padding: 18px 50px;
border: none;
border-radius: 50px;
font-size: 1.2em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}

.btn-primary {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
}

.btn-success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
}

.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: white;
}

.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}

.log-panel {
background: #1e1e2f;
color: #fff;
padding: 20px;
border-radius: 15px;
font-family: 'Courier New', monospace;
height: 300px;
overflow-y: auto;
font-size: 0.95em;
line-height: 1.6;
margin: 30px 0;
}

.log-entry {
padding: 5px 0;
border-bottom: 1px solid #333;
color: #a0a0ff;
}

.log-entry.success {
color: #98fb98;
}

.log-entry.error {
color: #ff6b6b;
}

.log-entry.warning {
color: #ffd700;
}

.add-cell-panel {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
padding: 25px;
border-radius: 15px;
color: white;
margin: 30px 0;
}

.add-cell-row {
display: flex;
gap: 15px;
margin-top: 15px;
align-items: center;
}

.add-cell-input {
flex: 2;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 1.1em;
}

.add-cell-select {
flex: 1;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 1.1em;
background: white;
}

.add-cell-btn {
background: #ffd700;
color: #333;
border: none;
padding: 15px 40px;
border-radius: 10px;
font-weight: 600;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s;
}

.add-cell-btn:hover {
background: #ffed4a;
transform: translateY(-2px);
}

.progress-bar {
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
display: none;
}

.progress-fill {
height: 100%;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
width: 0%;
transition: width 0.3s;
}

.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
border-top: 1px solid #dee2e6;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📎 Excel Подписи Сотрудников PRO</h1>
<p>Плавающие изображения поверх ячеек | Полное сохранение структуры</p>
</div>

<div class="main-content">
<!-- Загрузка файлов -->
<div class="upload-section">
<div class="upload-card" id="excelDropZone">
<div class="upload-icon">📊</div>
<div class="upload-title">Excel файл</div>
<p style="color: #666;">Нажмите для выбора или перетащите файл</p>
<input type="file" id="excelFile" accept=".xlsx" style="display: none;">
<div id="excelFileName" class="file-info"></div>
</div>

<div class="upload-card" id="imageDropZone">
<div class="upload-icon">🖼️</div>
<div class="upload-title">Папка с подписями</div>
<p style="color: #666;">Выберите папку с PNG изображениями</p>
<input type="file" id="imageFiles" accept="image/*" multiple webkitdirectory style="display: none;">
<button class="scan-btn" onclick="document.getElementById('imageFiles').click()">
📁 Сканировать папку
</button>
<div id="imageFileList" class="file-info"></div>
</div>
</div>

<!-- Статус бар -->
<div class="status-bar" id="statusBar" style="display: none;">
<div class="status-item">
<div class="status-value" id="totalCells">0</div>
<div class="status-label">Всего ячеек</div>
</div>
<div class="status-item">
<div class="status-value" id="autoMatched">0</div>
<div class="status-label">Авто-найдено</div>
</div>
<div class="status-item">
<div class="status-value" id="manualSelected">0</div>
<div class="status-label">Выбрано вручную</div>
</div>
<div class="status-item">
<div class="status-value" id="readyToInsert">0</div>
<div class="status-label">Готово к вставке</div>
</div>
</div>

<!-- Прогресс бар -->
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>

<!-- Панель добавления ячеек -->
<div class="add-cell-panel">
<h3>➕ Добавить ячейку для подписи</h3>
<p>Стандартные ячейки: Титул!D28, Титул!F29, Титул!F31, Титул!F33, Титул!F35, Лист1!N25</p>
<div class="add-cell-row">
<input type="text" id="newCellAddress" class="add-cell-input" placeholder="Адрес ячейки (например: G42)" value="">
<select id="newCellSheet" class="add-cell-select">
<option value="Титул">Титул</option>
<option value="Лист1">Лист1</option>
</select>
<button class="add-cell-btn" onclick="addNewCell()">Добавить</button>
</div>
</div>

<!-- Вкладки листов -->
<div class="sheets-tabs" id="sheetsTabs"></div>

<!-- Сетка ячеек -->
<div id="cellsGridContainer">
<div style="text-align: center; padding: 60px; color: #999; font-size: 1.2em;">
🔍 Загрузите Excel файл для начала работы
</div>
</div>

<!-- Кнопки действий -->
<div class="action-buttons">
<button class="btn btn-primary" onclick="scanExcelFile()" id="scanBtn" disabled>
🔍 Сканировать
</button>
<button class="btn btn-success" onclick="insertSignatures()" id="processBtn" disabled>
💾 Вставить подписи (поверх ячеек)
</button>
<button class="btn btn-warning" onclick="downloadReport()" id="reportBtn" disabled>
📊 Отчет
</button>
</div>

<!-- Логи -->
<div class="log-panel" id="logArea"></div>
</div>

<div class="footer">
⚡ Плавающие изображения | Полное сохранение формул и стилей | Без поворота изображений
</div>
</div>

<script>
// Состояние приложения
let currentWorkbook = null;
let currentFileName = '';
let imageFiles = [];
let imageMap = new Map(); // имя -> {file, dataURL, buffer}
let cellData = new Map(); // sheet!address -> данные
let targetCells = [
{ sheet: 'Титул', address: 'D28' },
{ sheet: 'Титул', address: 'F29' },
{ sheet: 'Титул', address: 'F31' },
{ sheet: 'Титул', address: 'F33' },
{ sheet: 'Титул', address: 'F35' },
{ sheet: 'Лист1', address: 'N25' }
];

// DOM элементы
const logArea = document.getElementById('logArea');

// Логирование
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const time = new Date().toLocaleTimeString();
entry.textContent = `[${time}] ${message}`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
}

// Drag & drop для Excel
const excelDropZone = document.getElementById('excelDropZone');

excelDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#2a5298';
excelDropZone.style.background = '#e8f0fe';
});

excelDropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';
});

excelDropZone.addEventListener('drop', async (e) => {
e.preventDefault();
excelDropZone.style.borderColor = '#dee2e6';
excelDropZone.style.background = '#f8f9fa';

const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.xlsx')) {
await loadExcelFile(file);
} else {
log('❌ Пожалуйста, выберите файл формата .xlsx', 'error');
}
});

excelDropZone.addEventListener('click', () => {
document.getElementById('excelFile').click();
});

// Загрузка Excel через input
document.getElementById('excelFile').addEventListener('change', async (e) => {
if (e.target.files[0]) {
await loadExcelFile(e.target.files[0]);
}
});

// Загрузка изображений
document.getElementById('imageFiles').addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
await loadImageFiles(files);
});

// Загрузка Excel файла с помощью ExcelJS
async function loadExcelFile(file) {
try {
log(`📊 Загрузка файла: ${file.name}`, 'info');

const arrayBuffer = await file.arrayBuffer();
currentWorkbook = new ExcelJS.Workbook();
await currentWorkbook.xlsx.load(arrayBuffer);

currentFileName = file.name;

// Проверяем наличие необходимых листов
const sheets = ['Титул', 'Лист1'];
const missingSheets = [];

for (const sheetName of sheets) {
if (!currentWorkbook.getWorksheet(sheetName)) {
missingSheets.push(sheetName);
}
}

if (missingSheets.length > 0) {
log(`⚠️ Отсутствуют листы: ${missingSheets.join(', ')}`, 'warning');
}

document.getElementById('excelFileName').innerHTML = `
<strong>✅ ${file.name}</strong><br>
Размер: ${(file.size / 1024).toFixed(2)} KB<br>
Листов: ${currentWorkbook.worksheets.length}
`;

excelDropZone.classList.add('has-file');
document.getElementById('scanBtn').disabled = false;

log('✅ Excel файл успешно загружен', 'success');

// Автоматически сканируем
await scanExcelFile();

} catch (error) {
log(`❌ Ошибка загрузки Excel: ${error.message}`, 'error');
console.error(error);
}
}

// Загрузка изображений
async function loadImageFiles(files) {
imageFiles = files.filter(f => f.type.startsWith('image/'));
imageMap.clear();

const list = document.getElementById('imageFileList');
list.innerHTML = `<strong>Найдено файлов: ${imageFiles.length}</strong><br>`;

let loadedCount = 0;

for (const file of imageFiles) {
const name = file.name.replace(/\.[^/.]+$/, "");
list.innerHTML += `<div>🖼️ ${file.name}</div>`;

try {
// Загружаем изображение как DataURL для превью
const dataURL = await readFileAsDataURL(file);

// Загружаем как ArrayBuffer для вставки в Excel
const buffer = await readFileAsArrayBuffer(file);

imageMap.set(name, {
file: file,
dataURL: dataURL,
buffer: buffer,
name: name,
extension: file.name.split('.').pop()
});

loadedCount++;

} catch (error) {
log(`❌ Ошибка загрузки ${file.name}: ${error.message}`, 'error');
}
}

log(`📸 Загружено изображений: ${loadedCount}`, 'success');

if (currentWorkbook) {
await scanExcelFile();
}

document.getElementById('imageDropZone').classList.add('has-file');
}

// Вспомогательные функции для чтения файлов
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}

function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}

// Извлечение имени из ячейки
function extractNameFromCell(cellValue) {
if (!cellValue) return null;

let value = String(cellValue).trim();

// Паттерны для русских ФИО
const patterns = [
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]?\.?)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.)/,
/([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]?\s+[А-ЯЁ][а-яё]?)/,
/([А-ЯЁ]{2,}\s+[А-ЯЁ]\.[А-ЯЁ]\.)/,
/(?:директор|начальник|зам\.|главный|ведущий|специалист|инженер|менеджер|генеральный)\s+([А-ЯЁ][а-яё]+\s+[А-ЯЁ]\.[А-ЯЁ]\.)/i
];

for (let pattern of patterns) {
const match = value.match(pattern);
if (match) {
return match[1] || match[0];
}
}

return value;
}

// Поиск соответствующего изображения
function findMatchingImage(extractedName) {
if (!extractedName || imageMap.size === 0) return null;

const searchName = extractedName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

let bestMatch = null;
let bestScore = 0;

for (const [imgName, imgData] of imageMap) {
const cleanImgName = imgName.toLowerCase()
.replace(/\s+/g, '')
.replace(/\./g, '');

// Точное совпадение
if (cleanImgName === searchName) {
return imgName;
}

// Одно содержит другое
if (cleanImgName.includes(searchName) || searchName.includes(cleanImgName)) {
return imgName;
}

// Подсчет совпадений
let score = 0;
for (let i = 0; i < Math.min(searchName.length, cleanImgName.length); i++) {
if (searchName[i] === cleanImgName[i]) score++;
}

if (score > bestScore && score > 3) {
bestScore = score;
bestMatch = imgName;
}
}

return bestMatch;
}

// Сканирование Excel файла
async function scanExcelFile() {
if (!currentWorkbook) {
log('❌ Сначала загрузите Excel файл', 'error');
return;
}

cellData.clear();
log('🔍 Сканирование ячеек...', 'info');

for (const cell of targetCells) {
const sheet = currentWorkbook.getWorksheet(cell.sheet);
if (!sheet) {
log(`⚠️ Лист "${cell.sheet}" не найден, пропускаем ${cell.sheet}!${cell.address}`, 'warning');
continue;
}

try {
const col = cell.address.match(/[A-Z]+/)[0];
const row = parseInt(cell.address.match(/\d+/)[0]);

const excelCell = sheet.getCell(`${col}${row}`);
const rawValue = excelCell.value || '(пусто)';
const extractedName = rawValue !== '(пусто)' ? extractNameFromCell(rawValue) : null;

const matchedImage = extractedName ? findMatchingImage(extractedName) : null;

const key = `${cell.sheet}!${cell.address}`;
cellData.set(key, {
sheet: cell.sheet,
address: cell.address,
col: col,
row: row,
rawValue: rawValue,
extractedName: extractedName,
autoMatchedImage: matchedImage,
selectedImage: matchedImage,
isManual: false
});

if (matchedImage) {
log(`✅ ${cell.sheet}!${cell.address}: ${extractedName} → ${matchedImage}`, 'success');
} else if (extractedName) {
log(`⚠️ ${cell.sheet}!${cell.address}: ${extractedName} → подпись не найдена`, 'warning');
}

} catch (error) {
log(`❌ Ошибка в ячейке ${cell.sheet}!${cell.address}: ${error.message}`, 'error');
}
}

updateSheetsTabs();
updateCellsGrid();
updateStatus();

document.getElementById('processBtn').disabled = false;
document.getElementById('reportBtn').disabled = false;
document.getElementById('statusBar').style.display = 'grid';
}

// Обновление вкладок листов
function updateSheetsTabs() {
const tabsContainer = document.getElementById('sheetsTabs');
const sheets = [...new Set(Array.from(cellData.values()).map(d => d.sheet))];

tabsContainer.innerHTML = '';

sheets.forEach(sheet => {
const tab = document.createElement('button');
tab.className = 'sheet-tab';
tab.textContent = sheet;
tab.onclick = () => filterBySheet(sheet);
tabsContainer.appendChild(tab);
});

// Добавляем кнопку "Все"
const allTab = document.createElement('button');
allTab.className = 'sheet-tab active';
allTab.textContent = 'Все листы';
allTab.onclick = () => filterBySheet('all');
tabsContainer.prepend(allTab);
}

// Фильтрация по листу
function filterBySheet(sheetName) {
document.querySelectorAll('.sheet-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');

updateCellsGrid(sheetName);
}

// Обновление сетки ячеек
function updateCellsGrid(filterSheet = 'all') {
const container = document.getElementById('cellsGridContainer');

if (cellData.size === 0) {
container.innerHTML = '<div style="text-align: center; padding: 60px; color: #999;">🔍 Нет данных для отображения</div>';
return;
}

let html = '<div class="cells-grid">';

Array.from(cellData.entries()).forEach(([key, data]) => {
if (filterSheet !== 'all' && data.sheet !== filterSheet) return;

const imageData = data.selectedImage ? imageMap.get(data.selectedImage) : null;
const matchedClass = data.selectedImage ? 'matched' : '';
const badgeClass = data.isManual ? 'badge-manual' : 'badge-auto';
const badgeText = data.isManual ? '👆 Ручной' : '🤖 Авто';

html += `
<div class="cell-card ${matchedClass}">
<div class="cell-header">
<span class="cell-address">${data.address}</span>
<span class="cell-sheet">${data.sheet}</span>
</div>

<div class="cell-header" style="justify-content: flex-end; margin-top: -10px;">
${data.selectedImage ? `<span class="cell-badge ${badgeClass}">${badgeText}</span>` : ''}
</div>

<div class="cell-content">${data.rawValue}</div>

<div class="employee-name">
👤 ${data.extractedName || 'Не распознано'}
</div>

${imageData ?
`<img class="image-preview" src="${imageData.dataURL}" alt="Подпись">` :
'<div style="color: #999; padding: 20px; text-align: center;">❌ Нет подписи</div>'}

<select class="image-select" onchange="changeImageForCell('${key}', this.value)">
<option value="">-- Выбрать подпись --</option>
${Array.from(imageMap.keys()).map(imgName =>
`<option value="${imgName}" ${data.selectedImage === imgName ? 'selected' : ''}>
${imgName} ${imgName === data.autoMatchedImage ? '⭐' : ''}
</option>`
).join('')}
</select>
</div>
`;
});

html += '</div>';
container.innerHTML = html;
}

// Изменение изображения для ячейки
function changeImageForCell(key, imageName) {
const data = cellData.get(key);
if (data) {
data.selectedImage = imageName || null;
data.isManual = imageName ? (imageName !== data.autoMatchedImage) : false;
cellData.set(key, data);

log(`✍️ Для ячейки ${key} выбрано: ${imageName || 'нет'}`, 'info');
updateCellsGrid();
updateStatus();
}
}

// Обновление статуса
function updateStatus() {
let total = cellData.size;
let autoMatched = 0;
let manualSelected = 0;
let readyToInsert = 0;

cellData.forEach(data => {
if (data.autoMatchedImage) autoMatched++;
if (data.isManual) manualSelected++;
if (data.selectedImage) readyToInsert++;
});

document.getElementById('totalCells').textContent = total;
document.getElementById('autoMatched').textContent = autoMatched;
document.getElementById('manualSelected').textContent = manualSelected;
document.getElementById('readyToInsert').textContent = readyToInsert;
}

// Добавление новой ячейки
function addNewCell() {
const address = document.getElementById('newCellAddress').value.toUpperCase().trim();
const sheet = document.getElementById('newCellSheet').value;

// Валидация формата ячейки
const cellPattern = /^[A-Z]+[0-9]+$/;

if (!cellPattern.test(address)) {
log('⚠️ Неверный формат ячейки. Используйте например: G42', 'warning');
return;
}

const exists = targetCells.some(c => c.sheet === sheet && c.address === address);

if (!exists) {
targetCells.push({ sheet, address });
log(`➕ Добавлена ячейка: ${sheet}!${address}`, 'success');
document.getElementById('newCellAddress').value = '';

if (currentWorkbook) {
scanExcelFile();
}
} else {
log('⚠️ Такая ячейка уже есть в списке', 'warning');
}
}

// Вставка подписей в Excel (как плавающие объекты)
async function insertSignatures() {
if (!currentWorkbook) {
log('❌ Нет загруженного Excel файла', 'error');
return;
}

const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');

progressBar.style.display = 'block';
log('🔄 Начинаю вставку подписей как плавающих объектов...', 'info');

try {
// Создаем копию workbook
const newWorkbook = new ExcelJS.Workbook();
await newWorkbook.xlsx.load(await currentWorkbook.xlsx.writeBuffer());

let inserted = 0;
const total = cellData.size;

// Вставляем изображения
for (const [key, data] of cellData) {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);

if (imageData && imageData.buffer) {
try {
const sheet = newWorkbook.getWorksheet(data.sheet);
if (!sheet) {
log(`⚠️ Лист ${data.sheet} не найден`, 'warning');
continue;
}

// Добавляем изображение в workbook
const imageId = newWorkbook.addImage({
buffer: imageData.buffer,
extension: imageData.extension || 'png',
});

// Получаем размеры ячейки для позиционирования
const col = data.col;
const row = data.row;

// Получаем координаты ячейки в пикселях
const colOffset = columnToIndex(col);
const rowOffset = row - 1;

// Вставляем изображение как плавающий объект поверх ячейки
// Используем absoluteAnchor для точного позиционирования
sheet.addImage(imageId, {
tl: { col: colOffset, row: rowOffset },
br: { col: colOffset + 1.2, row: rowOffset + 0.8 }, // Немного больше ячейки
editAs: 'absolute' // Плавающий объект, не привязанный к ячейке
});

inserted++;
progressFill.style.width = `${(inserted / total) * 100}%`;

log(`📸 Вставлена подпись для ${data.sheet}!${data.address}`, 'success');

} catch (imgError) {
log(`⚠️ Ошибка вставки в ${key}: ${imgError.message}`, 'warning');
}
}
}
}

// Сохраняем файл
log('💾 Сохранение файла...', 'info');

const buffer = await newWorkbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});

const fileName = currentFileName.replace('.xlsx', '_signed.xlsx');
saveAs(blob, fileName);

log(`✅ Файл успешно сохранен: ${fileName}`, 'success');
log(`📊 Вставлено подписей: ${inserted} из ${total}`, 'success');

// Создаем отчет
createReport();

} catch (error) {
log(`❌ Ошибка при вставке: ${error.message}`, 'error');
console.error(error);
}

progressBar.style.display = 'none';
}

// Преобразование букв колонки в индекс
function columnToIndex(column) {
let index = 0;
for (let i = 0; i < column.length; i++) {
index = index * 26 + column.charCodeAt(i) - 64;
}
return index;
}

// Создание отчета
function createReport() {
let report = 'ОТЧЕТ О ВСТАВЛЕННЫХ ПОДПИСЯХ\n';
report += '='.repeat(60) + '\n\n';
report += `Файл: ${currentFileName}\n`;
report += `Дата: ${new Date().toLocaleString()}\n\n`;

report += 'СООТВЕТСТВИЯ:\n';
report += '-'.repeat(60) + '\n\n';

cellData.forEach((data, key) => {
if (data.selectedImage) {
const imageData = imageMap.get(data.selectedImage);
report += `Лист: ${data.sheet}\n`;
report += `Ячейка: ${data.address}\n`;
report += `Сотрудник: ${data.extractedName || 'Не распознан'}\n`;
report += `Оригинал: ${data.rawValue}\n`;
report += `Подпись: ${data.selectedImage}\n`;
report += `Файл: ${imageData.file.name}\n`;
report += `Тип: ${data.isManual ? 'Ручной выбор' : 'Автоматически'}\n`;
report += '-'.repeat(60) + '\n\n';
}
});

const blob = new Blob([report], { type: 'text/plain' });
saveAs(blob, `signature_report_${Date.now()}.txt`);

log('📊 Отчет сохранен', 'success');
}

// Скачивание отчета
function downloadReport() {
createReport();
}

// Инициализация
log('🔧 Профессиональная версия с плавающими изображениями запущена', 'success');
log('📚 Поддерживаются листы: Титул, Лист1', 'info');
log('🎯 Ячейка Лист1!N25 добавлена в список', 'info');
</script>
</body>
</html>

Ттт
Прикрепления:
tttttt.noext (47.8 Kb)
Ппмии
Прикрепления:
6109155.noext (52.1 Kb)
Ррр
Прикрепления:
4454015.noext (48.0 Kb)
Оор
Прикрепления:
8896197.noext (51.0 Kb)
Ооо
Прикрепления:
3019816.noext (51.6 Kb)
Ррр
Прикрепления:
6796900.noext (52.6 Kb)
Оррр
Прикрепления:
ddddddd.noext (52.6 Kb)
Ррпрр
Прикрепления:
4581919.noext (45.8 Kb)
Проп
Прикрепления:
7464656.noext (44.8 Kb)
Ggfg
Прикрепления:
5163367.noext (44.3 Kb)
Рори
Прикрепления:
9383397.noext (47.8 Kb)
Ррмм
Прикрепления:
6659542.noext (46.2 Kb)
Ромртт
Прикрепления:
2877590.noext (38.1 Kb)
Рорр
Прикрепления:
4101592.noext (45.0 Kb)
  • Страница 6 из 8
  • «
  • 1
  • 2
  • 4
  • 5
  • 6
  • 7
  • 8
  • »
Поиск:
Новый ответ
Имя:
Текст сообщения: