From cd02307a9d129b03452f0ac376fed3c109fe6059 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 5 Jun 2025 19:40:35 +0200 Subject: [PATCH] initial tests for cospend incl. image upload --- src/lib/db/db.ts | 37 ++++++++ src/lib/models/Payment.ts | 25 +++++ src/routes/api/cospend/+server.ts | 96 +++++++++++++++++++ src/routes/cospend/+layout.server.ts | 7 ++ src/routes/cospend/+layout.svelte | 18 ++++ src/routes/cospend/+page.svelte | 133 +++++++++++++++++++++++++++ 6 files changed, 316 insertions(+) create mode 100644 src/lib/db/db.ts create mode 100644 src/lib/models/Payment.ts create mode 100644 src/routes/api/cospend/+server.ts create mode 100644 src/routes/cospend/+layout.server.ts create mode 100644 src/routes/cospend/+layout.svelte create mode 100644 src/routes/cospend/+page.svelte diff --git a/src/lib/db/db.ts b/src/lib/db/db.ts new file mode 100644 index 0000000..32e4191 --- /dev/null +++ b/src/lib/db/db.ts @@ -0,0 +1,37 @@ +import mongoose from 'mongoose'; +import { MONGO_URL } from '$env/static/private'; +/* + 0 - disconnected + 1 - connected + 2 - connecting + 3 - disconnecting + 4 - uninitialized +*/ +const mongoConnection = { + isConnected: 0, +}; + +export const dbConnect = async () => { + if (mongoConnection.isConnected === 1) { + return; + } + + if (mongoose.connections.length > 0) { + mongoConnection.isConnected = mongoose.connections[0].readyState; + if (mongoConnection.isConnected === 1) { + return; + } + + await mongoose.disconnect(); + } + await mongoose.connect(MONGO_URL ?? ''); + mongoConnection.isConnected = 1; +}; + +export const dbDisconnect = async () => { + if (process.env.NODE_ENV === 'development') return; + if (mongoConnection.isConnected === 0) return; + + await mongoose.disconnect(); + mongoConnection.isConnected = 0; +}; diff --git a/src/lib/models/Payment.ts b/src/lib/models/Payment.ts new file mode 100644 index 0000000..c0b8379 --- /dev/null +++ b/src/lib/models/Payment.ts @@ -0,0 +1,25 @@ +import mongoose from 'mongoose'; + +const PaymentSchema = new mongoose.Schema( + { + type: {type: String, required: true, enum: ['payment', 'reimbursement']}, + name: {type: String, required: true}, + category : {type: String, required: false,}, + date: {type: Date, default: Date.now}, + images: [ { + mediapath: {type: String, required: false}, + }], + description: {type: String, required: false}, + note: {type: String, required: false}, + tags : [String], + original_amount: {type: Number, required: true}, + total_amount: {type: Number, required: true}, + personal_amounts: [{ + user: {type:String, required: true}, + amount: {type: Number, required: true, default:0} + }], + currency: {type: String, required: true, default: 'CHF'}, + }, {timestamps: true} +); + +export const Payment= mongoose.model("Payment", PaymentSchema); diff --git a/src/routes/api/cospend/+server.ts b/src/routes/api/cospend/+server.ts new file mode 100644 index 0000000..8b28b5f --- /dev/null +++ b/src/routes/api/cospend/+server.ts @@ -0,0 +1,96 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import fs from 'fs'; +import path from 'path'; +import { mkdir } from 'fs/promises'; +import { Payment } from '$lib/models/Payment'; // adjust path as needed +import { dbConnect, dbDisconnect } from '$lib/db/db'; + +const UPLOAD_DIR = '/var/lib/www/static/test'; +const BASE_CURRENCY = 'CHF'; // Default currency + +export const POST: RequestHandler = async ({ request, locals }) => { + const formData = await request.formData(); + + try { + const name = formData.get('name') as string; + const category = formData.get('category') as string; + const date= new Date(formData.get('date') as string); + const description = formData.get('description') as string; + const note = formData.get('note') as string; + const tags = JSON.parse(formData.get('tags') as string) as string[]; + let currency = formData.get('currency') as string; + let original_amount = parseFloat(formData.get('original_amount') as string); + let total_amount = NaN; + + // if currency is not BASE_CURRENCY, fetch current conversion rate using frankfurter API and date in YYYY-MM-DD format + if (!currency || currency === BASE_CURRENCY) { + currency = BASE_CURRENCY; + total_amount = parseFloat(formData.get('total_amount') as string); + } else { + const date_fmt = date.toISOString().split('T')[0]; // Convert date to YYYY-MM-DD format + // Fetch conversion rate logic here (not implemented in this example) + const res = await fetch(`https://api.frankfurter.app/${date_fmt}?from=${currency}&to=${BASE_CURRENCY}`) + const { result } = await res.json(); + if (!result || !result[BASE_CURRENCY]) { + return new Response(JSON.stringify({ message: 'Currency conversion failed.' }), { status: 400 }); + } + // Assuming you want to convert the total amount to BASE_CURRENCY + const conversionRate = parseFloat(result.rates[BASE_CURRENCY]); + alert(`Conversion rate from ${currency} to ${BASE_CURRENCY} on ${date_fmt}: ${conversionRate}`); + total_amount = original_amount * conversionRate; + } + + const personal_amounts = JSON.parse(formData.get('personal_amounts') as string) as { user: string, amount: number }[]; + + + if (!name || isNaN(total_amount)) { + return new Response(JSON.stringify({ message: 'Invalid required fields.' }), { status: 400 }); + } + + // await mkdir(UPLOAD_DIR, { recursive: true }); + + const images: { mediapath: string }[] = []; + const imageFiles = formData.getAll('images') as File[]; + + for (const file of imageFiles) { + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const safeName = `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9_.-]/g, '_')}`; + const fullPath = path.join(UPLOAD_DIR, safeName); + //fs.writeFileSync(fullPath, buffer); + images.push({ mediapath: `/static/test/${safeName}` }); + } + + await dbConnect(); + const payment = new Payment({ + name, + category, + date, + description, + note, + tags, + total_amount, + original_amount, + currency, + personal_amounts, + images + }); + + + // let auth = await locals.auth(); + // // if(!auth){ + // throw error(401, "Not logged in") + // } + + try{ + await Payment.create(payment); + } catch(e){ + throw error(400, e) + } + await dbDisconnect(); + return new Response(JSON.stringify({ message: 'Payment event created successfully.' }), { status: 201 }); + } catch (err) { + console.error(err); + return new Response(JSON.stringify({ message: 'Error processing request.' }), { status: 500 }); + } +}; diff --git a/src/routes/cospend/+layout.server.ts b/src/routes/cospend/+layout.server.ts new file mode 100644 index 0000000..f4f4110 --- /dev/null +++ b/src/routes/cospend/+layout.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from "./$types" + +export const load : PageServerLoad = (async ({locals}) => { + return { + session: await locals.auth(), + } +}); diff --git a/src/routes/cospend/+layout.svelte b/src/routes/cospend/+layout.svelte new file mode 100644 index 0000000..5f13278 --- /dev/null +++ b/src/routes/cospend/+layout.svelte @@ -0,0 +1,18 @@ + +
+ + + +
diff --git a/src/routes/cospend/+page.svelte b/src/routes/cospend/+page.svelte new file mode 100644 index 0000000..1c0beb3 --- /dev/null +++ b/src/routes/cospend/+page.svelte @@ -0,0 +1,133 @@ + + +

Settlement Plan

+ + +
+ + + + + + + + + + + + + + + + +
+ Personal Amounts + {#each personal_amounts as entry, i} +
+ + +
+ {/each} +
+ + + + + + +