feat: export historie jídel uživatele
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m53s
CI / Build and push Docker image (push) Successful in 1m1s
CI / Notify (push) Successful in 3s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m53s
CI / Build and push Docker image (push) Successful in 1m1s
CI / Notify (push) Successful in 3s
This commit is contained in:
@@ -7,3 +7,4 @@ server/public/
|
||||
.claude/*.lock
|
||||
.claude/worktrees
|
||||
.playwright-mcp
|
||||
.idea/
|
||||
|
||||
@@ -46,6 +46,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
.export-panel {
|
||||
margin-top: 32px;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
background: var(--luncher-bg-card);
|
||||
border: 1px solid var(--luncher-border-light);
|
||||
border-radius: var(--luncher-radius-lg);
|
||||
box-shadow: var(--luncher-shadow-sm);
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--luncher-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.export-hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--luncher-text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.export-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
input {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
select {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chart container
|
||||
.recharts-wrapper {
|
||||
background: var(--luncher-bg-card);
|
||||
|
||||
@@ -4,9 +4,10 @@ import Header from "../components/Header";
|
||||
import { useAuth } from "../context/auth";
|
||||
import Login from "../Login";
|
||||
import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils";
|
||||
import { WeeklyStats, LunchChoice, getStats } from "../../../types";
|
||||
import { WeeklyStats, LunchChoice, getStats, getStatsExport } from "../../../types";
|
||||
import Loader from "../components/Loader";
|
||||
import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faChevronLeft, faChevronRight, faGear, faFileExcel, faFileCsv, faFileCode } from "@fortawesome/free-solid-svg-icons";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { getLunchChoiceName } from "../enums";
|
||||
@@ -28,10 +29,35 @@ const COLORS = [
|
||||
'#7c7c7c',
|
||||
]
|
||||
|
||||
/** Podporované formáty exportu přehledu (viz GET /api/stats/export). */
|
||||
const EXPORT_FORMATS = ['xlsx', 'csv', 'json'] as const;
|
||||
|
||||
type ExportFormat = typeof EXPORT_FORMATS[number];
|
||||
|
||||
const EXPORT_FORMAT_LABELS: Record<ExportFormat, string> = {
|
||||
xlsx: 'Excel (.xlsx)',
|
||||
csv: 'CSV (.csv)',
|
||||
json: 'JSON (.json)',
|
||||
};
|
||||
|
||||
const EXPORT_FORMAT_ICONS: Record<ExportFormat, IconDefinition> = {
|
||||
xlsx: faFileExcel,
|
||||
csv: faFileCsv,
|
||||
json: faFileCode,
|
||||
};
|
||||
|
||||
/** Vrátí měsíc předaného data ve formátu YYYY-MM (hodnota pro input type="month"). */
|
||||
function getMonthValue(date: Date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function StatsPage() {
|
||||
const auth = useAuth();
|
||||
const [dateRange, setDateRange] = useState<Date[]>();
|
||||
const [data, setData] = useState<WeeklyStats>();
|
||||
const [exportMonth, setExportMonth] = useState<string>(() => getMonthValue(new Date()));
|
||||
const [exportFormat, setExportFormat] = useState<ExportFormat>('xlsx');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
// Prvotní nastavení aktuálního týdne
|
||||
useEffect(() => {
|
||||
@@ -53,6 +79,31 @@ export default function StatsPage() {
|
||||
return <Line key={location} name={getLunchChoiceName(location)} type="monotone" dataKey={data => data.locations[location] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
|
||||
}
|
||||
|
||||
/** Stáhne přehled stravování přihlášeného uživatele za vybraný měsíc ve vybraném formátu. */
|
||||
const handleExport = async () => {
|
||||
const [year, month] = exportMonth.split('-').map(Number);
|
||||
if (!year || !month) {
|
||||
return;
|
||||
}
|
||||
setExporting(true);
|
||||
try {
|
||||
// parseAs 'blob' — odpověď chceme stáhnout tak, jak přišla, bez parsování dle typu
|
||||
const { data: file } = await getStatsExport({ query: { year, month, format: exportFormat }, parseAs: 'blob' });
|
||||
// Chyby řeší globální interceptor (toaster), stahujeme jen při úspěchu
|
||||
if (!(file instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(file);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `luncher-${auth?.login ?? 'prehled'}-${exportMonth}.${exportFormat}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviousWeek = () => {
|
||||
if (dateRange) {
|
||||
const previousStartDate = new Date(dateRange[0]);
|
||||
@@ -128,6 +179,40 @@ export default function StatsPage() {
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</LineChart>
|
||||
<div className="export-panel">
|
||||
<h3>Můj přehled</h3>
|
||||
<p className="export-hint">
|
||||
Přehled vašich záznamů za vybraný měsíc — datum, kde jste jedli, vybrané jídlo, poznámka
|
||||
a u objednávek i částka po slevě.
|
||||
</p>
|
||||
<div className="export-controls">
|
||||
<input
|
||||
type="month"
|
||||
className="form-control"
|
||||
value={exportMonth}
|
||||
max={getMonthValue(new Date())}
|
||||
onChange={e => setExportMonth(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="form-select"
|
||||
value={exportFormat}
|
||||
onChange={e => setExportFormat(e.target.value as ExportFormat)}
|
||||
>
|
||||
{EXPORT_FORMATS.map(format => (
|
||||
<option key={format} value={format}>{EXPORT_FORMAT_LABELS[format]}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={exporting || !exportMonth}
|
||||
onClick={handleExport}
|
||||
>
|
||||
<FontAwesomeIcon icon={EXPORT_FORMAT_ICONS[exportFormat]} />{' '}
|
||||
{exporting ? 'Generuji...' : 'Stáhnout přehled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"Na stránce statistik lze stáhnout vlastní přehled stravování za vybraný měsíc ve formátu Excel, CSV nebo JSON (včetně vybraného jídla, poznámky a částky u objednávek po slevě)"
|
||||
]
|
||||
@@ -36,6 +36,7 @@
|
||||
"cheerio": "^1.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"redis": "^5.9.0",
|
||||
|
||||
@@ -2,10 +2,34 @@ import express, { Request, Response } from "express";
|
||||
import { getLogin } from "../auth";
|
||||
import { parseToken } from "../utils";
|
||||
import { getStats } from "../stats";
|
||||
import { EXPORT_FORMATS, generateUserExport, isExportFormat } from "../userExport";
|
||||
import { WeeklyStats } from "../../../types/gen/types.gen";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Vrátí přehled stravování přihlášeného uživatele za vybraný měsíc (XLSX, CSV nebo JSON).
|
||||
*/
|
||||
router.get("/export", async (req: Request<{}, any, undefined>, res: Response) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
const year = Number(req.query.year);
|
||||
const month = Number(req.query.month);
|
||||
const format = req.query.format ?? 'xlsx';
|
||||
if (!Number.isInteger(year) || year < 2000 || year > 2100) {
|
||||
return res.status(400).json({ error: "Neplatný rok" });
|
||||
}
|
||||
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
||||
return res.status(400).json({ error: "Neplatný měsíc" });
|
||||
}
|
||||
if (!isExportFormat(format)) {
|
||||
return res.status(400).json({ error: `Neplatný formát exportu, podporované jsou: ${EXPORT_FORMATS.join(', ')}` });
|
||||
}
|
||||
const { content, mimeType, fileName } = await generateUserExport(login, year, month, format);
|
||||
res.setHeader('Content-Type', mimeType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
return res.status(200).send(content);
|
||||
});
|
||||
|
||||
router.get("/", async (req: Request<{}, any, undefined>, res: Response<WeeklyStats>) => {
|
||||
getLogin(parseToken(req));
|
||||
if (typeof req.query.startDate === 'string' && typeof req.query.endDate === 'string') {
|
||||
|
||||
@@ -52,6 +52,64 @@ test('GET /stats s budoucím datem vrátí 400', async () => {
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /stats/export vrátí XLSX soubor', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.query({ year: 2025, month: 3 })
|
||||
.responseType('blob')
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers['content-type']).toContain('spreadsheetml');
|
||||
expect(res.headers['content-disposition']).toContain('luncher-testuser-2025-03.xlsx');
|
||||
// ZIP signatura XLSX souboru
|
||||
expect(res.body.slice(0, 2).toString()).toBe('PK');
|
||||
});
|
||||
|
||||
test('GET /stats/export?format=csv vrátí CSV', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.query({ year: 2025, month: 3, format: 'csv' })
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers['content-type']).toContain('text/csv');
|
||||
expect(res.headers['content-disposition']).toContain('luncher-testuser-2025-03.csv');
|
||||
expect(res.text).toContain('"Datum";"Den";"Typ"');
|
||||
});
|
||||
|
||||
test('GET /stats/export?format=json vrátí JSON s metadaty', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.query({ year: 2025, month: 3, format: 'json' })
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers['content-type']).toContain('application/json');
|
||||
expect(res.headers['content-disposition']).toContain('luncher-testuser-2025-03.json');
|
||||
expect(res.body).toMatchObject({ login: 'testuser', year: 2025, month: 3, rowCount: 0, rows: [] });
|
||||
});
|
||||
|
||||
test('GET /stats/export s neznámým formátem vrátí 400', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.query({ year: 2025, month: 3, format: 'pdf' })
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /stats/export s neplatným měsícem vrátí 400', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.query({ year: 2025, month: 13 })
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /stats/export bez parametrů vrátí 400', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats/export')
|
||||
.set('Authorization', TOKEN);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /stats bez tokenu vrátí chybu', async () => {
|
||||
const res = await request(buildApp())
|
||||
.get('/api/stats')
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import getStorage from '../storage';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import { getUserExportRows, computeMemberAmount, getUserExportFileName, buildCsv, buildJson, generateUserExport, isExportFormat, UserExportRow } from '../userExport';
|
||||
import { ClientData, GroupState, LunchChoice, OrderGroup, PizzaDayState, WeekMenu } from '../../../types/gen/types.gen';
|
||||
|
||||
const storage = getStorage();
|
||||
const USER = 'petr';
|
||||
const OTHER = 'tomas';
|
||||
|
||||
// 2025-03-03 je pondělí, 2025-03-04 úterý, ...
|
||||
const YEAR = 2025;
|
||||
const MONTH = 3;
|
||||
|
||||
/** Uloží denní data pro slot oběd. */
|
||||
async function saveLunch(isoDate: string, choices: ClientData["choices"], pizzaDay?: ClientData["pizzaDay"]) {
|
||||
await storage.setData<Partial<ClientData>>(isoDate, { choices, pizzaDay });
|
||||
}
|
||||
|
||||
/** Uloží data slotu extra s předanými skupinami. */
|
||||
async function saveExtra(isoDate: string, groups: OrderGroup[]) {
|
||||
await storage.setData<Partial<ClientData>>(`${isoDate}_extra`, { choices: {}, groups });
|
||||
}
|
||||
|
||||
/** Uloží týdenní menu tak, aby daný den obsahoval předaná jídla. */
|
||||
async function saveWeekMenu(menuKey: string, dayIndex: number, foodNames: string[]) {
|
||||
const week: WeekMenu = [{}, {}, {}, {}, {}] as WeekMenu;
|
||||
week[dayIndex] = {
|
||||
TECHTOWER: {
|
||||
lastUpdate: 0,
|
||||
closed: false,
|
||||
food: foodNames.map(name => ({ name, isSoup: false })),
|
||||
},
|
||||
};
|
||||
await storage.setData(menuKey, week);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
describe('getUserExportRows', () => {
|
||||
test('vynechá dny bez záznamu i volbu "mám vlastní/neobědvám"', async () => {
|
||||
await saveLunch('2025-03-03', { NEOBEDVAM: { [USER]: {} } });
|
||||
await saveLunch('2025-03-04', { SPSE: { [USER]: {} } });
|
||||
// den, kde má volbu jen jiný uživatel
|
||||
await saveLunch('2025-03-05', { SPSE: { [OTHER]: {} } });
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({ date: '2025-03-04', type: 'SPŠE' });
|
||||
});
|
||||
|
||||
test('vyplní vybrané jídlo z uloženého menu i poznámku', async () => {
|
||||
await saveWeekMenu('menu_2025_10', 0, ['Polévka', 'Svíčková', 'Guláš']);
|
||||
await saveLunch('2025-03-03', {
|
||||
TECHTOWER: { [USER]: { selectedFoods: [1, 2], note: 'bez knedlíku' } },
|
||||
});
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
date: '2025-03-03',
|
||||
dayOfWeek: 'pondělí',
|
||||
type: 'TechTower',
|
||||
food: 'Svíčková + Guláš',
|
||||
note: 'bez knedlíku',
|
||||
});
|
||||
});
|
||||
|
||||
test('u volby "budu objednávat" sloučí záznam s objednávkovou skupinou včetně ceny po slevě', async () => {
|
||||
await saveLunch('2025-03-06', { OBJEDNAVAM: { [USER]: { note: 'platím kartou' } } });
|
||||
await saveExtra('2025-03-06', [{
|
||||
id: 'g1',
|
||||
name: 'KFC',
|
||||
creatorLogin: OTHER,
|
||||
state: GroupState.ORDERED,
|
||||
orderedAt: '11:30',
|
||||
shipping: 4000,
|
||||
discountType: 'percent',
|
||||
discountValue: 10,
|
||||
members: {
|
||||
[USER]: { amount: 20000, note: 'Twister menu', paid: true },
|
||||
[OTHER]: { amount: 20000 },
|
||||
},
|
||||
}]);
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
date: '2025-03-06',
|
||||
type: 'Budu objednávat',
|
||||
food: 'Twister menu',
|
||||
note: 'platím kartou',
|
||||
store: 'KFC',
|
||||
orderedBy: OTHER,
|
||||
// 200 Kč + 20 Kč doprava (poloviny ze 40) - 20 Kč sleva (10 %)
|
||||
amount: 20000 + 2000 - 2000,
|
||||
});
|
||||
});
|
||||
|
||||
test('objednávku bez volby oběda vypíše jako samostatný záznam', async () => {
|
||||
await saveExtra('2025-03-07', [{
|
||||
id: 'g2',
|
||||
name: 'Bageterie',
|
||||
creatorLogin: USER,
|
||||
state: GroupState.OPEN,
|
||||
members: { [USER]: { amount: 15000, surchargeText: 'sýr', surchargeAmount: 2000 } },
|
||||
}]);
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
date: '2025-03-07',
|
||||
type: 'Objednávka',
|
||||
store: 'Bageterie',
|
||||
orderedBy: USER,
|
||||
food: 'příplatek: sýr',
|
||||
amount: 17000,
|
||||
});
|
||||
});
|
||||
|
||||
test('u Pizza day vypíše objednané pizzy a celkovou cenu', async () => {
|
||||
await saveLunch('2025-03-10', { PIZZA: { [USER]: {} } }, {
|
||||
state: PizzaDayState.DELIVERED,
|
||||
creator: OTHER,
|
||||
orders: [{
|
||||
customer: USER,
|
||||
pizzaList: [{ varId: 1, name: 'Margherita', size: '32cm', price: 18000 }],
|
||||
fee: { text: 'kuřecí maso', price: 3000 },
|
||||
totalPrice: 21000,
|
||||
note: 'bez oliv',
|
||||
hasQr: true,
|
||||
}],
|
||||
} as ClientData["pizzaDay"]);
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
type: 'Pizza day',
|
||||
food: 'Margherita (32cm) + příplatek: kuřecí maso',
|
||||
note: 'bez oliv',
|
||||
amount: 21000,
|
||||
});
|
||||
});
|
||||
|
||||
test('více skupin v jeden den vypíše jako samostatné řádky', async () => {
|
||||
await saveLunch('2025-03-11', { OBJEDNAVAM: { [USER]: {} } });
|
||||
await saveExtra('2025-03-11', [
|
||||
{ id: 'a', name: 'KFC', creatorLogin: USER, state: GroupState.OPEN, members: { [USER]: { amount: 10000 } } },
|
||||
{ id: 'b', name: 'Bageterie', creatorLogin: USER, state: GroupState.OPEN, members: { [USER]: { amount: 5000 } } },
|
||||
]);
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(2);
|
||||
// V rámci dne je nejdřív volba oběda (se sloučenou první skupinou), pak zbytek objednávek
|
||||
expect(rows.map(r => r.type)).toEqual(['Budu objednávat', 'Objednávka']);
|
||||
expect(rows.map(r => r.store)).toEqual(['KFC', 'Bageterie']);
|
||||
});
|
||||
|
||||
test('nezahrne data mimo vybraný měsíc', async () => {
|
||||
await saveLunch('2025-02-28', { SPSE: { [USER]: {} } });
|
||||
await saveLunch('2025-03-03', { SPSE: { [USER]: {} } });
|
||||
await saveLunch('2025-04-01', { SPSE: { [USER]: {} } });
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows.map(r => r.date)).toEqual(['2025-03-03']);
|
||||
});
|
||||
});
|
||||
|
||||
test('člen skupiny, který si nic neobjednal, nemá vyplněnou částku', async () => {
|
||||
await saveExtra('2025-03-12', [{
|
||||
id: 'g3',
|
||||
name: 'KFC',
|
||||
creatorLogin: USER,
|
||||
state: GroupState.ORDERED,
|
||||
members: { [USER]: {}, [OTHER]: { amount: 15000 } },
|
||||
}]);
|
||||
|
||||
const rows = await getUserExportRows(USER, YEAR, MONTH);
|
||||
expect(rows).toHaveLength(1);
|
||||
// Objednávající je uveden, ale částka zůstává prázdná — není co platit
|
||||
expect(rows[0].orderedBy).toBe(USER);
|
||||
expect(rows[0].amount).toBeUndefined();
|
||||
expect(buildCsv(rows).split('\r\n')[1].split(';')[7]).toBe('');
|
||||
expect(buildJson(USER, YEAR, MONTH, rows).rows[0].amount).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('computeMemberAmount', () => {
|
||||
const group = (overrides: Partial<OrderGroup>): OrderGroup => ({
|
||||
id: 'g',
|
||||
name: 'KFC',
|
||||
creatorLogin: OTHER,
|
||||
state: GroupState.OPEN,
|
||||
members: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('neaktivní člen (nic si neobjednal) platí nula', () => {
|
||||
const g = group({ shipping: 4000, members: { [USER]: {}, [OTHER]: { amount: 10000 } } });
|
||||
expect(computeMemberAmount(g, USER)).toBe(0);
|
||||
});
|
||||
|
||||
test('pevná sleva se dělí mezi aktivní členy', () => {
|
||||
const g = group({
|
||||
discountType: 'fixed',
|
||||
discountValue: 5000,
|
||||
members: { [USER]: { amount: 10000 }, [OTHER]: { amount: 10000 } },
|
||||
});
|
||||
expect(computeMemberAmount(g, USER)).toBe(10000 - 2500);
|
||||
});
|
||||
|
||||
test('poplatky se rozpočítají jen mezi aktivní členy', () => {
|
||||
const g = group({
|
||||
fees: 1000,
|
||||
shipping: 5000,
|
||||
tip: 0,
|
||||
members: { [USER]: { amount: 10000 }, [OTHER]: {} },
|
||||
});
|
||||
expect(computeMemberAmount(g, USER)).toBe(16000);
|
||||
});
|
||||
});
|
||||
|
||||
test('getUserExportFileName očistí login a doplní měsíc s příponou dle formátu', () => {
|
||||
expect(getUserExportFileName('domena\\petr', 2025, 3)).toBe('luncher-domena_petr-2025-03.xlsx');
|
||||
expect(getUserExportFileName('petr', 2025, 3, 'csv')).toBe('luncher-petr-2025-03.csv');
|
||||
expect(getUserExportFileName('petr', 2025, 12, 'json')).toBe('luncher-petr-2025-12.json');
|
||||
});
|
||||
|
||||
test('isExportFormat propustí jen podporované formáty', () => {
|
||||
expect(isExportFormat('xlsx')).toBe(true);
|
||||
expect(isExportFormat('csv')).toBe(true);
|
||||
expect(isExportFormat('json')).toBe(true);
|
||||
expect(isExportFormat('pdf')).toBe(false);
|
||||
expect(isExportFormat(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
describe('buildCsv', () => {
|
||||
const rows: UserExportRow[] = [
|
||||
{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'TechTower', food: 'Svíčková', note: 'bez knedlíku' },
|
||||
{
|
||||
date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Budu objednávat', food: 'Twister; s "extra" sýrem',
|
||||
store: 'KFC', orderedBy: 'tomas', amount: 20000,
|
||||
},
|
||||
];
|
||||
|
||||
test('začíná BOM a hlavičkou oddělenou středníkem', () => {
|
||||
const csv = buildCsv(rows);
|
||||
expect(csv.charCodeAt(0)).toBe(0xFEFF);
|
||||
expect(csv.slice(1).split('\r\n')[0]).toBe('"Datum";"Den";"Typ";"Vybrané jídlo";"Poznámka";"Objednávka";"Objednával";"Částka"');
|
||||
});
|
||||
|
||||
test('formátuje datum, částku s desetinnou čárkou a escapuje uvozovky i středníky', () => {
|
||||
const lines = buildCsv(rows).split('\r\n');
|
||||
expect(lines[1]).toBe('"03.03.2025";"pondělí";"TechTower";"Svíčková";"bez knedlíku";;;');
|
||||
expect(lines[2]).toBe('"06.03.2025";"čtvrtek";"Budu objednávat";"Twister; s ""extra"" sýrem";;"KFC";"tomas";"200,00"');
|
||||
});
|
||||
|
||||
test('u záznamu bez objednávky nechá sloupce objednávky prázdné', () => {
|
||||
const csv = buildCsv([{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'Pizza day', amount: 21000 }]);
|
||||
const cells = csv.split('\r\n')[1].split(';');
|
||||
expect(cells[5]).toBe('');
|
||||
expect(cells[6]).toBe('');
|
||||
expect(cells[7]).toBe('"210,00"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildJson', () => {
|
||||
test('obsahuje metadata, součet v korunách a jen vyplněná pole', () => {
|
||||
const rows: UserExportRow[] = [
|
||||
{ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'SPŠE' },
|
||||
{ date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Objednávka', store: 'KFC', orderedBy: 'tomas', amount: 22000 },
|
||||
];
|
||||
const json = buildJson('petr', 2025, 3, rows);
|
||||
expect(json).toMatchObject({ login: 'petr', year: 2025, month: 3, rowCount: 2, totalAmount: 220 });
|
||||
expect(json.rows[0]).toEqual({ date: '2025-03-03', dayOfWeek: 'pondělí', type: 'SPŠE' });
|
||||
expect(json.rows[1]).toEqual({
|
||||
date: '2025-03-06', dayOfWeek: 'čtvrtek', type: 'Objednávka',
|
||||
store: 'KFC', orderedBy: 'tomas', amount: 220,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateUserExport', () => {
|
||||
beforeEach(async () => {
|
||||
await storage.setData('2025-03-03', { choices: { SPSE: { [USER]: { note: 'polévka' } } } });
|
||||
});
|
||||
|
||||
test('xlsx vrátí ZIP obsah se správným MIME a názvem', async () => {
|
||||
const result = await generateUserExport(USER, YEAR, MONTH);
|
||||
expect(result.content.subarray(0, 2).toString()).toBe('PK');
|
||||
expect(result.mimeType).toContain('spreadsheetml');
|
||||
expect(result.fileName).toBe('luncher-petr-2025-03.xlsx');
|
||||
});
|
||||
|
||||
test('csv vrátí textový obsah s daty uživatele', async () => {
|
||||
const result = await generateUserExport(USER, YEAR, MONTH, 'csv');
|
||||
expect(result.mimeType).toBe('text/csv; charset=utf-8');
|
||||
expect(result.fileName).toBe('luncher-petr-2025-03.csv');
|
||||
expect(result.content.toString('utf-8')).toContain('"03.03.2025";"pondělí";"SPŠE"');
|
||||
});
|
||||
|
||||
test('json vrátí parsovatelný obsah s metadaty', async () => {
|
||||
const result = await generateUserExport(USER, YEAR, MONTH, 'json');
|
||||
expect(result.mimeType).toBe('application/json; charset=utf-8');
|
||||
expect(result.fileName).toBe('luncher-petr-2025-03.json');
|
||||
const parsed = JSON.parse(result.content.toString('utf-8'));
|
||||
expect(parsed).toMatchObject({ login: USER, year: YEAR, month: MONTH, rowCount: 1 });
|
||||
expect(parsed.rows[0]).toMatchObject({ type: 'SPŠE', note: 'polévka' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,385 @@
|
||||
import ExcelJS from "exceljs";
|
||||
import { ClientData, LunchChoice, OrderGroup, OrderGroupMember, Restaurant, UserExport, WeekMenu } from "../../types/gen/types.gen";
|
||||
import getStorage from "./storage";
|
||||
import { formatDate, getDayOfWeekIndex } from "./utils";
|
||||
import { getMenuKey, getToday } from "./service";
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
const DAY_OF_WEEK_FORMAT = new Intl.DateTimeFormat('cs-CZ', { weekday: 'long' });
|
||||
|
||||
/** Lidsky čitelné názvy voleb stravování (serverová obdoba klientského enums.ts). */
|
||||
const CHOICE_NAMES: Record<LunchChoice, string> = {
|
||||
SLADOVNICKA: 'Sladovnická',
|
||||
TECHTOWER: 'TechTower',
|
||||
ZASTAVKAUMICHALA: 'Zastávka u Michala',
|
||||
SENKSERIKOVA: 'Šenk Šeříková',
|
||||
SPSE: 'SPŠE',
|
||||
PIZZA: 'Pizza day',
|
||||
OBJEDNAVAM: 'Budu objednávat',
|
||||
NEOBEDVAM: 'Mám vlastní/neobědvám',
|
||||
ROZHODUJI: 'Rozhoduji se',
|
||||
};
|
||||
|
||||
/** Název typu záznamu pro objednávku bez odpovídající volby oběda. */
|
||||
const ORDER_ONLY_LABEL = 'Objednávka';
|
||||
|
||||
/** Volby, které v přehledu nechceme (uživatel si nic neobjednal ani nikam nešel). */
|
||||
const EXCLUDED_CHOICES: LunchChoice[] = [LunchChoice.NEOBEDVAM];
|
||||
|
||||
/**
|
||||
* Jeden řádek přehledu stravování uživatele — interní podoba.
|
||||
* Částky jsou zde v haléřích, na koruny se převádějí až při generování výstupu.
|
||||
*/
|
||||
export type UserExportRow = {
|
||||
/** Datum záznamu ve formátu YYYY-MM-DD */
|
||||
date: string;
|
||||
/** Den v týdnu (pondělí, ...) */
|
||||
dayOfWeek: string;
|
||||
/** Název typu záznamu (podnik, "Budu objednávat", "Objednávka", ...) */
|
||||
type: string;
|
||||
/** Konkrétní vybrané jídlo (jídla z menu, pizzy, položky objednávky) */
|
||||
food?: string;
|
||||
/** Poznámka uživatele k volbě */
|
||||
note?: string;
|
||||
/** Obchod/restaurace objednávkové skupiny */
|
||||
store?: string;
|
||||
/** Login objednávajícího (zakladatele objednávkové skupiny) */
|
||||
orderedBy?: string;
|
||||
/** Částka k úhradě po slevě v haléřích */
|
||||
amount?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vrátí true, pokud si člen skupiny reálně něco objednal.
|
||||
* Duplikát klientského {@link ../../client/src/utils/groupFees.ts} — server a klient
|
||||
* nemají společný modul pro doménovou logiku, výpočet ale musí dávat stejné výsledky.
|
||||
*/
|
||||
function isActiveMember(member: OrderGroupMember): boolean {
|
||||
return (member.amount ?? 0) + (member.surchargeAmount ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vypočte částku člena skupiny po rozpočítání poplatků a slevy (v haléřích).
|
||||
* Poplatky i pevná sleva se dělí pouze mezi členy, kteří si něco objednali.
|
||||
*/
|
||||
export function computeMemberAmount(group: OrderGroup, login: string): number {
|
||||
const member = group.members[login];
|
||||
if (!member || !isActiveMember(member)) return 0;
|
||||
const activeCount = Object.values(group.members).filter(isActiveMember).length;
|
||||
const totalFees = (group.fees ?? 0) + (group.shipping ?? 0) + (group.tip ?? 0);
|
||||
const feeShare = activeCount > 0 ? Math.round(totalFees / activeCount) : 0;
|
||||
const base = member.amount ?? 0;
|
||||
const surcharge = member.surchargeAmount ?? 0;
|
||||
const discountValue = group.discountValue ?? 0;
|
||||
const discount = discountValue > 0
|
||||
? (group.discountType === 'percent'
|
||||
? Math.round((base + surcharge) * discountValue / 100)
|
||||
: Math.round(discountValue / activeCount))
|
||||
: 0;
|
||||
return base + surcharge + feeShare - discount;
|
||||
}
|
||||
|
||||
/** Vrátí názvy jídel vybraných uživatelem v daném podniku, dle uloženého týdenního menu. */
|
||||
function getSelectedFoodNames(weekMenu: WeekMenu | undefined, dayIndex: number, restaurant: Restaurant, indexes: number[]): string[] {
|
||||
const food = weekMenu?.[dayIndex]?.[restaurant]?.food;
|
||||
if (!food?.length) return [];
|
||||
return indexes
|
||||
.map(index => food[index]?.name)
|
||||
.filter((name): name is string => !!name?.length);
|
||||
}
|
||||
|
||||
/** Spojí neprázdné texty do jednoho řádku. */
|
||||
function join(parts: (string | undefined)[], separator = ', '): string | undefined {
|
||||
const filtered = parts.filter((part): part is string => !!part?.trim().length);
|
||||
return filtered.length ? filtered.join(separator) : undefined;
|
||||
}
|
||||
|
||||
/** Popis jednoho člena objednávky (co si objednal) — poznámka + případný příplatek. */
|
||||
function getOrderItemText(member: OrderGroupMember): string | undefined {
|
||||
return join([member.note, member.surchargeText ? `příplatek: ${member.surchargeText}` : undefined], ' + ');
|
||||
}
|
||||
|
||||
/** Vrátí popis pizz objednaných uživatelem v rámci Pizza day. */
|
||||
function getPizzaText(data: ClientData | undefined, login: string): { food?: string, note?: string, amount?: number } {
|
||||
const order = data?.pizzaDay?.orders?.find(o => o.customer === login);
|
||||
if (!order) return {};
|
||||
const pizzas = (order.pizzaList ?? []).map(variant => `${variant.name} (${variant.size})`);
|
||||
return {
|
||||
food: join([...pizzas, order.fee?.text ? `příplatek: ${order.fee.text}` : undefined], ' + '),
|
||||
note: order.note,
|
||||
amount: order.totalPrice,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sestaví přehled stravování jednoho uživatele za vybraný měsíc.
|
||||
*
|
||||
* Do přehledu se dostanou pouze dny, kdy uživatel něco zvolil (vynechány jsou dny bez záznamu
|
||||
* a volba "Mám vlastní/neobědvám"), plus dny, kdy byl členem objednávkové skupiny.
|
||||
* Volba "Budu objednávat" se slučuje s odpovídající objednávkovou skupinou do jednoho řádku;
|
||||
* objednávky bez této volby se vypisují jako samostatné řádky.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param year rok
|
||||
* @param month měsíc (1 = leden)
|
||||
* @returns řádky přehledu seřazené dle data
|
||||
*/
|
||||
export async function getUserExportRows(login: string, year: number, month: number): Promise<UserExportRow[]> {
|
||||
const rows: UserExportRow[] = [];
|
||||
const lastDayOfMonth = new Date(year, month, 0).getDate();
|
||||
const today = getToday();
|
||||
today.setHours(23, 59, 59, 999);
|
||||
const weekMenuCache = new Map<string, WeekMenu | undefined>();
|
||||
|
||||
for (let day = 1; day <= lastDayOfMonth; day++) {
|
||||
const date = new Date(year, month - 1, day);
|
||||
if (date > today) break;
|
||||
const isoDate = formatDate(date);
|
||||
const dayOfWeek = DAY_OF_WEEK_FORMAT.format(date);
|
||||
const lunchData = await storage.getData<ClientData>(isoDate);
|
||||
const extraData = await storage.getData<ClientData>(`${isoDate}_extra`);
|
||||
|
||||
// Volba oběda daného uživatele
|
||||
let choice: LunchChoice | undefined;
|
||||
let choiceRow: UserExportRow | undefined;
|
||||
for (const key of Object.keys(lunchData?.choices ?? {})) {
|
||||
const locationKey = key as LunchChoice;
|
||||
const userChoice = lunchData!.choices[locationKey]?.[login];
|
||||
if (!userChoice) continue;
|
||||
choice = locationKey;
|
||||
if (EXCLUDED_CHOICES.includes(locationKey)) break;
|
||||
|
||||
let food: string | undefined;
|
||||
let note = userChoice.note;
|
||||
let amount: number | undefined;
|
||||
if (locationKey === LunchChoice.PIZZA) {
|
||||
const pizza = getPizzaText(lunchData, login);
|
||||
food = pizza.food;
|
||||
note = join([note, pizza.note], ' | ');
|
||||
amount = pizza.amount;
|
||||
} else if (userChoice.selectedFoods?.length) {
|
||||
const menuKey = getMenuKey(date);
|
||||
if (!weekMenuCache.has(menuKey)) {
|
||||
weekMenuCache.set(menuKey, await storage.getData<WeekMenu>(menuKey));
|
||||
}
|
||||
food = join(getSelectedFoodNames(weekMenuCache.get(menuKey), getDayOfWeekIndex(date), locationKey as Restaurant, userChoice.selectedFoods), ' + ');
|
||||
}
|
||||
choiceRow = {
|
||||
date: isoDate,
|
||||
dayOfWeek,
|
||||
// Fallback na klíč — v datech se mohou vyskytnout i historické/neznámé volby
|
||||
type: CHOICE_NAMES[locationKey] ?? locationKey,
|
||||
food,
|
||||
note,
|
||||
amount,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// Objednávkové skupiny, ve kterých byl uživatel členem
|
||||
const groups = (extraData?.groups ?? []).filter(group => !!group.members[login]);
|
||||
const groupRows: UserExportRow[] = [];
|
||||
let mergedGroup = false;
|
||||
for (const group of groups) {
|
||||
const member = group.members[login];
|
||||
const orderInfo = {
|
||||
store: group.name,
|
||||
orderedBy: group.creatorLogin,
|
||||
amount: isActiveMember(member) ? computeMemberAmount(group, login) : undefined,
|
||||
};
|
||||
// První objednávku sloučíme s volbou "Budu objednávat", pokud ji uživatel má
|
||||
if (!mergedGroup && choiceRow && choice === LunchChoice.OBJEDNAVAM) {
|
||||
choiceRow.food = join([choiceRow.food, getOrderItemText(member)], ' + ');
|
||||
Object.assign(choiceRow, orderInfo);
|
||||
mergedGroup = true;
|
||||
continue;
|
||||
}
|
||||
groupRows.push({
|
||||
date: isoDate,
|
||||
dayOfWeek,
|
||||
type: ORDER_ONLY_LABEL,
|
||||
food: getOrderItemText(member),
|
||||
...orderInfo,
|
||||
});
|
||||
}
|
||||
|
||||
// V rámci dne je první volba oběda, až za ní případné samostatné objednávky
|
||||
if (choiceRow) {
|
||||
rows.push(choiceRow);
|
||||
}
|
||||
rows.push(...groupRows);
|
||||
}
|
||||
|
||||
return rows.sort((a, b) => a.date.localeCompare(b.date));
|
||||
}
|
||||
|
||||
const MONEY_FORMAT = '#,##0.00 "Kč"';
|
||||
|
||||
/** Převede částku v haléřích na koruny pro zápis do XLSX. */
|
||||
function toCrowns(amount?: number): number | undefined {
|
||||
return amount == null ? undefined : amount / 100;
|
||||
}
|
||||
|
||||
/** Podporované formáty exportu. */
|
||||
export const EXPORT_FORMATS = ['xlsx', 'csv', 'json'] as const;
|
||||
|
||||
export type ExportFormat = typeof EXPORT_FORMATS[number];
|
||||
|
||||
/** MIME typy jednotlivých formátů. */
|
||||
const MIME_TYPES: Record<ExportFormat, string> = {
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
csv: 'text/csv; charset=utf-8',
|
||||
json: 'application/json; charset=utf-8',
|
||||
};
|
||||
|
||||
/** Vrátí true, pokud je předaná hodnota podporovaným formátem exportu. */
|
||||
export function isExportFormat(value: unknown): value is ExportFormat {
|
||||
return typeof value === 'string' && (EXPORT_FORMATS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Hlavičky sloupců přehledu (společné pro XLSX i CSV). */
|
||||
const COLUMNS: { header: string, key: keyof UserExportRow, width: number }[] = [
|
||||
{ header: 'Datum', key: 'date', width: 12 },
|
||||
{ header: 'Den', key: 'dayOfWeek', width: 12 },
|
||||
{ header: 'Typ', key: 'type', width: 22 },
|
||||
{ header: 'Vybrané jídlo', key: 'food', width: 45 },
|
||||
{ header: 'Poznámka', key: 'note', width: 30 },
|
||||
{ header: 'Objednávka', key: 'store', width: 20 },
|
||||
{ header: 'Objednával', key: 'orderedBy', width: 16 },
|
||||
{ header: 'Částka', key: 'amount', width: 14 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Vygeneruje CSV přehled stravování uživatele.
|
||||
*
|
||||
* Formát je uzpůsobený českému Excelu: oddělovač ';', desetinná čárka a UTF-8 BOM,
|
||||
* aby se soubor otevřel se správnou diakritikou i bez ručního nastavování importu.
|
||||
*/
|
||||
export function buildCsv(rows: UserExportRow[]): string {
|
||||
const SEPARATOR = ';';
|
||||
const escape = (value: string) => `"${value.replace(/"/g, '""')}"`;
|
||||
const lines = [COLUMNS.map(column => escape(column.header)).join(SEPARATOR)];
|
||||
for (const row of rows) {
|
||||
const values = COLUMNS.map(column => {
|
||||
if (column.key === 'date') {
|
||||
const [y, m, d] = row.date.split('-');
|
||||
return escape(`${d}.${m}.${y}`);
|
||||
}
|
||||
if (column.key === 'amount') {
|
||||
const crowns = toCrowns(row.amount);
|
||||
// Desetinná čárka — český Excel jinak částku načte jako text
|
||||
return crowns == null ? '' : escape(crowns.toFixed(2).replace('.', ','));
|
||||
}
|
||||
const value = row[column.key];
|
||||
return value == null ? '' : escape(String(value));
|
||||
});
|
||||
lines.push(values.join(SEPARATOR));
|
||||
}
|
||||
// BOM kvůli správnému rozpoznání UTF-8 v Excelu
|
||||
return `${lines.join('\r\n')}\r\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vygeneruje JSON přehled stravování uživatele.
|
||||
* Částky jsou v korunách (na rozdíl od interní reprezentace v haléřích).
|
||||
*/
|
||||
export function buildJson(login: string, year: number, month: number, rows: UserExportRow[]): UserExport {
|
||||
return {
|
||||
login,
|
||||
year,
|
||||
month,
|
||||
rowCount: rows.length,
|
||||
totalAmount: toCrowns(rows.reduce((sum, row) => sum + (row.amount ?? 0), 0))!,
|
||||
rows: rows.map(row => ({
|
||||
date: row.date,
|
||||
dayOfWeek: row.dayOfWeek,
|
||||
type: row.type,
|
||||
...(row.food ? { food: row.food } : {}),
|
||||
...(row.note ? { note: row.note } : {}),
|
||||
...(row.store ? { store: row.store } : {}),
|
||||
...(row.orderedBy ? { orderedBy: row.orderedBy } : {}),
|
||||
...(row.amount != null ? { amount: toCrowns(row.amount) } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vygeneruje přehled stravování uživatele za vybraný měsíc ve vyžádaném formátu.
|
||||
*
|
||||
* @param login přihlašovací jméno uživatele
|
||||
* @param year rok
|
||||
* @param month měsíc (1 = leden)
|
||||
* @param format formát exportu (výchozí xlsx)
|
||||
* @returns obsah souboru, jeho MIME typ a název
|
||||
*/
|
||||
export async function generateUserExport(login: string, year: number, month: number, format: ExportFormat = 'xlsx'): Promise<{ content: Buffer, mimeType: string, fileName: string }> {
|
||||
const rows = await getUserExportRows(login, year, month);
|
||||
const result = {
|
||||
mimeType: MIME_TYPES[format],
|
||||
fileName: getUserExportFileName(login, year, month, format),
|
||||
};
|
||||
if (format === 'csv') {
|
||||
return { ...result, content: Buffer.from(buildCsv(rows), 'utf-8') };
|
||||
}
|
||||
if (format === 'json') {
|
||||
return { ...result, content: Buffer.from(JSON.stringify(buildJson(login, year, month, rows), null, 2), 'utf-8') };
|
||||
}
|
||||
return { ...result, content: await buildXlsx(login, year, month, rows) };
|
||||
}
|
||||
|
||||
/** Vygeneruje XLSX podobu přehledu. */
|
||||
async function buildXlsx(login: string, year: number, month: number, rows: UserExportRow[]): Promise<Buffer> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = 'Luncher';
|
||||
const sheet = workbook.addWorksheet('Přehled');
|
||||
sheet.columns = COLUMNS.map(column => ({
|
||||
...column,
|
||||
...(column.key === 'date' ? { style: { numFmt: 'dd.mm.yyyy' } } : {}),
|
||||
...(column.key === 'amount' ? { style: { numFmt: MONEY_FORMAT } } : {}),
|
||||
}));
|
||||
sheet.getRow(1).font = { bold: true };
|
||||
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||
|
||||
for (const row of rows) {
|
||||
const [y, m, d] = row.date.split('-').map(Number);
|
||||
sheet.addRow({
|
||||
...row,
|
||||
// UTC, aby Excel nezobrazil o den dřív vlivem časové zóny
|
||||
date: new Date(Date.UTC(y, m - 1, d)),
|
||||
amount: toCrowns(row.amount),
|
||||
});
|
||||
}
|
||||
sheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: sheet.columns.length } };
|
||||
|
||||
// Souhrn: počty záznamů dle typu a celková zaplacená částka
|
||||
const summary = workbook.addWorksheet('Souhrn');
|
||||
summary.columns = [
|
||||
{ header: 'Typ', key: 'type', width: 24 },
|
||||
{ header: 'Počet záznamů', key: 'count', width: 15 },
|
||||
{ header: 'Částka celkem', key: 'amount', width: 16, style: { numFmt: MONEY_FORMAT } },
|
||||
];
|
||||
summary.getRow(1).font = { bold: true };
|
||||
const perType = new Map<string, { count: number, amount: number }>();
|
||||
for (const row of rows) {
|
||||
const entry = perType.get(row.type) ?? { count: 0, amount: 0 };
|
||||
entry.count++;
|
||||
entry.amount += row.amount ?? 0;
|
||||
perType.set(row.type, entry);
|
||||
}
|
||||
for (const [type, entry] of perType) {
|
||||
summary.addRow({ type, count: entry.count, amount: toCrowns(entry.amount) });
|
||||
}
|
||||
const totalAmount = rows.reduce((sum, row) => sum + (row.amount ?? 0), 0);
|
||||
const totalRow = summary.addRow({ type: 'Celkem', count: rows.length, amount: toCrowns(totalAmount) });
|
||||
totalRow.font = { bold: true };
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
|
||||
/** Vrátí název souboru přehledu pro daného uživatele, měsíc a formát. */
|
||||
export function getUserExportFileName(login: string, year: number, month: number, format: ExportFormat = 'xlsx'): string {
|
||||
const safeLogin = login.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
return `luncher-${safeLogin}-${year}-${String(month).padStart(2, '0')}.${format}`;
|
||||
}
|
||||
+490
-5
@@ -992,6 +992,31 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@fast-csv/format@4.3.5":
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@fast-csv/format/-/format-4.3.5.tgz#90d83d1b47b6aaf67be70d6118f84f3e12ee1ff3"
|
||||
integrity sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==
|
||||
dependencies:
|
||||
"@types/node" "^14.0.1"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isboolean "^3.0.3"
|
||||
lodash.isequal "^4.5.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isnil "^4.0.0"
|
||||
|
||||
"@fast-csv/parse@4.3.6":
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@fast-csv/parse/-/parse-4.3.6.tgz#ee47d0640ca0291034c7aa94039a744cfb019264"
|
||||
integrity sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==
|
||||
dependencies:
|
||||
"@types/node" "^14.0.1"
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.groupby "^4.6.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isnil "^4.0.0"
|
||||
lodash.isundefined "^3.0.1"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
@@ -1711,6 +1736,11 @@
|
||||
dependencies:
|
||||
undici-types "~7.16.0"
|
||||
|
||||
"@types/node@^14.0.1":
|
||||
version "14.18.63"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
|
||||
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==
|
||||
|
||||
"@types/node@^24.10.0":
|
||||
version "24.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16"
|
||||
@@ -2009,6 +2039,51 @@ anymatch@^3.1.3, anymatch@~3.1.2:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
archiver-utils@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
|
||||
integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==
|
||||
dependencies:
|
||||
glob "^7.1.4"
|
||||
graceful-fs "^4.2.0"
|
||||
lazystream "^1.0.0"
|
||||
lodash.defaults "^4.2.0"
|
||||
lodash.difference "^4.5.0"
|
||||
lodash.flatten "^4.4.0"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.union "^4.6.0"
|
||||
normalize-path "^3.0.0"
|
||||
readable-stream "^2.0.0"
|
||||
|
||||
archiver-utils@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7"
|
||||
integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==
|
||||
dependencies:
|
||||
glob "^7.2.3"
|
||||
graceful-fs "^4.2.0"
|
||||
lazystream "^1.0.0"
|
||||
lodash.defaults "^4.2.0"
|
||||
lodash.difference "^4.5.0"
|
||||
lodash.flatten "^4.4.0"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.union "^4.6.0"
|
||||
normalize-path "^3.0.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
archiver@^5.0.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0"
|
||||
integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==
|
||||
dependencies:
|
||||
archiver-utils "^2.1.0"
|
||||
async "^3.2.4"
|
||||
buffer-crc32 "^0.2.1"
|
||||
readable-stream "^3.6.0"
|
||||
readdir-glob "^1.1.2"
|
||||
tar-stream "^2.2.0"
|
||||
zip-stream "^4.1.0"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
@@ -2041,6 +2116,11 @@ astring@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef"
|
||||
integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==
|
||||
|
||||
async@^3.2.4:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
||||
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -2150,6 +2230,11 @@ balanced-match@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
base64id@2.0.0, base64id@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
|
||||
@@ -2160,11 +2245,38 @@ baseline-browser-mapping@^2.10.42:
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz#7b5d11590ce5acdbe4859443e3c940e81ce8c02d"
|
||||
integrity sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==
|
||||
|
||||
big-integer@^1.6.17:
|
||||
version "1.6.52"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85"
|
||||
integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
binary@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
|
||||
integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==
|
||||
dependencies:
|
||||
buffers "~0.1.1"
|
||||
chainsaw "~0.1.0"
|
||||
|
||||
bl@^4.0.3:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||
dependencies:
|
||||
buffer "^5.5.0"
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
bluebird@~3.4.1:
|
||||
version "3.4.7"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
|
||||
integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==
|
||||
|
||||
bn.js@^4.0.0:
|
||||
version "4.12.3"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.3.tgz#2cc2c679188eb35b006f2d0d4710bed8437a769e"
|
||||
@@ -2198,6 +2310,13 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326"
|
||||
integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
brace-expansion@^2.0.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.2.tgz#0bba2271feb7d458b0d31ad13625aaa4754431e2"
|
||||
@@ -2237,6 +2356,11 @@ bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64 "^0.4.0"
|
||||
|
||||
buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
buffer-equal-constant-time@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
@@ -2247,6 +2371,24 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
buffer-indexof-polyfill@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c"
|
||||
integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==
|
||||
|
||||
buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffers@~0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
|
||||
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
|
||||
|
||||
bytes@^3.1.2, bytes@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
@@ -2288,6 +2430,13 @@ caniuse-lite@^1.0.30001803:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
|
||||
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
|
||||
|
||||
chainsaw@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
|
||||
integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==
|
||||
dependencies:
|
||||
traverse ">=0.3.0 <0.4"
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
@@ -2403,6 +2552,16 @@ component-emitter@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
|
||||
integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
|
||||
|
||||
compress-commons@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df"
|
||||
integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==
|
||||
dependencies:
|
||||
buffer-crc32 "^0.2.13"
|
||||
crc32-stream "^4.0.2"
|
||||
normalize-path "^3.0.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
@@ -2445,6 +2604,11 @@ core-js-compat@^3.48.0:
|
||||
dependencies:
|
||||
browserslist "^4.28.1"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.6"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96"
|
||||
@@ -2461,6 +2625,19 @@ cors@~2.8.5:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
crc-32@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
|
||||
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
|
||||
|
||||
crc32-stream@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33"
|
||||
integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==
|
||||
dependencies:
|
||||
crc-32 "^1.2.0"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
@@ -2491,6 +2668,11 @@ css-what@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea"
|
||||
integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==
|
||||
|
||||
dayjs@^1.8.34:
|
||||
version "1.11.21"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2"
|
||||
integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==
|
||||
|
||||
debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.5, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3, debug@~4.4.1:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||
@@ -2587,6 +2769,13 @@ dunder-proto@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
duplexer2@~0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
||||
integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==
|
||||
dependencies:
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
@@ -2637,6 +2826,13 @@ encoding-sniffer@^0.2.1:
|
||||
iconv-lite "^0.6.3"
|
||||
whatwg-encoding "^3.1.1"
|
||||
|
||||
end-of-stream@^1.4.1:
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
|
||||
integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
engine.io-parser@~5.2.1:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
|
||||
@@ -2753,6 +2949,21 @@ etag@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
exceljs@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/exceljs/-/exceljs-4.4.0.tgz#cfb1cb8dcc82c760a9fc9faa9e52dadab66b0156"
|
||||
integrity sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==
|
||||
dependencies:
|
||||
archiver "^5.0.0"
|
||||
dayjs "^1.8.34"
|
||||
fast-csv "^4.3.1"
|
||||
jszip "^3.10.1"
|
||||
readable-stream "^3.6.0"
|
||||
saxes "^5.0.1"
|
||||
tmp "^0.2.0"
|
||||
unzipper "^0.10.11"
|
||||
uuid "^8.3.0"
|
||||
|
||||
execa@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
@@ -2831,6 +3042,14 @@ express@^5.1.0:
|
||||
type-is "^2.0.1"
|
||||
vary "^1.1.2"
|
||||
|
||||
fast-csv@^4.3.1:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-csv/-/fast-csv-4.3.6.tgz#70349bdd8fe4d66b1130d8c91820b64a21bc4a63"
|
||||
integrity sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==
|
||||
dependencies:
|
||||
"@fast-csv/format" "4.3.5"
|
||||
"@fast-csv/parse" "4.3.6"
|
||||
|
||||
fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
@@ -2928,6 +3147,11 @@ fresh@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
|
||||
integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
@@ -2938,6 +3162,16 @@ fsevents@^2.3.3, fsevents@~2.3.2:
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
fstream@^1.0.12:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
|
||||
integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
inherits "~2.0.0"
|
||||
mkdirp ">=0.5 0"
|
||||
rimraf "2"
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
@@ -3006,7 +3240,7 @@ glob@^10.5.0:
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^7.1.4:
|
||||
glob@^7.1.3, glob@^7.1.4, glob@^7.2.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@@ -3023,7 +3257,7 @@ gopd@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
graceful-fs@^4.2.11:
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
@@ -3123,11 +3357,21 @@ iconv-lite@^0.7.0, iconv-lite@~0.7.0:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ignore-by-default@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
|
||||
|
||||
import-in-the-middle@^3.0.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz#4fe3b46cc1b4573b00bc69343e5f9bbf25679743"
|
||||
@@ -3158,7 +3402,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.1, inherits@~2.0.4:
|
||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -3224,6 +3468,11 @@ is-stream@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
||||
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@@ -3741,6 +3990,16 @@ jsonwebtoken@^9.0.0:
|
||||
ms "^2.1.1"
|
||||
semver "^7.5.4"
|
||||
|
||||
jszip@^3.10.1:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
|
||||
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
|
||||
dependencies:
|
||||
lie "~3.3.0"
|
||||
pako "~1.0.2"
|
||||
readable-stream "~2.3.6"
|
||||
setimmediate "^1.0.5"
|
||||
|
||||
jwa@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
|
||||
@@ -3758,16 +4017,35 @@ jws@^4.0.0, jws@^4.0.1:
|
||||
jwa "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
lazystream@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
|
||||
integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==
|
||||
dependencies:
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
leven@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
|
||||
|
||||
lie@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
listenercount@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
|
||||
integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==
|
||||
|
||||
locate-path@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
|
||||
@@ -3780,6 +4058,31 @@ lodash.debounce@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||
|
||||
lodash.defaults@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
||||
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
|
||||
|
||||
lodash.difference@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
|
||||
integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==
|
||||
|
||||
lodash.escaperegexp@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
|
||||
integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==
|
||||
|
||||
lodash.flatten@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
|
||||
|
||||
lodash.groupby@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1"
|
||||
integrity sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
@@ -3790,11 +4093,26 @@ lodash.isboolean@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
|
||||
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.isfunction@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
|
||||
|
||||
lodash.isinteger@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
|
||||
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
|
||||
|
||||
lodash.isnil@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c"
|
||||
integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==
|
||||
|
||||
lodash.isnumber@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
|
||||
@@ -3810,11 +4128,26 @@ lodash.isstring@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
|
||||
|
||||
lodash.isundefined@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
|
||||
integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==
|
||||
|
||||
lodash.once@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
|
||||
|
||||
lodash.union@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
|
||||
integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
@@ -3944,6 +4277,13 @@ minimatch@^3.0.4, minimatch@^3.1.1:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.1.0:
|
||||
version "5.1.9"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b"
|
||||
integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.9"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
|
||||
@@ -3951,7 +4291,7 @@ minimatch@^9.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.2"
|
||||
|
||||
minimist@^1.2.5:
|
||||
minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@@ -3961,6 +4301,13 @@ minimist@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
|
||||
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
|
||||
|
||||
"mkdirp@>=0.5 0":
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
module-details-from-path@^1.0.3, module-details-from-path@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94"
|
||||
@@ -4103,6 +4450,11 @@ package-json-from-dist@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
|
||||
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
|
||||
|
||||
pako@~1.0.2:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
||||
parse-json@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
|
||||
@@ -4219,6 +4571,11 @@ pretty-format@30.4.1:
|
||||
react-is-18 "npm:react-is@^18.3.1"
|
||||
react-is-19 "npm:react-is@^19.2.5"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
proxy-addr@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
@@ -4279,6 +4636,35 @@ react-is@^18.3.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
|
||||
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readdir-glob@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584"
|
||||
integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==
|
||||
dependencies:
|
||||
minimatch "^5.1.0"
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
@@ -4368,6 +4754,13 @@ resolve@^1.22.11:
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
rimraf@2:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
router@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef"
|
||||
@@ -4379,16 +4772,28 @@ router@^2.2.0:
|
||||
parseurl "^1.3.3"
|
||||
path-to-regexp "^8.0.0"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.2.1:
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
saxes@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
|
||||
integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
|
||||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
semifies@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semifies/-/semifies-1.0.0.tgz#b69569f32c2ba2ac04f705ea82831364289b2ae2"
|
||||
@@ -4431,6 +4836,11 @@ serve-static@^2.2.0:
|
||||
parseurl "^1.3.3"
|
||||
send "^1.2.0"
|
||||
|
||||
setimmediate@^1.0.5, setimmediate@~1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
|
||||
|
||||
setprototypeof@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
@@ -4609,6 +5019,20 @@ string-width@^5.0.1, string-width@^5.1.2:
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
@@ -4702,6 +5126,17 @@ synckit@^0.11.8:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.3.6"
|
||||
|
||||
tar-stream@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
||||
@@ -4711,6 +5146,11 @@ test-exclude@^6.0.0:
|
||||
glob "^7.1.4"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
tmp@^0.2.0:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059"
|
||||
integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==
|
||||
|
||||
tmpl@1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||
@@ -4733,6 +5173,11 @@ touch@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
|
||||
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
|
||||
|
||||
"traverse@>=0.3.0 <0.4":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
|
||||
integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==
|
||||
|
||||
ts-node@^10.9.1:
|
||||
version "10.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||
@@ -4869,6 +5314,22 @@ unrs-resolver@^1.7.11:
|
||||
"@unrs/resolver-binding-win32-ia32-msvc" "1.12.2"
|
||||
"@unrs/resolver-binding-win32-x64-msvc" "1.12.2"
|
||||
|
||||
unzipper@^0.10.11:
|
||||
version "0.10.14"
|
||||
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1"
|
||||
integrity sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==
|
||||
dependencies:
|
||||
big-integer "^1.6.17"
|
||||
binary "~0.3.0"
|
||||
bluebird "~3.4.1"
|
||||
buffer-indexof-polyfill "~1.0.0"
|
||||
duplexer2 "~0.1.4"
|
||||
fstream "^1.0.12"
|
||||
graceful-fs "^4.2.2"
|
||||
listenercount "~1.0.1"
|
||||
readable-stream "~2.3.6"
|
||||
setimmediate "~1.0.4"
|
||||
|
||||
update-browserslist-db@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
|
||||
@@ -4877,6 +5338,16 @@ update-browserslist-db@^1.2.3:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
uuid@^8.3.0:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
@@ -4978,6 +5449,11 @@ ws@~8.18.3:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
|
||||
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
@@ -5015,3 +5491,12 @@ yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zip-stream@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135"
|
||||
integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==
|
||||
dependencies:
|
||||
archiver-utils "^3.0.4"
|
||||
compress-commons "^4.1.2"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
@@ -70,6 +70,8 @@ paths:
|
||||
# Statistiky (/api/stats)
|
||||
/stats:
|
||||
$ref: "./paths/stats/stats.yml"
|
||||
/stats/export:
|
||||
$ref: "./paths/stats/export.yml"
|
||||
|
||||
# Návrhy na vylepšení (/api/suggestions)
|
||||
/suggestions/list:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
get:
|
||||
operationId: getStatsExport
|
||||
summary: Vrátí přehled stravování přihlášeného uživatele za vybraný měsíc (XLSX, CSV nebo JSON).
|
||||
parameters:
|
||||
- in: query
|
||||
name: year
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: Rok, za který přehled vygenerovat
|
||||
- in: query
|
||||
name: month
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 12
|
||||
description: Měsíc (1 = leden), za který přehled vygenerovat
|
||||
- in: query
|
||||
name: format
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [xlsx, csv, json]
|
||||
default: xlsx
|
||||
description: Formát exportu. Výchozí je xlsx.
|
||||
responses:
|
||||
"200":
|
||||
description: Přehled stravování uživatele ve vyžádaném formátu
|
||||
content:
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
text/csv:
|
||||
schema:
|
||||
type: string
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "../../schemas/_index.yml#/UserExport"
|
||||
"400":
|
||||
description: Neplatné parametry
|
||||
"401":
|
||||
description: Neautentizovaný uživatel
|
||||
@@ -406,6 +406,72 @@ WeeklyStats:
|
||||
maxItems: 5
|
||||
items:
|
||||
$ref: "#/DailyStats"
|
||||
UserExportRow:
|
||||
description: Jeden záznam přehledu stravování uživatele (jeden den a typ záznamu)
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- date
|
||||
- dayOfWeek
|
||||
- type
|
||||
properties:
|
||||
date:
|
||||
description: Datum záznamu ve formátu YYYY-MM-DD
|
||||
type: string
|
||||
format: date
|
||||
dayOfWeek:
|
||||
description: Den v týdnu (pondělí, ...)
|
||||
type: string
|
||||
type:
|
||||
description: Typ záznamu — název podniku/volby, nebo "Objednávka" u objednávky bez volby oběda
|
||||
type: string
|
||||
food:
|
||||
description: Vybrané jídlo (jídla z menu, pizzy, položky objednávky)
|
||||
type: string
|
||||
note:
|
||||
description: Poznámka uživatele k záznamu
|
||||
type: string
|
||||
store:
|
||||
description: Obchod/restaurace objednávkové skupiny
|
||||
type: string
|
||||
orderedBy:
|
||||
description: Login objednávajícího (zakladatele objednávkové skupiny)
|
||||
type: string
|
||||
amount:
|
||||
description: Částka k úhradě po slevě v korunách
|
||||
type: number
|
||||
UserExport:
|
||||
description: Přehled stravování jednoho uživatele za vybraný měsíc
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- login
|
||||
- year
|
||||
- month
|
||||
- rowCount
|
||||
- totalAmount
|
||||
- rows
|
||||
properties:
|
||||
login:
|
||||
description: Přihlašovací jméno uživatele, kterému přehled patří
|
||||
type: string
|
||||
year:
|
||||
description: Rok přehledu
|
||||
type: integer
|
||||
month:
|
||||
description: Měsíc přehledu (1 = leden)
|
||||
type: integer
|
||||
rowCount:
|
||||
description: Počet záznamů v přehledu
|
||||
type: integer
|
||||
totalAmount:
|
||||
description: Součet částek všech záznamů v korunách
|
||||
type: number
|
||||
rows:
|
||||
description: Jednotlivé záznamy přehledu seřazené dle data
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/UserExportRow"
|
||||
|
||||
# --- PIZZA DAY ---
|
||||
PizzaDayState:
|
||||
|
||||
Reference in New Issue
Block a user