v2: форма по МАЦ Приложению 2
This commit is contained in:
parent
614ba78c99
commit
fb0ccc1573
569
index.html
569
index.html
@ -3,139 +3,203 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Маркетинговый анализ цен</title>
|
<title>МАЦ — Маркетинговый анализ цен</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; box-sizing: border-box; }
|
* { margin: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; line-height: 1.5; }
|
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; line-height: 1.5; }
|
||||||
header { background: linear-gradient(135deg, #007AFF 0%, #00C7BE 100%); color: white; padding: 32px 24px; text-align: center; }
|
header { background: linear-gradient(135deg, #007AFF 0%, #00C7BE 100%); color: white; padding: 32px 24px; text-align: center; }
|
||||||
header h1 { font-size: clamp(24px, 4vw, 36px); margin-bottom: 8px; }
|
header h1 { font-size: clamp(24px, 4vw, 36px); margin-bottom: 8px; }
|
||||||
header p { opacity: 0.9; font-size: 16px; }
|
header p { opacity: 0.9; font-size: 16px; }
|
||||||
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
||||||
.tabs { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
|
.section-num { display: inline-block; background: #007AFF; color: white; width: 32px; height: 32px; border-radius: 50%; text-align: center; line-height: 32px; font-weight: 700; margin-right: 12px; }
|
||||||
.tab { padding: 10px 20px; border: none; background: white; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 500; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
.section-title { font-size: 22px; margin-bottom: 20px; color: #1d1d1f; display: flex; align-items: center; }
|
||||||
.tab.active { background: #007AFF; color: white; }
|
|
||||||
.section { display: none; }
|
|
||||||
.section.active { display: block; }
|
|
||||||
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||||
.card h2 { font-size: 20px; margin-bottom: 16px; color: #1d1d1f; }
|
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 16px; }
|
||||||
.form-group { margin-bottom: 16px; }
|
.form-group { margin-bottom: 16px; }
|
||||||
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #6e6e73; }
|
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #6e6e73; }
|
||||||
.form-group input, .form-group select { width: 100%; padding: 12px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; }
|
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 12px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; }
|
||||||
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
.form-group textarea { min-height: 100px; resize: vertical; }
|
||||||
.btn { padding: 12px 24px; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
|
.btn { padding: 12px 24px; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
|
||||||
.btn-primary { background: #007AFF; color: white; }
|
.btn-primary { background: #007AFF; color: white; }
|
||||||
.btn-primary:hover { background: #0056b3; }
|
.btn-primary:hover { background: #0056b3; }
|
||||||
.btn-success { background: #34C759; color: white; }
|
.btn-success { background: #34C759; color: white; }
|
||||||
.btn-danger { background: #FF3B30; color: white; padding: 8px 16px; font-size: 13px; }
|
.btn-danger { background: #FF3B30; color: white; padding: 8px 16px; font-size: 13px; }
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
|
table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
|
||||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e5ea; }
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e5ea; }
|
||||||
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #6e6e73; text-transform: uppercase; }
|
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #6e6e73; text-transform: uppercase; }
|
||||||
tr:hover { background: #f9f9fb; }
|
tr:hover { background: #f9f9fb; }
|
||||||
|
.checkbox-group { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; padding: 16px; background: #f9f9fb; border-radius: 10px; }
|
||||||
|
.checkbox-group input[type="checkbox"] { width: 20px; height: 20px; margin-top: 2px; }
|
||||||
|
.checkbox-group label { font-size: 14px; line-height: 1.4; cursor: pointer; }
|
||||||
|
.price-highlight { background: #d4edda; font-weight: 600; }
|
||||||
|
.price-low { background: #fff3cd; }
|
||||||
|
.empty { text-align: center; padding: 40px; color: #86868b; }
|
||||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
.stat { background: white; padding: 20px; border-radius: 12px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
.stat { background: white; padding: 20px; border-radius: 12px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||||
.stat-value { font-size: 32px; font-weight: 700; color: #007AFF; }
|
.stat-value { font-size: 32px; font-weight: 700; color: #007AFF; }
|
||||||
.stat-label { font-size: 13px; color: #6e6e73; margin-top: 4px; }
|
.stat-label { font-size: 13px; color: #6e6e73; margin-top: 4px; }
|
||||||
.price-highlight { background: #d4edda; font-weight: 600; }
|
.conclusion-box { background: #f0f8ff; border: 2px solid #007AFF; border-radius: 12px; padding: 20px; margin-top: 20px; }
|
||||||
.empty { text-align: center; padding: 40px; color: #86868b; }
|
.signature-block { margin-top: 40px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||||
|
.signature-line { border-top: 1px solid #1d1d1f; padding-top: 8px; margin-top: 40px; }
|
||||||
|
@media print {
|
||||||
|
.btn, .no-print { display: none !important; }
|
||||||
|
.card { box-shadow: none; border: 1px solid #ddd; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>📊 Маркетинговый анализ цен</h1>
|
<h1>📊 МАЦ — Маркетинговый анализ цен</h1>
|
||||||
<p>Сравнение цен казахстанских товаропроизводителей</p>
|
<p>Форма по Приложению 2 (Самрук-Қазына)</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="tabs">
|
<div class="no-print" style="text-align: right; margin-bottom: 20px;">
|
||||||
<button class="tab active" onclick="showTab('producers')">Производители</button>
|
<button class="btn btn-primary" onclick="window.print()">🖨 Печать / PDF</button>
|
||||||
<button class="tab" onclick="showTab('products')">Товары и цены</button>
|
<button class="btn btn-success" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
|
||||||
<button class="tab" onclick="showTab('analysis')">Анализ</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Производители -->
|
<!-- Раздел 1: Входные данные -->
|
||||||
<div id="producers" class="section active">
|
<div class="card">
|
||||||
<div class="card">
|
<h2 class="section-title"><span class="section-num">1</span>Входные данные</h2>
|
||||||
<h2>Добавить производителя</h2>
|
<form id="inputDataForm">
|
||||||
<form id="producerForm">
|
<div class="form-row">
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label>Код ЕНС ТРУ</label>
|
||||||
<label>Название компании</label>
|
<input type="text" name="ensCode" placeholder="XX.XX.XX-XXX.X">
|
||||||
<input type="text" name="name" required placeholder="ТОО «Пример»">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Город</label>
|
|
||||||
<input type="text" name="city" required placeholder="Алматы">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Контакты</label>
|
|
||||||
<input type="text" name="contact" placeholder="Телефон / сайт">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Добавить производителя</button>
|
<div class="form-group">
|
||||||
</form>
|
<label>Объем</label>
|
||||||
</div>
|
<input type="text" name="volume" placeholder="с указанием единицы измерения">
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Список производителей</h2>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Название</th><th>Город</th><th>Контакты</th><th>Действия</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="producersTable"></tbody>
|
|
||||||
</table>
|
|
||||||
<div id="producersEmpty" class="empty">Нет производителей</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Товары -->
|
|
||||||
<div id="products" class="section">
|
|
||||||
<div class="card">
|
|
||||||
<h2>Добавить товар и цену</h2>
|
|
||||||
<form id="productForm">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Производитель</label>
|
|
||||||
<select name="producerId" id="producerSelect" required></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Наименование товара</label>
|
|
||||||
<input type="text" name="productName" required placeholder="Например: Мука в/с, 50кг">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Цена (₸)</label>
|
|
||||||
<input type="number" name="price" required placeholder="15000">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Ед. измерения</label>
|
|
||||||
<input type="text" name="unit" placeholder="шт, кг, уп">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Добавить товар</button>
|
</div>
|
||||||
</form>
|
<div class="form-group">
|
||||||
</div>
|
<label>Место поставки</label>
|
||||||
|
<input type="text" name="deliveryPlace" placeholder="адрес доставки">
|
||||||
<div class="card">
|
</div>
|
||||||
<h2>Все товары</h2>
|
<div class="form-group">
|
||||||
<table>
|
<label>Техническая спецификация</label>
|
||||||
<thead>
|
<textarea name="techSpec" placeholder="описание товара/работы/услуги"></textarea>
|
||||||
<tr><th>Товар</th><th>Производитель</th><th>Цена</th><th>Ед.</th><th>Действия</th></tr>
|
</div>
|
||||||
</thead>
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
<tbody id="productsTable"></tbody>
|
</form>
|
||||||
</table>
|
<div id="inputDataDisplay" style="margin-top: 20px; padding: 16px; background: #f9f9fb; border-radius: 10px; display: none;"></div>
|
||||||
<div id="productsEmpty" class="empty">Нет товаров</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Анализ -->
|
<!-- Раздел 2: Список рассылки -->
|
||||||
<div id="analysis" class="section">
|
<div class="card">
|
||||||
|
<h2 class="section-title"><span class="section-num">2</span>Список рассылки и поставщики</h2>
|
||||||
|
<form id="supplierForm" style="margin-bottom: 20px;">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Наименование компании</label>
|
||||||
|
<input type="text" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Резидент/нерезидент</label>
|
||||||
|
<select name="resident" required>
|
||||||
|
<option value="Резидент">Резидент РК</option>
|
||||||
|
<option value="Нерезидент">Нерезидент</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Статус</label>
|
||||||
|
<select name="status" required>
|
||||||
|
<option value="Производитель">Производитель</option>
|
||||||
|
<option value="Дилер">Дилер</option>
|
||||||
|
<option value="Поставщик">Поставщик</option>
|
||||||
|
<option value="ТПФ">ТПФ</option>
|
||||||
|
<option value="Другое">Другое</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Контактные данные (веб-сайт, телефон, адрес, email)</label>
|
||||||
|
<input type="text" name="contacts" placeholder="+7 XXX XXX-XX-XX, example.kz, info@example.kz">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Добавить поставщика</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Компания</th><th>Резидентство</th><th>Статус</th><th>Контакты</th><th class="no-print">Действия</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="suppliersTable"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="suppliersEmpty" class="empty">Нет поставщиков</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Раздел 3: Ценовые предложения -->
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title"><span class="section-num">3</span>Таблица полученных ценовых/коммерческих предложений</h2>
|
||||||
|
<form id="offerForm" style="margin-bottom: 20px;">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Поставщик</label>
|
||||||
|
<select name="supplierId" id="supplierSelect" required></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Номер запроса</label>
|
||||||
|
<input type="text" name="requestNumber" placeholder="№ письма/запроса">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Цена за ед. (тенге, без НДС)</label>
|
||||||
|
<input type="number" name="price" required placeholder="15000">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Дата получения</label>
|
||||||
|
<input type="date" name="date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">Добавить предложение</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Поставщик</th><th>№ запроса</th><th>Цена (тенге)</th><th>Дата</th><th class="no-print">Действия</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="offersTable"></tbody>
|
||||||
|
</table>
|
||||||
|
<div id="offersEmpty" class="empty">Нет предложений</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Раздел 4: Проверка по статье 31 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title"><span class="section-num">4</span>Заключение по проверке (ст. 31 Порядка закупок)</h2>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="check1">
|
||||||
|
<label for="check1">Потенциальный поставщик или его субподрядчик (соисполнитель) либо участник консорциума НЕ состоит в реестре недобросовестных участников государственных закупок и (или) в реестре недобросовестных участников закупок, и (или) в перечне ненадежных потенциальных поставщиков (поставщиков) Фонда, и (или) в списке должников, в отношении которых вступило в законную силу решение суда о признании их банкротами</label>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="check2">
|
||||||
|
<label for="check2">Потенциальный поставщик и (или) привлекаемый им субподрядчик (соисполнитель), и (или) их руководитель, и (или) учредители (акционеры) НЕ включены в перечень организаций и лиц, связанных с финансированием терроризма и экстремизма</label>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<input type="checkbox" id="check3">
|
||||||
|
<label for="check3">Потенциальный поставщик НЕ является юридическим лицом, местом регистрации которого является государство или территория, включенные в перечень государств с льготным налогообложением</label>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary no-print" onclick="saveChecks()">Сохранить проверку</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Раздел 5: Расчеты и анализ -->
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title"><span class="section-num">5</span>Расчеты и анализ</h2>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value" id="statProducers">0</div>
|
<div class="stat-value" id="statSuppliers">0</div>
|
||||||
<div class="stat-label">Производителей</div>
|
<div class="stat-label">Поставщиков</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value" id="statProducts">0</div>
|
<div class="stat-value" id="statOffers">0</div>
|
||||||
<div class="stat-label">Товаров</div>
|
<div class="stat-label">Предложений</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="statMinPrice">0</div>
|
||||||
|
<div class="stat-label">Мин. цена</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="statMaxPrice">0</div>
|
||||||
|
<div class="stat-label">Макс. цена</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-value" id="statAvgPrice">0</div>
|
<div class="stat-value" id="statAvgPrice">0</div>
|
||||||
@ -143,185 +207,250 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<table>
|
||||||
<h2>Сравнение цен по товарам</h2>
|
<thead>
|
||||||
<table>
|
<tr><th>Поставщик</th><th>Цена за ед. (тенге)</th><th>Маркетинговая цена</th><th>Отклонение</th></tr>
|
||||||
<thead>
|
</thead>
|
||||||
<tr><th>Товар</th><th>Мин. цена</th><th>Макс. цена</th><th>Разница</th><th>Лучшее предложение</th></tr>
|
<tbody id="analysisTable"></tbody>
|
||||||
</thead>
|
</table>
|
||||||
<tbody id="analysisTable"></tbody>
|
</div>
|
||||||
</table>
|
|
||||||
<div id="analysisEmpty" class="empty">Нет данных для анализа</div>
|
<!-- Раздел 6: Заключение -->
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title"><span class="section-num">6</span>Заключение с выводами и рекомендациями</h2>
|
||||||
|
<div class="conclusion-box">
|
||||||
|
<p id="conclusionText" style="white-space: pre-line; line-height: 1.8;"></p>
|
||||||
|
<p style="margin-top: 20px; font-style: italic; color: #6e6e73;">На основании цен потенциальных поставщиков/производителей определены маркетинговые цены без учета НДС (КП представлены).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="signature-block">
|
||||||
<h2>Экспорт</h2>
|
<div>
|
||||||
<button class="btn btn-primary" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
|
<p><strong>Инициатор потребности:</strong></p>
|
||||||
|
<div class="signature-line">__________________ / _________________</div>
|
||||||
|
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><strong>Исполнитель:</strong></p>
|
||||||
|
<div class="signature-line">__________________ / _________________</div>
|
||||||
|
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 40px;">
|
||||||
|
<p><strong>Утверждено:</strong></p>
|
||||||
|
<div class="signature-line">__________________ / _________________</div>
|
||||||
|
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||||||
|
<p style="margin-top: 16px;">«___» __________ 2026 г.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let data = { producers: [], products: [] };
|
let data = { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false } };
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const r = await fetch('/api/data');
|
const r = await fetch('/api/data');
|
||||||
data = await r.json();
|
data = await r.json();
|
||||||
renderProducers();
|
if (data.inputData) displayInputData();
|
||||||
renderProducts();
|
renderSuppliers();
|
||||||
|
renderOffers();
|
||||||
renderAnalysis();
|
renderAnalysis();
|
||||||
updateProducerSelect();
|
updateSupplierSelect();
|
||||||
|
document.getElementById('check1').checked = data.checks.c1;
|
||||||
|
document.getElementById('check2').checked = data.checks.c2;
|
||||||
|
document.getElementById('check3').checked = data.checks.c3;
|
||||||
|
generateConclusion();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProducers() {
|
function displayInputData() {
|
||||||
const tbody = document.getElementById('producersTable');
|
const d = data.inputData;
|
||||||
const empty = document.getElementById('producersEmpty');
|
document.getElementById('inputDataDisplay').innerHTML =
|
||||||
if (data.producers.length === 0) {
|
'<strong>Код ЕНС:</strong> ' + (d.ensCode || '—') +
|
||||||
tbody.innerHTML = '';
|
' | <strong>Объем:</strong> ' + (d.volume || '—') +
|
||||||
empty.style.display = 'block';
|
' | <strong>Место поставки:</strong> ' + (d.deliveryPlace || '—') +
|
||||||
return;
|
(d.techSpec ? '<br><strong>Спецификация:</strong> ' + d.techSpec : '');
|
||||||
}
|
document.getElementById('inputDataDisplay').style.display = 'block';
|
||||||
empty.style.display = 'none';
|
|
||||||
tbody.innerHTML = data.producers.map(p => `
|
|
||||||
<tr>
|
|
||||||
<td>${p.name}</td>
|
|
||||||
<td>${p.city}</td>
|
|
||||||
<td>${p.contact || '—'}</td>
|
|
||||||
<td><button class="btn btn-danger" onclick="deleteProducer(${p.id})">Удалить</button></td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProducts() {
|
function renderSuppliers() {
|
||||||
const tbody = document.getElementById('productsTable');
|
const tbody = document.getElementById('suppliersTable');
|
||||||
const empty = document.getElementById('productsEmpty');
|
const empty = document.getElementById('suppliersEmpty');
|
||||||
if (data.products.length === 0) {
|
if (data.suppliers.length === 0) { empty.style.display = 'block'; tbody.innerHTML = ''; return; }
|
||||||
tbody.innerHTML = '';
|
|
||||||
empty.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
empty.style.display = 'none';
|
empty.style.display = 'none';
|
||||||
tbody.innerHTML = data.products.map(p => {
|
tbody.innerHTML = data.suppliers.map(s =>
|
||||||
const producer = data.producers.find(pr => pr.id === p.producerId);
|
'<tr><td>' + s.name + '</td><td>' + s.resident + '</td><td>' + s.status + '</td><td>' + (s.contacts || '—') +
|
||||||
return `
|
'</td><td class="no-print"><button class="btn btn-danger" onclick="deleteSupplier(' + s.id + ')">Удалить</button></td></tr>'
|
||||||
<tr>
|
).join('');
|
||||||
<td>${p.productName}</td>
|
}
|
||||||
<td>${producer ? producer.name : '—'}</td>
|
|
||||||
<td>${Number(p.price).toLocaleString()} ₸</td>
|
function renderOffers() {
|
||||||
<td>${p.unit || '—'}</td>
|
const tbody = document.getElementById('offersTable');
|
||||||
<td><button class="btn btn-danger" onclick="deleteProduct(${p.id})">Удалить</button></td>
|
const empty = document.getElementById('offersEmpty');
|
||||||
</tr>
|
if (data.offers.length === 0) { empty.style.display = 'block'; tbody.innerHTML = ''; return; }
|
||||||
`;
|
empty.style.display = 'none';
|
||||||
|
tbody.innerHTML = data.offers.map(o => {
|
||||||
|
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||||||
|
return '<tr><td>' + (s ? s.name : '—') + '</td><td>' + (o.requestNumber || '—') +
|
||||||
|
'</td><td>' + Number(o.price).toLocaleString() + '</td><td>' + (o.date || '—') +
|
||||||
|
'</td><td class="no-print"><button class="btn btn-danger" onclick="deleteOffer(' + o.id + ')">Удалить</button></td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAnalysis() {
|
function renderAnalysis() {
|
||||||
const byProduct = {};
|
const prices = data.offers.map(o => Number(o.price));
|
||||||
data.products.forEach(p => {
|
const min = prices.length ? Math.min(...prices) : 0;
|
||||||
if (!byProduct[p.productName]) byProduct[p.productName] = [];
|
const max = prices.length ? Math.max(...prices) : 0;
|
||||||
const producer = data.producers.find(pr => pr.id === p.producerId);
|
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||||||
byProduct[p.productName].push({ price: Number(p.price), producer: producer ? producer.name : '—', unit: p.unit });
|
|
||||||
});
|
|
||||||
|
|
||||||
const tbody = document.getElementById('analysisTable');
|
document.getElementById('statSuppliers').textContent = data.suppliers.length;
|
||||||
const empty = document.getElementById('analysisEmpty');
|
document.getElementById('statOffers').textContent = data.offers.length;
|
||||||
|
document.getElementById('statMinPrice').textContent = min.toLocaleString() + ' ₸';
|
||||||
document.getElementById('statProducers').textContent = data.producers.length;
|
document.getElementById('statMaxPrice').textContent = max.toLocaleString() + ' ₸';
|
||||||
document.getElementById('statProducts').textContent = data.products.length;
|
|
||||||
|
|
||||||
const allPrices = data.products.map(p => Number(p.price));
|
|
||||||
const avg = allPrices.length ? Math.round(allPrices.reduce((a,b) => a+b, 0) / allPrices.length) : 0;
|
|
||||||
document.getElementById('statAvgPrice').textContent = avg.toLocaleString() + ' ₸';
|
document.getElementById('statAvgPrice').textContent = avg.toLocaleString() + ' ₸';
|
||||||
|
|
||||||
if (Object.keys(byProduct).length === 0) {
|
const tbody = document.getElementById('analysisTable');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = data.offers.map(o => {
|
||||||
empty.style.display = 'block';
|
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||||||
return;
|
const p = Number(o.price);
|
||||||
}
|
const dev = min > 0 ? ((p - min) / min * 100).toFixed(1) : 0;
|
||||||
empty.style.display = 'none';
|
const isMin = p === min && min > 0;
|
||||||
|
return '<tr' + (isMin ? ' class="price-highlight"' : '') +
|
||||||
tbody.innerHTML = Object.entries(byProduct).map(([name, items]) => {
|
'><td>' + (s ? s.name : '—') + '</td><td>' + p.toLocaleString() +
|
||||||
const prices = items.map(i => i.price);
|
'</td><td>' + (isMin ? min.toLocaleString() + ' ✓' : '—') +
|
||||||
const min = Math.min(...prices);
|
'</td><td>' + (dev > 0 ? '+' + dev + '%' : '0%') + '</td></tr>';
|
||||||
const max = Math.max(...prices);
|
|
||||||
const diff = max - min;
|
|
||||||
const best = items.find(i => i.price === min);
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>${name}</td>
|
|
||||||
<td>${min.toLocaleString()} ₸</td>
|
|
||||||
<td>${max.toLocaleString()} ₸</td>
|
|
||||||
<td>${diff.toLocaleString()} ₸ (${diff > 0 ? Math.round(diff/min*100) : 0}%)</td>
|
|
||||||
<td class="price-highlight">${best.producer} — ${best.price.toLocaleString()} ₸/${best.unit || 'шт'}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
generateConclusion(min, avg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProducerSelect() {
|
function generateConclusion(min, avg) {
|
||||||
const sel = document.getElementById('producerSelect');
|
const minOffer = data.offers.find(o => Number(o.price) === min);
|
||||||
sel.innerHTML = '<option value="">Выберите производителя</option>' +
|
const s = minOffer ? data.suppliers.find(x => x.id === minOffer.supplierId) : null;
|
||||||
data.producers.map(p => `<option value="${p.id}">${p.name} (${p.city})</option>`).join('');
|
const allChecked = data.checks.c1 && data.checks.c2 && data.checks.c3;
|
||||||
|
|
||||||
|
let text = 'На основании проведенного маркетингового анализа цен:\n\n';
|
||||||
|
text += '1. Запрошено коммерческих предложений: ' + data.offers.length + ' шт.\n';
|
||||||
|
text += '2. Количество потенциальных поставщиков: ' + data.suppliers.length + ' ед.\n';
|
||||||
|
text += '3. Диапазон цен: от ' + (min || 0).toLocaleString() + ' до ' + (avg ? Math.max(...data.offers.map(o => Number(o.price))) : 0).toLocaleString() + ' тенге без НДС.\n';
|
||||||
|
text += '4. Средняя рыночная цена: ' + (avg || 0).toLocaleString() + ' тенге без НДС.\n\n';
|
||||||
|
|
||||||
|
if (minOffer && s) {
|
||||||
|
text += 'РЕКОМЕНДАЦИЯ: Наиболее выгодное предложение от ' + s.name + ' (' + s.status.toLowerCase() + ', ' + s.resident.toLowerCase() + ') по цене ' + min.toLocaleString() + ' тенге.\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChecked) {
|
||||||
|
text += 'ПРОВЕРКА ПО СТ. 31: Все пункты подтверждены — поставщик соответствует требованиям Порядка закупок.\n';
|
||||||
|
} else {
|
||||||
|
text += 'ПРОВЕРКА ПО СТ. 31: Требуется проверка отдельных пунктов.\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('conclusionText').textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTab(id) {
|
function updateSupplierSelect() {
|
||||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
document.getElementById('supplierSelect').innerHTML = '<option value="">Выберите поставщика</option>' +
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
data.suppliers.map(s => '<option value="' + s.id + '">' + s.name + ' (' + s.status + ')</option>').join('');
|
||||||
document.getElementById(id).classList.add('active');
|
|
||||||
event.target.classList.add('active');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('producerForm').addEventListener('submit', async (e) => {
|
document.getElementById('inputDataForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const f = e.target;
|
const f = e.target;
|
||||||
await fetch('/api/producers', {
|
await fetch('/api/inputData', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name: f.name.value, city: f.city.value, contact: f.contact.value })
|
body: JSON.stringify({ ensCode: f.ensCode.value, volume: f.volume.value, deliveryPlace: f.deliveryPlace.value, techSpec: f.techSpec.value })
|
||||||
|
});
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('supplierForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const f = e.target;
|
||||||
|
await fetch('/api/suppliers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: f.name.value, resident: f.resident.value, status: f.status.value, contacts: f.contacts.value })
|
||||||
});
|
});
|
||||||
f.reset();
|
f.reset();
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('productForm').addEventListener('submit', async (e) => {
|
document.getElementById('offerForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const f = e.target;
|
const f = e.target;
|
||||||
await fetch('/api/products', {
|
await fetch('/api/offers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ supplierId: Number(f.supplierId.value), requestNumber: f.requestNumber.value, price: Number(f.price.value), date: f.date.value })
|
||||||
|
});
|
||||||
|
f.reset();
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteSupplier(id) {
|
||||||
|
if (!confirm('Удалить поставщика и все его предложения?')) return;
|
||||||
|
await fetch('/api/suppliers/' + id, { method: 'DELETE' });
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOffer(id) {
|
||||||
|
if (!confirm('Удалить предложение?')) return;
|
||||||
|
await fetch('/api/offers/' + id, { method: 'DELETE' });
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChecks() {
|
||||||
|
await fetch('/api/checks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
producerId: Number(f.producerId.value),
|
c1: document.getElementById('check1').checked,
|
||||||
productName: f.productName.value,
|
c2: document.getElementById('check2').checked,
|
||||||
price: Number(f.price.value),
|
c3: document.getElementById('check3').checked
|
||||||
unit: f.unit.value
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
f.reset();
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function deleteProducer(id) {
|
|
||||||
if (!confirm('Удалить производителя и все его товары?')) return;
|
|
||||||
await fetch('/api/producers/' + id, { method: 'DELETE' });
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteProduct(id) {
|
|
||||||
if (!confirm('Удалить товар?')) return;
|
|
||||||
await fetch('/api/products/' + id, { method: 'DELETE' });
|
|
||||||
loadData();
|
loadData();
|
||||||
|
alert('Проверка сохранена');
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportToExcel() {
|
function exportToExcel() {
|
||||||
const head = ["Товар", "Производитель", "Город", "Цена", "Ед.изм."];
|
const head = ["Раздел", "Параметр", "Значение"];
|
||||||
const body = data.products.map(p => {
|
const rows = [];
|
||||||
const pr = data.producers.find(x => x.id === p.producerId);
|
|
||||||
return [p.productName, pr ? pr.name : '', pr ? pr.city : '', p.price, p.unit || ''];
|
if (data.inputData) {
|
||||||
|
rows.push(["1. Входные данные", "Код ЕНС ТРУ", data.inputData.ensCode || '']);
|
||||||
|
rows.push(["1. Входные данные", "Объем", data.inputData.volume || '']);
|
||||||
|
rows.push(["1. Входные данные", "Место поставки", data.inputData.deliveryPlace || '']);
|
||||||
|
rows.push(["1. Входные данные", "Техническая спецификация", data.inputData.techSpec || '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.suppliers.forEach((s, i) => {
|
||||||
|
rows.push(["2. Поставщики", "Компания " + (i+1), s.name]);
|
||||||
|
rows.push(["2. Поставщики", "Резидентство", s.resident]);
|
||||||
|
rows.push(["2. Поставщики", "Статус", s.status]);
|
||||||
|
rows.push(["2. Поставщики", "Контакты", s.contacts || '']);
|
||||||
});
|
});
|
||||||
const csv = '\uFEFF' + [head.join(';'), ...body.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
|
|
||||||
|
data.offers.forEach((o, i) => {
|
||||||
|
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||||||
|
rows.push(["3. Предложения", "Предложение " + (i+1) + " - " + (s ? s.name : ''), o.price + ' тенге']);
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push(["4. Проверка ст.31", "Пункт 1", data.checks.c1 ? 'Подтверждено' : 'Не проверено']);
|
||||||
|
rows.push(["4. Проверка ст.31", "Пункт 2", data.checks.c2 ? 'Подтверждено' : 'Не проверено']);
|
||||||
|
rows.push(["4. Проверка ст.31", "Пункт 3", data.checks.c3 ? 'Подтверждено' : 'Не проверено']);
|
||||||
|
|
||||||
|
const prices = data.offers.map(o => Number(o.price));
|
||||||
|
const min = prices.length ? Math.min(...prices) : 0;
|
||||||
|
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||||||
|
rows.push(["5. Анализ", "Минимальная цена", min.toLocaleString() + ' тенге']);
|
||||||
|
rows.push(["5. Анализ", "Средняя цена", avg.toLocaleString() + ' тенге']);
|
||||||
|
rows.push(["5. Анализ", "Количество предложений", data.offers.length]);
|
||||||
|
|
||||||
|
const csv = '\uFEFF' + [head.join(';'), ...rows.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
|
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
|
||||||
a.download = 'analiz_cen.csv';
|
a.download = 'MAC_' + (data.inputData?.ensCode || 'otchet') + '.csv';
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "price-analysis",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "price-analysis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
server.js
53
server.js
@ -6,7 +6,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
const DATA = path.join(__dirname, "data.json");
|
const DATA = path.join(__dirname, "data.json");
|
||||||
|
|
||||||
function readData() {
|
function readData() {
|
||||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { producers: [], products: [] }; }
|
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false } }; }
|
||||||
}
|
}
|
||||||
function writeData(data) {
|
function writeData(data) {
|
||||||
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||||||
@ -20,49 +20,66 @@ function body(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.createServer(async (req, res) => {
|
http.createServer(async (req, res) => {
|
||||||
// API: получить все данные
|
|
||||||
if (req.method === "GET" && req.url === "/api/data") {
|
if (req.method === "GET" && req.url === "/api/data") {
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify(readData()));
|
return res.end(JSON.stringify(readData()));
|
||||||
}
|
}
|
||||||
// API: добавить производителя
|
|
||||||
if (req.method === "POST" && req.url === "/api/producers") {
|
if (req.method === "POST" && req.url === "/api/inputData") {
|
||||||
const p = await body(req);
|
const d = await body(req);
|
||||||
const data = readData();
|
const data = readData();
|
||||||
data.producers.push({ id: Date.now(), ...p });
|
data.inputData = d;
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
}
|
}
|
||||||
// API: добавить товар с ценой
|
|
||||||
if (req.method === "POST" && req.url === "/api/products") {
|
if (req.method === "POST" && req.url === "/api/suppliers") {
|
||||||
const prod = await body(req);
|
const s = await body(req);
|
||||||
const data = readData();
|
const data = readData();
|
||||||
data.products.push({ id: Date.now(), ...prod });
|
data.suppliers.push({ id: Date.now(), ...s });
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
}
|
}
|
||||||
// API: удалить производителя
|
|
||||||
if (req.method === "DELETE" && req.url.startsWith("/api/producers/")) {
|
if (req.method === "POST" && req.url === "/api/offers") {
|
||||||
|
const o = await body(req);
|
||||||
|
const data = readData();
|
||||||
|
data.offers.push({ id: Date.now(), ...o });
|
||||||
|
writeData(data);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && req.url === "/api/checks") {
|
||||||
|
const c = await body(req);
|
||||||
|
const data = readData();
|
||||||
|
data.checks = c;
|
||||||
|
writeData(data);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "DELETE" && req.url.startsWith("/api/suppliers/")) {
|
||||||
const id = parseInt(req.url.split("/")[3]);
|
const id = parseInt(req.url.split("/")[3]);
|
||||||
const data = readData();
|
const data = readData();
|
||||||
data.producers = data.producers.filter(p => p.id !== id);
|
data.suppliers = data.suppliers.filter(s => s.id !== id);
|
||||||
data.products = data.products.filter(pr => pr.producerId !== id);
|
data.offers = data.offers.filter(o => o.supplierId !== id);
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
}
|
}
|
||||||
// API: удалить товар
|
|
||||||
if (req.method === "DELETE" && req.url.startsWith("/api/products/")) {
|
if (req.method === "DELETE" && req.url.startsWith("/api/offers/")) {
|
||||||
const id = parseInt(req.url.split("/")[3]);
|
const id = parseInt(req.url.split("/")[3]);
|
||||||
const data = readData();
|
const data = readData();
|
||||||
data.products = data.products.filter(p => p.id !== id);
|
data.offers = data.offers.filter(o => o.id !== id);
|
||||||
writeData(data);
|
writeData(data);
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
return res.end(JSON.stringify({ ok: true }));
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
}
|
}
|
||||||
// Статика
|
|
||||||
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
||||||
const full = path.join(__dirname, file);
|
const full = path.join(__dirname, file);
|
||||||
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||||||
|
|||||||
BIN
МАЦ_приложение 2.docx
Normal file
BIN
МАЦ_приложение 2.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user