Compare commits
3 Commits
d63f096aaa
...
888e35f839
| Author | SHA1 | Date |
|---|---|---|
|
|
888e35f839 | |
|
|
e18adab888 | |
|
|
d77bbb317d |
|
|
@ -0,0 +1,16 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
databases
|
||||||
|
docs
|
||||||
|
documents
|
||||||
|
pdfgen
|
||||||
|
!pdfgen/template-*
|
||||||
|
scripts
|
||||||
|
tmp-user-data
|
||||||
|
user-data
|
||||||
|
|
@ -33,3 +33,5 @@ pdfgen/*
|
||||||
!pdfgen/template*.tex
|
!pdfgen/template*.tex
|
||||||
|
|
||||||
documents/*
|
documents/*
|
||||||
|
user-data/*
|
||||||
|
tmp-user-data/*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
FROM oven/bun:alpine AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
ENV APP_USER_DATA_PATH="/app-data/user-data"
|
||||||
|
ENV APP_TMP_USER_DATA_PATH="/app-data/tmp"
|
||||||
|
|
||||||
|
## Stage 1: Install dependencies
|
||||||
|
FROM base AS install
|
||||||
|
|
||||||
|
# install development dependencies
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lockb /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# install production dependencies
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lockb /temp/prod/
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
## Stage 2: Build app
|
||||||
|
FROM base AS prerelease
|
||||||
|
|
||||||
|
# copy dependencies into workdir
|
||||||
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
|
|
||||||
|
# add project files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# build project
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
## Stage 3: Put everything together
|
||||||
|
FROM base AS release
|
||||||
|
|
||||||
|
# compose the final image
|
||||||
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/build .
|
||||||
|
#COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
|
||||||
|
RUN mkdir /app-data && chown bun:bun /app-data
|
||||||
|
|
||||||
|
VOLUME ["/app-data"]
|
||||||
|
|
||||||
|
USER bun
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
ENTRYPOINT [ "bun", "--bun", "run", "start" ]
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
services:
|
||||||
|
application:
|
||||||
|
build: .
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: Stundenaufzeichnung
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
user-data:
|
||||||
|
|
||||||
|
services:
|
||||||
|
application:
|
||||||
|
build: https://git.maschek.info/patrick/stundenaufzeichnung.git#main
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: user-data
|
||||||
|
target: /app-data
|
||||||
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"@sveltejs/kit": "^2.20.4",
|
"@sveltejs/kit": "^2.20.4",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
|
"@types/bun": "^1.2.10",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
"svelte": "^5.25.6",
|
"svelte": "^5.25.6",
|
||||||
"svelte-adapter-bun": "^0.5.2",
|
"svelte-adapter-bun": "^0.5.2",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
|
||||||
|
import type { User } from "$lib/server/database";
|
||||||
|
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
|
import type { Handle } from "@sveltejs/kit";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
import { User, UserDB, init_db, close_db, create_user, get_user, get_user_db } from "$lib/server/database";
|
import { init_db, close_db, get_user } from "$lib/server/database";
|
||||||
|
|
||||||
import { Database } from "bun:sqlite";
|
async function init() {
|
||||||
|
|
||||||
function init() {
|
if (process.env.APP_USER_DATA_PATH == null) {
|
||||||
|
console.log("APP_USER_DATA_PATH is not defined. Exiting.");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.APP_TMP_USER_DATA_PATH == null) {
|
||||||
|
console.log("APP_TMP_USER_DATA_PATH is not defined. Exiting.");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
init_db();
|
await init_db();
|
||||||
|
|
||||||
console.log("started");
|
console.log("started");
|
||||||
|
|
||||||
|
|
@ -16,24 +26,27 @@ function deinit() {
|
||||||
console.log('exit');
|
console.log('exit');
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
await init();
|
||||||
|
|
||||||
process.on('exit', (reason) => {
|
process.on('exit', (_) => {
|
||||||
deinit();
|
deinit();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', (reason) => {
|
process.on('SIGINT', (_) => {
|
||||||
console.log("SIGINT")
|
console.log("SIGINT")
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
})
|
})
|
||||||
|
|
||||||
import { getAllFiles } from "$lib/server/docstore"
|
export let handle: Handle = async function ({ event, resolve }) {
|
||||||
|
|
||||||
export async function handle({ event, resolve }) {
|
|
||||||
|
|
||||||
event.locals.user = get_user();
|
let user = get_user();
|
||||||
console.log("handle");
|
|
||||||
|
if (!user) {
|
||||||
|
return error(404, "No user"); // redirect login
|
||||||
|
}
|
||||||
|
|
||||||
|
event.locals.user = user;
|
||||||
|
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
|
|
||||||
import { Database } from 'bun:sqlite';
|
|
||||||
|
|
||||||
export interface UserEntry {
|
export interface UserEntry {
|
||||||
id: number;
|
id: number;
|
||||||
gender: string;
|
gender: string;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { mkdir } from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { Database, SQLiteError } from "bun:sqlite";
|
import { Database, SQLiteError } from "bun:sqlite";
|
||||||
|
|
||||||
import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
|
import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
|
||||||
import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util";
|
import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util";
|
||||||
|
|
||||||
const DATABASES_PATH: string = "./databases/";
|
const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/";
|
||||||
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
||||||
|
|
||||||
const CHECK_QUERY: string =
|
const CHECK_QUERY: string =
|
||||||
|
|
@ -291,7 +291,7 @@ function is_db_initialized(db: Database): boolean {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(e);
|
console.log(exception);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -305,8 +305,10 @@ function setup_db(db: Database, setup_queries: string[]) {
|
||||||
setup_queries.forEach((q) => { /*console.log(q);*/ db.query(q).run(); });
|
setup_queries.forEach((q) => { /*console.log(q);*/ db.query(q).run(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init_db() {
|
export async function init_db() {
|
||||||
|
|
||||||
|
const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true });
|
||||||
|
console.log(stdout)
|
||||||
user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true });
|
user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true });
|
||||||
|
|
||||||
if (!is_db_initialized(user_database)) {
|
if (!is_db_initialized(user_database)) {
|
||||||
|
|
@ -366,6 +368,8 @@ export function get_user(): User | null {
|
||||||
const db_name = get_user_db_name(user);
|
const db_name = get_user_db_name(user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
fs.mkdir(DATABASES_PATH, { recursive: true });
|
||||||
|
|
||||||
let userdb = new Database(db_name, { create: true, strict: true });
|
let userdb = new Database(db_name, { create: true, strict: true });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
import b from "bun";
|
import b from "bun";
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
|
|
||||||
import type { RecordEntry } from "$lib/db_types";
|
|
||||||
import { User } from "$lib/server/database";
|
import { User } from "$lib/server/database";
|
||||||
import { MONTHS, toInt, padInt, parseDate, calculateDuration, isoToLocalDate, month_of, weekday_of } from "$lib/util"
|
import { MONTHS, toInt, padInt, parseDate, calculateDuration, isoToLocalDate, month_of, weekday_of } from "$lib/util"
|
||||||
|
|
||||||
|
|
||||||
export class UserBusyException extends Error {
|
export class UserBusyException extends Error {
|
||||||
constructor(message, userid) {
|
userid: number
|
||||||
super(`user (id: ${userid}) is busy: ${message}`);
|
|
||||||
this.name = this.constructor.name;
|
constructor(message: string, userid: number) {
|
||||||
this.userid = userid;
|
super(`user (id: ${userid}) is busy: ${message}`);
|
||||||
}
|
this.name = this.constructor.name;
|
||||||
|
this.userid = userid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LatexException extends Error {
|
export class LatexException extends Error {
|
||||||
constructor(message, userid, errno, stdout) {
|
errno: number
|
||||||
|
stdout: string
|
||||||
|
|
||||||
|
constructor(message: string, userid: number, errno: number, stdout: string) {
|
||||||
super(message + ` (userid: ${userid}, errno: ${errno})\n${stdout}`);
|
super(message + ` (userid: ${userid}, errno: ${errno})\n${stdout}`);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.errno = errno;
|
this.errno = errno;
|
||||||
|
|
@ -24,107 +28,129 @@ export class LatexException extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileExistsException extends Error {
|
export class FileExistsException extends Error {
|
||||||
constructor(path) {
|
path: string
|
||||||
|
|
||||||
|
constructor(path: string) {
|
||||||
super("File already exists: " + path);
|
super("File already exists: " + path);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileProperties {
|
||||||
|
identifier: string
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
cdate: Date
|
||||||
|
}
|
||||||
|
|
||||||
const DOCUMENTS_PATH: string = "./documents"
|
|
||||||
|
const DOCUMENTS_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/documents"
|
||||||
const ESTIMATES_FOLD: string = "estimates"
|
const ESTIMATES_FOLD: string = "estimates"
|
||||||
const RECORDS_FOLD: string = "records"
|
const RECORDS_FOLD: string = "records"
|
||||||
|
|
||||||
const GENERATION_PATH: string = "./pdfgen";
|
const GENERATION_PATH: string = (process.env.APP_TMP_USER_DATA_PATH ?? ".") + "/pdfgen";
|
||||||
|
|
||||||
const TEMPLATE_REC = "template-rec.tex";
|
const TEMPLATE_REC = "template-rec.tex";
|
||||||
const TEMPLATE_REC_PATH = `${GENERATION_PATH}/${TEMPLATE_REC}`;
|
const TEMPLATE_REC_PATH = `./templates/${TEMPLATE_REC}`;
|
||||||
|
|
||||||
const TEMPLATE_EST = "template-est.tex";
|
const TEMPLATE_EST = "template-est.tex";
|
||||||
const TEMPLATE_EST_PATH = `${GENERATION_PATH}/${TEMPLATE_EST}`;
|
const TEMPLATE_EST_PATH = `./templates/${TEMPLATE_EST}`;
|
||||||
|
|
||||||
const SALUTATION = {
|
const SALUTATION: Record<string, string> = {
|
||||||
m: "Herrn",
|
m: "Herrn",
|
||||||
w: "Frau"
|
w: "Frau"
|
||||||
}
|
}
|
||||||
|
|
||||||
const GENDER_END = {
|
const GENDER_END: Record<string, string> = {
|
||||||
m: "",
|
m: "",
|
||||||
w: "in",
|
w: "in",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
const SALUTATION: Map<string, string> = new Map(Object.entries({
|
||||||
|
m: "Herrn",
|
||||||
|
w: "Frau"
|
||||||
|
}))
|
||||||
|
|
||||||
|
const GENDER_END: Map<string, string> = new Map(Object.entries({
|
||||||
|
"m": "",
|
||||||
|
"w": "in",
|
||||||
|
}))
|
||||||
|
*/
|
||||||
|
|
||||||
// this may be a race condition???
|
// this may be a race condition???
|
||||||
// is a mutex needed in JS?
|
// is a mutex needed in JS?
|
||||||
let locked_users: number[] = new Set();
|
let locked_users: Set<Number> = new Set();
|
||||||
|
|
||||||
export async function getAllFiles(user: User) {
|
export async function getAllFiles(user: User) {
|
||||||
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
||||||
|
|
||||||
let file_names = (await fs.readdir(path, { recursive: true }).catch((err) => {
|
let file_names = (await fs.readdir(path, { recursive: true }).catch((_) => {
|
||||||
return [];
|
return [];
|
||||||
})).filter((v) => {
|
})).filter((v) => {
|
||||||
return !(v == ESTIMATES_FOLD || v == RECORDS_FOLD);
|
return !(v == ESTIMATES_FOLD || v == RECORDS_FOLD);
|
||||||
});
|
});
|
||||||
|
|
||||||
let files = (await Promise.all(file_names.map(async (v) => {
|
let files: FileProperties[] = (await Promise.all(file_names.map(async (v) => {
|
||||||
return {
|
return {
|
||||||
|
identifier: v.split("/").pop(),
|
||||||
path: v,
|
path: v,
|
||||||
name: v.split("/").pop(),
|
name: v.split("/").pop(),
|
||||||
timestamp: (await fs.stat(`${path}/${v}`)).mtime
|
cdate: (await fs.stat(`${path}/${v}`)).ctime
|
||||||
}
|
} as FileProperties;
|
||||||
}))).sort((a, b) => {
|
}))).sort((a, b) => {
|
||||||
return a.timestamp - b.timestamp
|
return a.cdate.getTime() - b.cdate.getTime()
|
||||||
});
|
});
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRecordFiles(user: User) {
|
export async function getRecordFiles(user: User): Promise<FileProperties[]> {
|
||||||
const path = `${DOCUMENTS_PATH}/user-${user.id}/${RECORDS_FOLD}`;
|
const path = `${DOCUMENTS_PATH}/user-${user.id}/${RECORDS_FOLD}`;
|
||||||
|
|
||||||
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
||||||
|
|
||||||
let files = await Promise.all(file_names.map(async (v) => {
|
let files: FileProperties[] = await Promise.all(file_names.map(async (v) => {
|
||||||
return {
|
return {
|
||||||
identifier: v.replace(/^Stundenliste-/, "").replace(/\.pdf$/, ""),
|
identifier: v.replace(/^Stundenliste-/, "").replace(/\.pdf$/, ""),
|
||||||
filename: v,
|
name: v,
|
||||||
path: `${RECORDS_FOLD}/${v}`,
|
path: `${RECORDS_FOLD}/${v}`,
|
||||||
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
||||||
}
|
} as FileProperties;
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEstimateFiles(user: User) {
|
export async function getEstimateFiles(user: User): Promise<FileProperties[]> {
|
||||||
const path = `${DOCUMENTS_PATH}/user-${user.id}/${ESTIMATES_FOLD}`;
|
const path = `${DOCUMENTS_PATH}/user-${user.id}/${ESTIMATES_FOLD}`;
|
||||||
|
|
||||||
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
||||||
|
|
||||||
let files = await Promise.all(file_names.map(async (v) => {
|
let files: FileProperties[] = await Promise.all(file_names.map(async (v) => {
|
||||||
return {
|
return {
|
||||||
identifier: v.match(/\d.Quartal_\d\d\d\d/)[0].replace(/.Quartal_/, ".").split(".").reverse().join("-"),
|
identifier: v.match(/\d.Quartal_\d\d\d\d/)?.[0].replace(/.Quartal_/, ".").split(".").reverse().join("-") ?? "",
|
||||||
filename: v,
|
name: v,
|
||||||
path: `${ESTIMATES_FOLD}/${v}`,
|
path: `${ESTIMATES_FOLD}/${v}`,
|
||||||
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
||||||
}
|
} as FileProperties;
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFile(user: User, filename: string) {
|
export async function getFile(user: User, filename: string): Promise<Uint8Array> {
|
||||||
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
||||||
|
|
||||||
let file = Bun.file(`${path}/${filename}`)
|
let file = Bun.file(`${path}/${filename}`)
|
||||||
|
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
return null;
|
return new Promise(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await file.bytes();
|
return file.bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEstimatePDF(user: User, year: number, quarter: number) {
|
export async function generateEstimatePDF(user: User, year: number, quarter: number) {
|
||||||
|
|
@ -134,10 +160,10 @@ export async function generateRecordPDF(user: User, year: number, quarter: numbe
|
||||||
return await _runProtocted(_generateRecordPDF, user, year, quarter);
|
return await _runProtocted(_generateRecordPDF, user, year, quarter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _runProtocted(f, user: User, ...args) {
|
async function _runProtocted(f: Function, user: User, ...args: any[]) {
|
||||||
|
|
||||||
if (locked_users.has(user.id)) {
|
if (locked_users.has(user.id)) {
|
||||||
throw UserBusyException(`Cannot generate pdf (type: ${type})` ,user.id);
|
throw new UserBusyException(`Cannot generate pdf for user: ` ,user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
locked_users.add(user.id);
|
locked_users.add(user.id);
|
||||||
|
|
@ -208,7 +234,7 @@ async function _genLatexRec(user: User, file_pref: string, year: number, month:
|
||||||
|
|
||||||
if (estimate == null || isNaN(estimate)) {
|
if (estimate == null || isNaN(estimate)) {
|
||||||
throw new Error("No estimate found")
|
throw new Error("No estimate found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: escape semicolon in comment
|
// TODO: escape semicolon in comment
|
||||||
|
|
||||||
|
|
@ -266,7 +292,7 @@ async function _genLatexEst(user: User, file_pref: string, year: number, quarter
|
||||||
csvfilewriter.write(`${SALUTATION[user.gender]} ${user.name};${user.address};Arbeitnehmer${GENDER_END[user.gender]};Teilzeitmitarbeiter${GENDER_END[user.gender]};${isoToLocalDate(new Date().toISOString())}\n`)
|
csvfilewriter.write(`${SALUTATION[user.gender]} ${user.name};${user.address};Arbeitnehmer${GENDER_END[user.gender]};Teilzeitmitarbeiter${GENDER_END[user.gender]};${isoToLocalDate(new Date().toISOString())}\n`)
|
||||||
|
|
||||||
for (let i = 0; i < 3; ++i) {
|
for (let i = 0; i < 3; ++i) {
|
||||||
csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${estimates[`estimate_${i}`]}\n`);
|
csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${(estimates as any)[`estimate_${i}`]}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
csvfilewriter.end();
|
csvfilewriter.end();
|
||||||
|
|
@ -274,7 +300,7 @@ async function _genLatexEst(user: User, file_pref: string, year: number, quarter
|
||||||
const { stdout, exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf ${TEMPLATE_EST}`.cwd(dir).quiet().nothrow();
|
const { stdout, exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf ${TEMPLATE_EST}`.cwd(dir).quiet().nothrow();
|
||||||
|
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
throw new LatexException("Failed to create estimate PDF", user.id, exitCode, stdout);
|
throw new LatexException("Failed to create estimate PDF", user.id, exitCode, stdout.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${dir}/${file_pref}.pdf`;
|
return `${dir}/${file_pref}.pdf`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue