fix #9
14
README.MD
14
README.MD
@ -165,6 +165,20 @@ GET /api/v1/receipts?limit=50&offset=0&status=created&clientType=individual&sear
|
|||||||
X-Api-Key: strong_api_password
|
X-Api-Key: strong_api_password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Журнал можно фильтровать по периоду через `dateFrom` и `dateTo`:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/receipts?dateFrom=2026-06-01&dateTo=2026-06-30
|
||||||
|
X-Api-Key: strong_api_password
|
||||||
|
```
|
||||||
|
|
||||||
|
Экспортировать все чеки за период в CSV без пагинации:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/receipts/export?dateFrom=2026-06-01&dateTo=2026-06-30
|
||||||
|
X-Api-Key: strong_api_password
|
||||||
|
```
|
||||||
|
|
||||||
Получить один чек:
|
Получить один чек:
|
||||||
|
|
||||||
```http
|
```http
|
||||||
|
|||||||
@ -36,6 +36,30 @@ async function api(path, options = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadFile(path) {
|
||||||
|
const response = await fetch(path, {
|
||||||
|
headers: headers()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const disposition = response.headers.get("Content-Disposition") || "";
|
||||||
|
const match = /filename="?([^"]+)"?/i.exec(disposition);
|
||||||
|
const filename = match?.[1] || "receipts.csv";
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.append(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
function saveToken(auth) {
|
function saveToken(auth) {
|
||||||
state.token = auth.token;
|
state.token = auth.token;
|
||||||
state.tokenExpiresAt = auth.expiresAt;
|
state.tokenExpiresAt = auth.expiresAt;
|
||||||
@ -116,9 +140,26 @@ function receiptText(receipt) {
|
|||||||
return `${receipt.email || ""} ${receipt.receiptId || ""} ${itemText}`.toLowerCase();
|
return `${receipt.email || ""} ${receipt.receiptId || ""} ${itemText}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function receiptTime(receipt) {
|
||||||
|
return new Date(receipt.createdAt || receipt.operationTime || receipt.raw?.operationTime || "").getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiptInSelectedPeriod(receipt) {
|
||||||
|
const from = $("#receipt-date-from").value;
|
||||||
|
const to = $("#receipt-date-to").value;
|
||||||
|
const time = receiptTime(receipt);
|
||||||
|
|
||||||
|
if ((from || to) && Number.isNaN(time)) return false;
|
||||||
|
if (from && time < new Date(`${from}T00:00:00.000Z`).getTime()) return false;
|
||||||
|
if (to && time > new Date(`${to}T23:59:59.999Z`).getTime()) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderReceipts() {
|
function renderReceipts() {
|
||||||
const query = $("#receipt-search").value.trim().toLowerCase();
|
const query = $("#receipt-search").value.trim().toLowerCase();
|
||||||
const filtered = state.receipts.filter(receipt => receiptText(receipt).includes(query));
|
const filtered = state.receipts.filter(receipt =>
|
||||||
|
receiptText(receipt).includes(query) && receiptInSelectedPeriod(receipt)
|
||||||
|
);
|
||||||
const activeReceipts = filtered.filter(receipt => receipt.status !== "cancelled");
|
const activeReceipts = filtered.filter(receipt => receipt.status !== "cancelled");
|
||||||
const grossTotal = activeReceipts.reduce((sum, receipt) => sum + receiptGross(receipt), 0);
|
const grossTotal = activeReceipts.reduce((sum, receipt) => sum + receiptGross(receipt), 0);
|
||||||
const taxTotal = activeReceipts.reduce((sum, receipt) => sum + receiptTax(receipt), 0);
|
const taxTotal = activeReceipts.reduce((sum, receipt) => sum + receiptTax(receipt), 0);
|
||||||
@ -270,7 +311,24 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#receipt-search").addEventListener("input", renderReceipts);
|
$("#receipt-search").addEventListener("input", renderReceipts);
|
||||||
|
$("#receipt-date-from").addEventListener("input", renderReceipts);
|
||||||
|
$("#receipt-date-to").addEventListener("input", renderReceipts);
|
||||||
$("#reload-receipts").addEventListener("click", () => loadReceipts().then(() => toast("Чеки обновлены")).catch(err => toast(err.message)));
|
$("#reload-receipts").addEventListener("click", () => loadReceipts().then(() => toast("Чеки обновлены")).catch(err => toast(err.message)));
|
||||||
|
$("#export-receipts").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const search = $("#receipt-search").value.trim();
|
||||||
|
const dateFrom = $("#receipt-date-from").value;
|
||||||
|
const dateTo = $("#receipt-date-to").value;
|
||||||
|
if (search) params.set("search", search);
|
||||||
|
if (dateFrom) params.set("dateFrom", dateFrom);
|
||||||
|
if (dateTo) params.set("dateTo", dateTo);
|
||||||
|
await downloadFile(`/admin/api/receipts/export?${params.toString()}`);
|
||||||
|
toast("Экспорт скачан");
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
$("#sync-fns").addEventListener("click", async () => {
|
$("#sync-fns").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
const month = $("#sync-month").value;
|
const month = $("#sync-month").value;
|
||||||
|
|||||||
@ -35,8 +35,11 @@
|
|||||||
<div class="search">
|
<div class="search">
|
||||||
<input id="receipt-search" type="search" placeholder="Поиск по email, ID чека или позиции">
|
<input id="receipt-search" type="search" placeholder="Поиск по email, ID чека или позиции">
|
||||||
</div>
|
</div>
|
||||||
|
<input id="receipt-date-from" class="date-input" type="date" aria-label="Начало периода">
|
||||||
|
<input id="receipt-date-to" class="date-input" type="date" aria-label="Конец периода">
|
||||||
<input id="sync-month" class="month-input" type="month" aria-label="Месяц синхронизации">
|
<input id="sync-month" class="month-input" type="month" aria-label="Месяц синхронизации">
|
||||||
<button id="reload-receipts" type="button">Обновить</button>
|
<button id="reload-receipts" type="button">Обновить</button>
|
||||||
|
<button id="export-receipts" type="button">Экспорт CSV</button>
|
||||||
<button id="sync-fns" type="button">Синхронизировать с ФНС</button>
|
<button id="sync-fns" type="button">Синхронизировать с ФНС</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -679,6 +679,24 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Поиск по email, ID чека и позициям"
|
"description": "Поиск по email, ID чека и позициям"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateFrom",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Начало периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateTo",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Конец периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -1072,6 +1090,174 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/receipts/export": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Receipts"
|
||||||
|
],
|
||||||
|
"summary": "Экспортировать чеки в CSV",
|
||||||
|
"description": "Возвращает все чеки, подходящие под фильтры search, status, clientType, dateFrom и dateTo, без пагинации.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminBearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AdminPassword": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"created",
|
||||||
|
"cancelled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clientType",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"individual",
|
||||||
|
"legal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Поиск по email, ID чека и позициям"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateFrom",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Начало периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateTo",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Конец периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "CSV-файл с чеками",
|
||||||
|
"content": {
|
||||||
|
"text/csv": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "binary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Неверный период экспорта"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Нет доступа"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/api/receipts/export": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Admin"
|
||||||
|
],
|
||||||
|
"summary": "Экспортировать чеки в CSV для админ-панели",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"AdminBearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"created",
|
||||||
|
"cancelled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clientType",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"individual",
|
||||||
|
"legal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Поиск по email, ID чека и позициям"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateFrom",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Начало периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dateTo",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"description": "Конец периода включительно, YYYY-MM-DD или ISO date-time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "CSV-файл с чеками",
|
||||||
|
"content": {
|
||||||
|
"text/csv": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "binary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Неверный период экспорта"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Нет доступа"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,6 +156,10 @@ main {
|
|||||||
max-width: 170px;
|
max-width: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.metrics {
|
.metrics {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||||||
|
|||||||
124
server.js
124
server.js
@ -127,6 +127,16 @@ function receiptAmount(receipt) {
|
|||||||
return Number(receipt.grossAmount ?? receipt.amount ?? receipt.totalAmount ?? 0) || 0;
|
return Number(receipt.grossAmount ?? receipt.amount ?? receipt.totalAmount ?? 0) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function receiptDateValue(receipt) {
|
||||||
|
return receipt.createdAt ||
|
||||||
|
receipt.operationTime ||
|
||||||
|
receipt.requestTime ||
|
||||||
|
receipt.raw?.operationTime ||
|
||||||
|
receipt.raw?.requestTime ||
|
||||||
|
receipt.raw?.createdAt ||
|
||||||
|
"";
|
||||||
|
}
|
||||||
|
|
||||||
function withReceiptFinancials(receipt) {
|
function withReceiptFinancials(receipt) {
|
||||||
const clientType = normalizeClientType(
|
const clientType = normalizeClientType(
|
||||||
receipt.clientType ||
|
receipt.clientType ||
|
||||||
@ -602,10 +612,18 @@ function filterReceipts(receipts, query = {}) {
|
|||||||
const status = String(query.status || "").trim().toLowerCase();
|
const status = String(query.status || "").trim().toLowerCase();
|
||||||
const clientType = String(query.clientType || "").trim().toLowerCase();
|
const clientType = String(query.clientType || "").trim().toLowerCase();
|
||||||
const search = String(query.search || "").trim().toLowerCase();
|
const search = String(query.search || "").trim().toLowerCase();
|
||||||
|
const dateFrom = parseDateBoundary(query.dateFrom || query.from, false);
|
||||||
|
const dateTo = parseDateBoundary(query.dateTo || query.to, true);
|
||||||
|
|
||||||
return receipts.filter(receipt => {
|
return receipts.filter(receipt => {
|
||||||
if (status && String(receipt.status || "").toLowerCase() !== status) return false;
|
if (status && String(receipt.status || "").toLowerCase() !== status) return false;
|
||||||
if (clientType && normalizeClientType(receipt.clientType) !== normalizeClientType(clientType)) return false;
|
if (clientType && normalizeClientType(receipt.clientType) !== normalizeClientType(clientType)) return false;
|
||||||
|
if (dateFrom || dateTo) {
|
||||||
|
const receiptTime = new Date(receiptDateValue(receipt)).getTime();
|
||||||
|
if (Number.isNaN(receiptTime)) return false;
|
||||||
|
if (dateFrom && receiptTime < dateFrom.getTime()) return false;
|
||||||
|
if (dateTo && receiptTime > dateTo.getTime()) return false;
|
||||||
|
}
|
||||||
if (!search) return true;
|
if (!search) return true;
|
||||||
|
|
||||||
const itemText = (receipt.items || [])
|
const itemText = (receipt.items || [])
|
||||||
@ -616,6 +634,30 @@ function filterReceipts(receipts, query = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDateBoundary(value, endOfDay = false) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
||||||
|
const date = dateOnly
|
||||||
|
? new Date(Date.UTC(
|
||||||
|
Number(dateOnly[1]),
|
||||||
|
Number(dateOnly[2]) - 1,
|
||||||
|
Number(dateOnly[3]),
|
||||||
|
endOfDay ? 23 : 0,
|
||||||
|
endOfDay ? 59 : 0,
|
||||||
|
endOfDay ? 59 : 0,
|
||||||
|
endOfDay ? 999 : 0
|
||||||
|
))
|
||||||
|
: new Date(text);
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
throw new Error("Период должен быть в формате YYYY-MM-DD или ISO date-time");
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
function paginate(items, query = {}) {
|
function paginate(items, query = {}) {
|
||||||
const limit = Math.min(Math.max(Number(query.limit) || 50, 1), 200);
|
const limit = Math.min(Math.max(Number(query.limit) || 50, 1), 200);
|
||||||
const offset = Math.max(Number(query.offset) || 0, 0);
|
const offset = Math.max(Number(query.offset) || 0, 0);
|
||||||
@ -628,6 +670,53 @@ function paginate(items, query = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function csvCell(value) {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
const text = Array.isArray(value) ? value.join("; ") : String(value);
|
||||||
|
const protectedText = /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
|
||||||
|
return `"${protectedText.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiptsToCsv(receipts) {
|
||||||
|
const columns = [
|
||||||
|
["createdAt", "Дата"],
|
||||||
|
["receiptId", "ID чека"],
|
||||||
|
["email", "Email"],
|
||||||
|
["clientType", "Тип клиента"],
|
||||||
|
["grossAmount", "Грязными"],
|
||||||
|
["taxRate", "Ставка налога"],
|
||||||
|
["taxAmount", "Налог"],
|
||||||
|
["netAmount", "Чистыми"],
|
||||||
|
["status", "Статус"],
|
||||||
|
["emailSent", "Письмо отправлено"],
|
||||||
|
["source", "Источник"],
|
||||||
|
["items", "Позиции"],
|
||||||
|
["printLink", "Ссылка"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = receipts.map(receipt => columns.map(([key]) => {
|
||||||
|
if (key === "createdAt") return csvCell(receiptDateValue(receipt));
|
||||||
|
if (key === "source") return csvCell(receipt.syncedFromFns ? "fns" : "local");
|
||||||
|
if (key === "items") {
|
||||||
|
return csvCell((receipt.items || [])
|
||||||
|
.map(item => [item.id, item.name || item.title, item.price, item.quantity].filter(Boolean).join(" "))
|
||||||
|
.filter(Boolean));
|
||||||
|
}
|
||||||
|
return csvCell(receipt[key]);
|
||||||
|
}).join(","));
|
||||||
|
|
||||||
|
return [
|
||||||
|
columns.map(([, label]) => csvCell(label)).join(","),
|
||||||
|
...rows
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFileName(query = {}) {
|
||||||
|
const from = String(query.dateFrom || query.from || "all").replace(/[^\dA-Za-z_-]/g, "-");
|
||||||
|
const to = String(query.dateTo || query.to || "all").replace(/[^\dA-Za-z_-]/g, "-");
|
||||||
|
return `receipts-${from}-${to}.csv`;
|
||||||
|
}
|
||||||
|
|
||||||
function isCancelledFnsIncome(item) {
|
function isCancelledFnsIncome(item) {
|
||||||
const values = [
|
const values = [
|
||||||
item.status,
|
item.status,
|
||||||
@ -995,8 +1084,28 @@ app.get("/admin/api/receipts", requireAdmin, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/admin/api/receipts/export", requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const receipts = filterReceipts(await readReceipts(), req.query);
|
||||||
|
const csv = receiptsToCsv(receipts);
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${exportFileName(req.query)}"`);
|
||||||
|
res.send(`\uFEFF${csv}`);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message || "Неверный период экспорта" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/v1/receipts", requireApiAccess, async (req, res) => {
|
app.get("/api/v1/receipts", requireApiAccess, async (req, res) => {
|
||||||
const receipts = filterReceipts(await readReceipts(), req.query);
|
let receipts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
receipts = filterReceipts(await readReceipts(), req.query);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).json({ error: err.message || "Неверный период фильтрации" });
|
||||||
|
}
|
||||||
|
|
||||||
const page = paginate(receipts, req.query);
|
const page = paginate(receipts, req.query);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -1010,6 +1119,19 @@ app.get("/api/v1/receipts", requireApiAccess, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/api/v1/receipts/export", requireApiAccess, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const receipts = filterReceipts(await readReceipts(), req.query);
|
||||||
|
const csv = receiptsToCsv(receipts);
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${exportFileName(req.query)}"`);
|
||||||
|
res.send(`\uFEFF${csv}`);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message || "Неверный период экспорта" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/v1/receipts/:receiptId", requireApiAccess, async (req, res) => {
|
app.get("/api/v1/receipts/:receiptId", requireApiAccess, async (req, res) => {
|
||||||
const receipts = await readReceipts();
|
const receipts = await readReceipts();
|
||||||
const receipt = receipts.find(item => item.receiptId === req.params.receiptId);
|
const receipt = receipts.find(item => item.receiptId === req.params.receiptId);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user