v87: +Word/PDF export, +hse.sk.kz integration (server.py)
This commit is contained in:
parent
8584419acc
commit
507281dfee
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
90
index.html
90
index.html
@ -83,7 +83,9 @@ tr:hover{background:#FAFBFC}
|
||||
<a class=" active" id="snav_events" onclick="switchTab('events')"><span>Мероприятия</span></a>
|
||||
<a class="" id="snav_analytics" onclick="switchTab('analytics')"><span>Аналитика</span></a>
|
||||
<a class="" id="snav_reports" onclick="switchTab('reports')"><span>Отчётность</span></a>
|
||||
<a class="" id="snav_reports" onclick="switchTab('reports')"><span>Отчётность</span></a>
|
||||
<a class="" id="snav_ai" onclick="switchTab('ai')"><span>ИИ-помощник</span></a>
|
||||
<a class="" id="snav_hse" onclick="switchTab('hse')"><span>HSE.sk.kz</span></a>
|
||||
<div class="logout"><button class="btn btn-sm btn-r" style="width:100%" onclick="doLogout()">Выйти</button></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
@ -137,9 +139,29 @@ tr:hover{background:#FAFBFC}
|
||||
</select>
|
||||
<button class="btn btn-sm btn-g" onclick="dlCSV()">CSV</button>
|
||||
<button class="btn btn-sm" onclick="dlHTML()">HTML</button>
|
||||
<button class="btn btn-sm btn-o" onclick="dlWord()">Word</button>
|
||||
<button class="btn btn-sm btn-r" onclick="dlPdf()">PDF</button>
|
||||
</div>
|
||||
<div class="card" id="rp_preview"></div>
|
||||
</div>
|
||||
<div id="tab_hse" style="display:none">
|
||||
<div class="card"><h3>Интеграция с HSE.sk.kz</h3>
|
||||
<p style="font-size:13px;color:#64748B;margin-bottom:16px">Направление подписанного сводного отчёта по месяцам в систему hse.sk.kz</p>
|
||||
<div style="margin-bottom:12px"><strong>Месяц:</strong>
|
||||
<input type="month" id="hse_month" style="padding:6px 10px;border:1px solid #E2E8F0;border-radius:6px;margin-left:8px">
|
||||
</div>
|
||||
<div style="margin-bottom:12px"><strong>Формат:</strong>
|
||||
<select id="hse_fmt" style="padding:6px 10px;border:1px solid #E2E8F0;border-radius:6px;margin-left:8px">
|
||||
<option value="word">Word (.docx)</option>
|
||||
<option value="pdf">PDF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom:12px"><strong>API Ключ:</strong>
|
||||
<input id="hse_key" type="password" placeholder="Ключ API HSE.sk.kz" style="padding:6px 10px;border:1px solid #E2E8F0;border-radius:6px;margin-left:8px;width:300px">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-g" onclick="hseSend()" id="hse_btn">Отправить отчёт в HSE.sk.kz</button>
|
||||
<div id="hse_result" style="margin-top:12px;font-size:13px"></div>
|
||||
</div></div>
|
||||
<div id="tab_ai" style="display:none">
|
||||
<div class="card">
|
||||
<div class="chat-q">
|
||||
@ -255,8 +277,8 @@ function saveEv(){
|
||||
|
||||
function switchTab(t){
|
||||
tab=t;
|
||||
var tabs=["events","analytics","reports","ai"];
|
||||
var tn={events:"Мероприятия",analytics:"Аналитика",reports:"Отчётность",ai:"ИИ-помощник"};
|
||||
var tabs=["events","analytics","reports","ai","hse"];
|
||||
var tn={events:"Мероприятия",analytics:"Аналитика",reports:"Отчётность",ai:"ИИ-помощник",hse:"HSE.sk.kz"};
|
||||
for(var i=0;i<tabs.length;i++){
|
||||
var el=document.getElementById("tab_"+tabs[i]);
|
||||
if(el)el.style.display="none";
|
||||
@ -270,6 +292,7 @@ function switchTab(t){
|
||||
if(t==="analytics")renderAnalytics();
|
||||
if(t==="reports")renderReports();
|
||||
if(t==="ai")renderAI()
|
||||
if(t==="hse"){var hm=document.getElementById("hse_month");if(hm&&!hm.value)hm.value=new Date().toISOString().slice(0,7)}
|
||||
}
|
||||
|
||||
function daysRem(due){
|
||||
@ -532,17 +555,58 @@ function saveBackup(){
|
||||
a.download="backup_"+new Date().toISOString().slice(0,10)+".json";
|
||||
a.click()
|
||||
}
|
||||
function loadBackup(inp){
|
||||
if(!inp.files||!inp.files[0])return;
|
||||
var fr=new FileReader();
|
||||
fr.onload=function(){
|
||||
try{
|
||||
var d=JSON.parse(fr.result);
|
||||
if(!d||!d.length){alert("\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");return}
|
||||
var out=[];
|
||||
for(var i=0;i<d.length;i++){
|
||||
out.push({id:d[i].id,s:d[i].s||"wait",p:d[i].p||0,done:d[i].done||"\u2014",h:d[i].h||[]})
|
||||
}
|
||||
function dlWord(){
|
||||
var fl=getFilteredEvs();
|
||||
var month=parseInt(document.getElementById("rp_month").value,10)+1;
|
||||
var year=document.getElementById("rp_year").value;
|
||||
var hh="<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'><head><meta charset='utf-8'><title>Отчёт План ПБ "+month+"."+year+"</title><style>@page{size:A4;margin:20mm}body{font:12pt 'Times New Roman'}h2{font-size:16pt;text-align:center}table{border-collapse:collapse;width:100%}th,td{border:1px solid #000;padding:4px 8px;font-size:11pt}th{background:#ddd}</style></head><body><h2>План производственной безопасности</h2><p style='text-align:center'>AO «Казахтелеком» за "+month+"."+year+"</p><br><table><tr><th>N</th><th>Мероприятие</th><th>Филиал</th><th>Срок</th><th>Статус</th><th>Прогресс</th><th>Кол-во</th><th>Примечание</th></tr>";
|
||||
for(var i=0;i<fl.length;i++){
|
||||
var e=fl[i];
|
||||
hh+="<tr><td>"+e.id+"</td><td>"+esc(e.t)+"</td><td>"+brs[e.b]+"</td><td>"+e.due+"</td><td>"+stn[e.s]+"</td><td>"+(e.p||0)+"%</td><td>"+(e.q||"")+"</td><td>"+esc(e.n||"")+"</td></tr>"
|
||||
}
|
||||
hh+="</table><p><br><em>Отчёт сформирован: "+new Date().toLocaleDateString("ru-RU")+"</em></p></body></html>";
|
||||
var blob=new Blob([hh],{type:"application/msword"});
|
||||
var a=document.createElement("a");
|
||||
a.href=URL.createObjectURL(blob);
|
||||
a.download="report_pb_"+year+"_"+month+".doc";
|
||||
a.click()
|
||||
}
|
||||
function dlPdf(){
|
||||
var fl=getFilteredEvs();
|
||||
var month=parseInt(document.getElementById("rp_month").value,10)+1;
|
||||
var year=document.getElementById("rp_year").value;
|
||||
var hh="<!DOCTYPE html><html><head><meta charset='utf-8'><title>Отчёт План ПБ "+month+"."+year+"</title><style>body{font:14px Arial;padding:20px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccc;padding:6px 10px;font-size:12px;text-align:left}th{background:#0B1A2E;color:#fff}@media print{body{padding:10mm}table{page-break-inside:auto}tr{page-break-inside:avoid}}</style></head><body><h2>План производственной безопасности</h2><p>AO «Казахтелеком» за "+month+"."+year+"</p><br><table><tr><th>N</th><th>Мероприятие</th><th>Филиал</th><th>Срок</th><th>Статус</th><th>Прогресс</th><th>Кол-во</th><th>Примечание</th></tr>";
|
||||
for(var i=0;i<fl.length;i++){
|
||||
var e=fl[i];
|
||||
hh+="<tr><td>"+e.id+"</td><td>"+esc(e.t)+"</td><td>"+brs[e.b]+"</td><td>"+e.due+"</td><td>"+stn[e.s]+"</td><td>"+(e.p||0)+"%</td><td>"+(e.q||"")+"</td><td>"+esc(e.n||"")+"</td></tr>"
|
||||
}
|
||||
hh+="</table><p><br><em>Отчёт сформирован: "+new Date().toLocaleDateString("ru-RU")+"</em></p><script>window.onload=function(){window.print()}<\/script></body></html>";
|
||||
var w=window.open("","_blank","width=900,height=700");
|
||||
w.document.write(hh);
|
||||
w.document.close()
|
||||
}
|
||||
function hseSend(){
|
||||
var btn=document.getElementById("hse_btn");
|
||||
var result=document.getElementById("hse_result");
|
||||
btn.disabled=true;btn.textContent="Отправка...";result.innerHTML="";
|
||||
var month=document.getElementById("hse_month").value;
|
||||
var fmt=document.getElementById("hse_fmt").value;
|
||||
var apiKey=document.getElementById("hse_key").value;
|
||||
if(!month){result.innerHTML="<span style='color:#EF4444'>Выберите месяц</span>";btn.disabled=false;btn.textContent="Отправить отчёт в HSE.sk.kz";return}
|
||||
if(!apiKey){result.innerHTML="<span style='color:#EF4444'>Введите API ключ</span>";btn.disabled=false;btn.textContent="Отправить отчёт в HSE.sk.kz";return}
|
||||
var fl=getFilteredEvs();
|
||||
var total=fl.length;
|
||||
var done=0;for(var i=0;i<fl.length;i++){if(fl[i].s==="done")done++}
|
||||
var pct=total?Math.round(done/total*100):0;
|
||||
var payload={month:month,events:fl.map(function(e){return{id:e.id,title:e.t,branch:brs[e.b],deadline:e.due,status:e.s,progress:e.p||0,quantity:e.q||"",note:e.n||""}}),summary:{total:total,done:done,pct:pct}};
|
||||
try{
|
||||
fetch("http://localhost:5000/api/hse/send",{method:"POST",headers:{"Content-Type":"application/json","Authorization":"Bearer hse-integration"},body:JSON.stringify({month:month,api_key:apiKey,format:fmt,report:payload})}).then(function(r){return r.json()}).then(function(d){
|
||||
if(d.ok){result.innerHTML="<span style='color:#10B981'>Отчёт за "+month+" отправлен в HSE.sk.kz</span>"}
|
||||
else{result.innerHTML="<span style='color:#EF4444'>Ошибка: "+(d.error||"соединение")+"</span>"}
|
||||
}).catch(function(e){result.innerHTML="<span style='color:#EF4444'>Сервер не запущен. Запустите <code>python3 server.py</code></span>"}).finally(function(){btn.disabled=false;btn.textContent="Отправить отчёт в HSE.sk.kz"})
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
localStorage.setItem("se5",JSON.stringify(out));
|
||||
loadEv();
|
||||
renderEv();
|
||||
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
flask>=3.0
|
||||
flask-cors>=4.0
|
||||
python-docx>=1.0
|
||||
reportlab>=4.0
|
||||
requests>=2.31
|
||||
133
server.py
Normal file
133
server.py
Normal file
@ -0,0 +1,133 @@
|
||||
import json
|
||||
import io
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
import requests as http_requests
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
HSE_API_URL = "https://hse.sk.kz/api/v1"
|
||||
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def make_docx(report):
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
doc = Document()
|
||||
doc.styles["Normal"].font.size = Pt(11)
|
||||
doc.add_heading("План ПБ — Казахтелеком", level=1)
|
||||
s = report.get("summary", {})
|
||||
doc.add_paragraph(
|
||||
f"Дата: {datetime.now().strftime('%d.%m.%Y')} | "
|
||||
f"Всего: {s.get('total', 0)} | "
|
||||
f"Выполнено: {s.get('done', 0)} ({s.get('pct', 0)}%)"
|
||||
)
|
||||
events = report.get("events", [])
|
||||
table = doc.add_table(rows=1, cols=6)
|
||||
table.style = "Light Grid Accent 1"
|
||||
for i, h in enumerate(["N", "Мероприятие", "Филиал", "Срок", "Статус", "%"]):
|
||||
table.rows[0].cells[i].text = h
|
||||
for e in events:
|
||||
row = table.add_row().cells
|
||||
row[0].text = str(e.get("id", ""))
|
||||
row[1].text = str(e.get("title", ""))[:100]
|
||||
row[2].text = str(e.get("branch", ""))
|
||||
row[3].text = str(e.get("deadline", ""))
|
||||
row[4].text = str(e.get("status", ""))
|
||||
row[5].text = str(e.get("progress", 0)) + "%"
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
buf.seek(0)
|
||||
return buf
|
||||
|
||||
|
||||
def make_pdf(report):
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buf, pagesize=A4, rightMargin=20 * mm, leftMargin=20 * mm,
|
||||
topMargin=20 * mm, bottomMargin=20 * mm)
|
||||
styles = getSampleStyleSheet()
|
||||
story = [Paragraph("План ПБ — Казахтелеком", styles["Title"]), Spacer(1, 10)]
|
||||
s = report.get("summary", {})
|
||||
story.append(Paragraph(
|
||||
f"Всего: {s.get('total', 0)} | Выполнено: {s.get('done', 0)} ({s.get('pct', 0)}%)",
|
||||
styles["Normal"]
|
||||
))
|
||||
story.append(Spacer(1, 10))
|
||||
data = [["N", "Мероприятие", "Филиал", "Срок", "Статус", "%"]]
|
||||
for e in report.get("events", []):
|
||||
data.append([
|
||||
str(e.get("id", "")), str(e.get("title", ""))[:80],
|
||||
str(e.get("branch", ""))[:25], str(e.get("deadline", "")),
|
||||
str(e.get("status", "")), str(e.get("progress", 0)) + "%",
|
||||
])
|
||||
table = Table(data, colWidths=[20, 220, 80, 50, 60, 40])
|
||||
table.setStyle(TableStyle([
|
||||
("FONTSIZE", (0, 0), (-1, 0), 9), ("FONTSIZE", (0, 1), (-1, -1), 8),
|
||||
("BACKGROUND", (0, 0), (-1, 0), HexColor("#003366")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#FFFFFF")),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
]))
|
||||
story.append(table)
|
||||
doc.build(story)
|
||||
buf.seek(0)
|
||||
return buf
|
||||
|
||||
|
||||
@app.route("/api/hse/send", methods=["POST"])
|
||||
def hse_send():
|
||||
data = request.get_json()
|
||||
month = data.get("month", "")
|
||||
api_key = data.get("api_key", "")
|
||||
fmt = data.get("format", "word")
|
||||
report = data.get("report", {})
|
||||
endpoint = data.get("endpoint", f"{HSE_API_URL}/documents/upload")
|
||||
|
||||
if not api_key:
|
||||
return jsonify({"ok": False, "error": "API key required"}), 400
|
||||
|
||||
if fmt == "pdf":
|
||||
buf = make_pdf(report)
|
||||
mime = "application/pdf"
|
||||
ext = "pdf"
|
||||
else:
|
||||
buf = make_docx(report)
|
||||
mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
ext = "docx"
|
||||
|
||||
try:
|
||||
files = {"file": (f"hse_report_{month}.{ext}", buf.getvalue(), mime)}
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
payload = {
|
||||
"title": f"Сводный отчет по ПБ за {month}",
|
||||
"description": "Автоматический отчет платформы мониторинга ПБ",
|
||||
"type": "safety_report",
|
||||
"period": month,
|
||||
}
|
||||
r = http_requests.post(endpoint, files=files, data=payload, headers=headers, timeout=30)
|
||||
if r.ok:
|
||||
return jsonify({"ok": True, "hse_response": r.json() if r.text else {"status": r.status_code}})
|
||||
return jsonify({"ok": False, "error": f"HSE API error: {r.status_code}", "detail": r.text[:500]}), 502
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 502
|
||||
|
||||
|
||||
@app.route("/api/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"ok": True, "time": datetime.now().isoformat()})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("HSE Integration Server — http://0.0.0.0:5000")
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
8
start.sh
Executable file
8
start.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
cd "$(dirname "$0")"
|
||||
echo "=== HSE Integration Server ==="
|
||||
echo "Installing..."
|
||||
pip3 install -r requirements.txt --break-system-packages -q 2>/dev/null
|
||||
echo "Starting on http://0.0.0.0:5000"
|
||||
python3 server.py
|
||||
Loading…
Reference in New Issue
Block a user