webtool to generate personalized documents from template and a person registry (custom mailMerge)
This commit is contained in:
parent
5b04942823
commit
5a23f6e62b
BIN
generate-ballots/ballot_template_test.docx
Normal file
BIN
generate-ballots/ballot_template_test.docx
Normal file
Binary file not shown.
167
generate-ballots/create-ballots.js
Normal file
167
generate-ballots/create-ballots.js
Normal file
@ -0,0 +1,167 @@
|
||||
import {
|
||||
TextRun,
|
||||
patchDocument,
|
||||
PatchType,
|
||||
} from "https://esm.sh/docx@9.5.1";
|
||||
|
||||
|
||||
function log(message)
|
||||
{
|
||||
const output_text = document.getElementById("output_text");
|
||||
output_text.value = output_text.value + "\n" + message;
|
||||
}
|
||||
|
||||
function progressStart()
|
||||
{
|
||||
const progressbar = document.getElementById("progressbar");
|
||||
progressbar.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function progressDone()
|
||||
{
|
||||
const progressbar = document.getElementById("progressbar");
|
||||
progressbar.classList.add("hidden");
|
||||
}
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const tmpLink = document.createElement("a");
|
||||
tmpLink.style.display = "none";
|
||||
tmpLink.href = url;
|
||||
tmpLink.download = filename;
|
||||
document.body.appendChild(tmpLink);
|
||||
tmpLink.click();
|
||||
document.body.removeChild(tmpLink);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function sanitizeRecord(record)
|
||||
{
|
||||
const noData="нет данных";
|
||||
for (var i = 0; i < record.length; ++i)
|
||||
{
|
||||
if ((""+record[i]).trim().toLowerCase() == noData)
|
||||
{
|
||||
record[i] = "";
|
||||
}
|
||||
else if(!record[i])
|
||||
{
|
||||
record[i] = "";
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
// Помещение (квартира) № {{APT_NUM}}
|
||||
// СНИЛС {{SNILS}}
|
||||
// Ф.И.О. собственника помещения {{FIO}}
|
||||
// Документ, подтверждающий право собственности: {{DOCUMENT_NUM}}
|
||||
// Дата документа: {{DOCUMENT_DATE}}
|
||||
// Общая площадь квартиры: {{APT_AREA}}
|
||||
// Размер доли в праве собственности: {{SHARE}}
|
||||
async function populateBallot(template, record){
|
||||
try {
|
||||
record = sanitizeRecord(record);
|
||||
const ballot = await patchDocument({
|
||||
data: template,
|
||||
outputType: "arraybuffer",
|
||||
patches: {
|
||||
APT_NUM: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[2])],
|
||||
},
|
||||
FIO: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[3])],
|
||||
},
|
||||
APT_AREA: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[5])],
|
||||
},
|
||||
SHARE: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[6])],
|
||||
},
|
||||
DOCUMENT_NUM: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[8])],
|
||||
},
|
||||
DOCUMENT_DATE: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(record[9] != "" ? ("от "+record[9]) : "")],
|
||||
},
|
||||
SNILS: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [ new TextRun(""+record[10])],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return ballot;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function generate()
|
||||
{
|
||||
progressStart();
|
||||
let registry_file = document.getElementById("registry_file").files[0];
|
||||
let template_file = await document.getElementById("template_file").files[0].arrayBuffer();
|
||||
|
||||
var dataset = await CSV.fetch({
|
||||
file: registry_file,
|
||||
delimiter: ";"
|
||||
}
|
||||
);
|
||||
log("Реестр загружен.");
|
||||
console.log(dataset);
|
||||
|
||||
var zip = new JSZip();
|
||||
var ballots = {};
|
||||
|
||||
for (const r of dataset.records)
|
||||
{
|
||||
const name = "ballot_"+ r[2];
|
||||
let filename = name + ".docx";
|
||||
let discriminator = 1;
|
||||
while(ballots[filename])
|
||||
{
|
||||
discriminator++;
|
||||
filename = name + "_" + discriminator + ".docx";
|
||||
}
|
||||
|
||||
ballots[filename] = (async () =>
|
||||
{
|
||||
var file = await populateBallot(template_file, r);
|
||||
zip.file(filename, file);
|
||||
|
||||
})();
|
||||
}
|
||||
|
||||
for (const b in ballots) await ballots[b];
|
||||
|
||||
var blob = await zip.generateAsync({type:"blob"});
|
||||
downloadBlob(blob, "ballots.zip");
|
||||
log("Готово!");
|
||||
|
||||
progressDone();
|
||||
}
|
||||
|
||||
async function asyncMain() {
|
||||
console.debug("in main");
|
||||
let generateButton = document.getElementById("create_ballots_btn");
|
||||
generateButton.onclick = generate;
|
||||
}
|
||||
|
||||
if (document.readyState != "loading")
|
||||
{
|
||||
asyncMain();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.onload = asyncMain;
|
||||
}
|
||||
367
generate-ballots/dist/csv.js
vendored
Normal file
367
generate-ballots/dist/csv.js
vendored
Normal file
@ -0,0 +1,367 @@
|
||||
/* global jQuery, _ */
|
||||
var CSV = {};
|
||||
|
||||
// Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file)
|
||||
(function(my) {
|
||||
"use strict";
|
||||
my.__type__ = "csv";
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred =
|
||||
(typeof jQuery !== "undefined" && jQuery.Deferred) ||
|
||||
(typeof _ !== "undefined" && _.Deferred) ||
|
||||
function() {
|
||||
var resolve, reject;
|
||||
var promise = new Promise(function(res, rej) {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
promise: function() {
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
my.fetch = function(dataset) {
|
||||
var dfd = new Deferred();
|
||||
if (dataset.file) {
|
||||
var reader = new FileReader();
|
||||
var encoding = dataset.encoding || "UTF-8";
|
||||
reader.onload = function(e) {
|
||||
var out = my.extractFields(my.parse(e.target.result, dataset), dataset);
|
||||
out.useMemoryStore = true;
|
||||
out.metadata = {
|
||||
filename: dataset.file.name
|
||||
};
|
||||
dfd.resolve(out);
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
dfd.reject({
|
||||
error: {
|
||||
message: "Failed to load file. Code: " + e.target.error.code
|
||||
}
|
||||
});
|
||||
};
|
||||
reader.readAsText(dataset.file, encoding);
|
||||
} else if (dataset.data) {
|
||||
var out = my.extractFields(my.parse(dataset.data, dataset), dataset);
|
||||
out.useMemoryStore = true;
|
||||
dfd.resolve(out);
|
||||
} else if (dataset.url) {
|
||||
var fetch =
|
||||
window.fetch ||
|
||||
function(url) {
|
||||
var jq = jQuery.get(url);
|
||||
|
||||
var promiseResult = {
|
||||
then: function(res) {
|
||||
jq.done(res);
|
||||
return promiseResult;
|
||||
},
|
||||
catch: function(rej) {
|
||||
jq.fail(rej);
|
||||
return promiseResult;
|
||||
}
|
||||
};
|
||||
return promiseResult;
|
||||
};
|
||||
fetch(dataset.url)
|
||||
.then(function(response) {
|
||||
if (response.text) {
|
||||
return response.text();
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
})
|
||||
.then(function(data) {
|
||||
var out = my.extractFields(my.parse(data, dataset), dataset);
|
||||
out.useMemoryStore = true;
|
||||
dfd.resolve(out);
|
||||
})
|
||||
.catch(function(req, status) {
|
||||
dfd.reject({
|
||||
error: {
|
||||
message: "Failed to load file. " +
|
||||
req.statusText +
|
||||
". Code: " +
|
||||
req.status,
|
||||
request: req
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// Convert array of rows in { records: [ ...] , fields: [ ... ] }
|
||||
// @param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data.
|
||||
my.extractFields = function(rows, noFields) {
|
||||
if (noFields.noHeaderRow !== true && rows.length > 0) {
|
||||
return {
|
||||
fields: rows[0],
|
||||
records: rows.slice(1)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
records: rows
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
my.normalizeDialectOptions = function(options) {
|
||||
// note lower case compared to CSV DDF
|
||||
var out = {
|
||||
delimiter: ",",
|
||||
doublequote: true,
|
||||
lineterminator: "\n",
|
||||
quotechar: '"',
|
||||
skipinitialspace: true,
|
||||
skipinitialrows: 0
|
||||
};
|
||||
for (var key in options) {
|
||||
if (key === "trim") {
|
||||
out["skipinitialspace"] = options.trim;
|
||||
} else {
|
||||
out[key.toLowerCase()] = options[key];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// ## parse
|
||||
//
|
||||
// For docs see the README
|
||||
//
|
||||
// Heavily based on uselesscode's JS CSV parser (MIT Licensed):
|
||||
// http://www.uselesscode.org/javascript/csv/
|
||||
my.parse = function(s, dialect) {
|
||||
// When line terminator is not provided then we try to guess it
|
||||
// and normalize it across the file.
|
||||
if (!dialect || (dialect && !dialect.lineterminator)) {
|
||||
s = my.normalizeLineTerminator(s, dialect);
|
||||
}
|
||||
|
||||
// Get rid of any trailing \n
|
||||
var options = my.normalizeDialectOptions(dialect);
|
||||
s = chomp(s, options.lineterminator);
|
||||
|
||||
var cur = "", // The character we are currently processing.
|
||||
inQuote = false,
|
||||
fieldQuoted = false,
|
||||
field = "", // Buffer for building up the current field
|
||||
row = [],
|
||||
out = [],
|
||||
i,
|
||||
processField;
|
||||
|
||||
processField = function(field) {
|
||||
if (fieldQuoted !== true) {
|
||||
// If field is empty set to null
|
||||
if (field === "") {
|
||||
field = null;
|
||||
// If the field was not quoted and we are trimming fields, trim it
|
||||
} else if (options.skipinitialspace === true) {
|
||||
field = trim(field);
|
||||
}
|
||||
|
||||
// Convert unquoted numbers to their appropriate types
|
||||
if (rxIsInt.test(field)) {
|
||||
field = parseInt(field, 10);
|
||||
} else if (rxIsFloat.test(field)) {
|
||||
field = parseFloat(field, 10);
|
||||
}
|
||||
}
|
||||
return field;
|
||||
};
|
||||
|
||||
for (i = 0; i < s.length; i += 1) {
|
||||
cur = s.charAt(i);
|
||||
|
||||
// If we are at a EOF or EOR
|
||||
if (
|
||||
inQuote === false &&
|
||||
(cur === options.delimiter || cur === options.lineterminator)
|
||||
) {
|
||||
field = processField(field);
|
||||
// Add the current field to the current row
|
||||
row.push(field);
|
||||
// If this is EOR append row to output and flush row
|
||||
if (cur === options.lineterminator) {
|
||||
out.push(row);
|
||||
row = [];
|
||||
}
|
||||
// Flush the field buffer
|
||||
field = "";
|
||||
fieldQuoted = false;
|
||||
} else {
|
||||
// If it's not a quotechar, add it to the field buffer
|
||||
if (cur !== options.quotechar) {
|
||||
field += cur;
|
||||
} else {
|
||||
if (!inQuote) {
|
||||
// We are not in a quote, start a quote
|
||||
inQuote = true;
|
||||
fieldQuoted = true;
|
||||
} else {
|
||||
// Next char is quotechar, this is an escaped quotechar
|
||||
if (s.charAt(i + 1) === options.quotechar) {
|
||||
field += options.quotechar;
|
||||
// Skip the next char
|
||||
i += 1;
|
||||
} else {
|
||||
// It's not escaping, so end quote
|
||||
inQuote = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last field
|
||||
field = processField(field);
|
||||
row.push(field);
|
||||
out.push(row);
|
||||
|
||||
// Expose the ability to discard initial rows
|
||||
if (options.skipinitialrows) out = out.slice(options.skipinitialrows);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
my.normalizeLineTerminator = function(csvString, dialect) {
|
||||
dialect = dialect || {};
|
||||
|
||||
// Try to guess line terminator if it's not provided.
|
||||
if (!dialect.lineterminator) {
|
||||
return csvString.replace(/(\r\n|\n|\r)/gm, "\n");
|
||||
}
|
||||
// if not return the string untouched.
|
||||
return csvString;
|
||||
};
|
||||
|
||||
my.objectToArray = function(dataToSerialize) {
|
||||
var a = [];
|
||||
var fieldNames = [];
|
||||
var fieldIds = [];
|
||||
for (var ii = 0; ii < dataToSerialize.fields.length; ii++) {
|
||||
var id = dataToSerialize.fields[ii].id;
|
||||
fieldIds.push(id);
|
||||
var label = dataToSerialize.fields[ii].label ? dataToSerialize.fields[ii].label : id;
|
||||
fieldNames.push(label);
|
||||
}
|
||||
a.push(fieldNames);
|
||||
for (var ii = 0; ii < dataToSerialize.records.length; ii++) {
|
||||
var tmp = [];
|
||||
var record = dataToSerialize.records[ii];
|
||||
for (var jj = 0; jj < fieldIds.length; jj++) {
|
||||
tmp.push(record[fieldIds[jj]]);
|
||||
}
|
||||
a.push(tmp);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
// ## serialize
|
||||
//
|
||||
// See README for docs
|
||||
//
|
||||
// Heavily based on uselesscode's JS CSV serializer (MIT Licensed):
|
||||
// http://www.uselesscode.org/javascript/csv/
|
||||
my.serialize = function(dataToSerialize, dialect) {
|
||||
var a = null;
|
||||
if (dataToSerialize instanceof Array) {
|
||||
a = dataToSerialize;
|
||||
} else {
|
||||
a = my.objectToArray(dataToSerialize);
|
||||
}
|
||||
var options = my.normalizeDialectOptions(dialect);
|
||||
|
||||
var cur = "", // The character we are currently processing.
|
||||
field = "", // Buffer for building up the current field
|
||||
row = "",
|
||||
out = "",
|
||||
i,
|
||||
j,
|
||||
processField;
|
||||
|
||||
processField = function(field) {
|
||||
if (field === null) {
|
||||
// If field is null set to empty string
|
||||
field = "";
|
||||
} else if (typeof field === "string" && rxNeedsQuoting.test(field)) {
|
||||
if (options.doublequote) {
|
||||
field = field.replace(/"/g, '""');
|
||||
}
|
||||
// Convert string to delimited string
|
||||
field = options.quotechar + field + options.quotechar;
|
||||
} else if (typeof field === "number") {
|
||||
// Convert number to string
|
||||
field = field.toString(10);
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
for (i = 0; i < a.length; i += 1) {
|
||||
cur = a[i];
|
||||
|
||||
for (j = 0; j < cur.length; j += 1) {
|
||||
field = processField(cur[j]);
|
||||
// If this is EOR append row to output and flush row
|
||||
if (j === cur.length - 1) {
|
||||
row += field;
|
||||
out += row + options.lineterminator;
|
||||
row = "";
|
||||
} else {
|
||||
// Add the current field to the current row
|
||||
row += field + options.delimiter;
|
||||
}
|
||||
// Flush the field buffer
|
||||
field = "";
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
var rxIsInt = /^\d+$/,
|
||||
rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
|
||||
// If a string has leading or trailing space,
|
||||
// contains a comma double quote or a newline
|
||||
// it needs to be quoted in CSV output
|
||||
rxNeedsQuoting = /^\s|\s$|,|"|\n/,
|
||||
trim = (function() {
|
||||
// Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
|
||||
if (String.prototype.trim) {
|
||||
return function(s) {
|
||||
return s.trim();
|
||||
};
|
||||
} else {
|
||||
return function(s) {
|
||||
return s.replace(/^\s*/, "").replace(/\s*$/, "");
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
function chomp(s, lineterminator) {
|
||||
if (s.charAt(s.length - lineterminator.length) !== lineterminator) {
|
||||
// Does not end with \n, just return string
|
||||
return s;
|
||||
} else {
|
||||
// Remove the \n
|
||||
return s.substring(0, s.length - lineterminator.length);
|
||||
}
|
||||
}
|
||||
})(CSV);
|
||||
|
||||
// backwards compatability for use in Recline
|
||||
var recline = recline || {};
|
||||
recline.Backend = recline.Backend || {};
|
||||
recline.Backend.CSV = CSV;
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CSV;
|
||||
}
|
||||
13
generate-ballots/dist/jszip.min.js
vendored
Normal file
13
generate-ballots/dist/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
generate-ballots/homeowners_registry_test.csv
Normal file
9
generate-ballots/homeowners_registry_test.csv
Normal file
@ -0,0 +1,9 @@
|
||||
"Кадастровый номер";"Тип помещения";"№ квартиры";"Фамили, Имя, Отчество собственника помещения";"Вид собственности";"Общая площадь помещения";"Доля в праве собствен-ности";"Площадь принадлежащая собственнику";"Номер государственной регистрации права/номер правоустанавливающего документа";"Дата регистрации права";"СНИЛС"
|
||||
"77:00:0000000:0062";"Квартира";2;"Икашева Татьяна Викторовна";"Собственность";35,5;"1/1";35,5;"77-77-01/900/0381-149";"07.12.2012";
|
||||
"77:00:0000000:0063";"Квартира";3;"Рудаков Георгий Венедиктович";"Собственность";45,4;"1/1";45,4;"77-77-03/448/9887-791";"20.06.2008";"815-853-756 40"
|
||||
"77:00:0000000:0064";"Квартира";4;"Востряков Геннадий Александрович";"Собственность";45,4;"3/4";34,05;"77-77-79/635/4667-104";" 01.04.2022";"689-210-452 60"
|
||||
"77:00:0000000:0064";"Квартира";4;"Нет данных";"Собственность";45,4;"1/4";45,4;"77-77-54/566/9485-489";"нет данных";
|
||||
"77:00:0000000:0065";"Квартира";5;"Закревская Екатерина Георгьевна";"Собственность";68;"1/1";68;"77-77-24/273/4804-271";"30.09.2011";"735-104-040 70"
|
||||
"77:00:0000000:0066";"Квартира";6;"Реутова Ника Власовна";"Собственность";30,2;"1/1";30,2;"77-77-21/595/0314-470";"17.11.2021";"527-677-519 21"
|
||||
"77:00:0000000:0067";"Квартира";7;"Эмануиль Иван Панкратович";"Собственность";35,5;"1/1";35,5;"77-77-90/756/8276-605";"28.12.1993";"857-484-321 48"
|
||||
"77:00:0000000:0068";"Квартира";8;"Набалкин Петр Наумович";"Собственность";50,7;"1/1";50,7;"77-77-73/634/3253-378";"25.09.1998";
|
||||
|
36
generate-ballots/index.html
Normal file
36
generate-ballots/index.html
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ЖСК Мечта - генерация бюллетеней</title>
|
||||
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<script src="dist/csv.js"></script>
|
||||
<script src="dist/jszip.min.js"></script>
|
||||
<script src="create-ballots.js" type="module"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<label for="registry_file">Реестр собственников, .csv</label>
|
||||
<br>
|
||||
<input type="file" id="registry_file" />
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<label for="template_file">Шаблон бюллетеня, .docx</label>
|
||||
<br>
|
||||
<input type="file" id="template_file" />
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<button id="create_ballots_btn">
|
||||
Сгенерировать бюллетени
|
||||
</button>
|
||||
<br>
|
||||
<progress class="hidden" id="progressbar"> </progress>
|
||||
|
||||
<br>
|
||||
<textarea readonly id="output_text"> </textarea>
|
||||
</body>
|
||||
23
generate-ballots/style.css
Normal file
23
generate-ballots/style.css
Normal file
@ -0,0 +1,23 @@
|
||||
body {
|
||||
line-height: 1.6;
|
||||
font-size: 18px;
|
||||
font-family: sans-serif;
|
||||
color: #222;
|
||||
margin: 40px 40px ;
|
||||
background-color: #fafaff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
input[type="file"]{
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
textarea#output_text
|
||||
{
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user