fix: replace any types with proper types across codebase
Replace ~100 `any` usages with proper types: use existing interfaces (RecipeModelType, BriefRecipeType, IPayment, etc.), Record<string, unknown> for dynamic objects, unknown for catch clauses with proper narrowing, and inline types for callbacks. Remaining `any` types are in Svelte components and cases where mongoose document mutation requires casts.
This commit is contained in:
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@@ -7,7 +7,7 @@ declare global {
|
|||||||
interface Error {
|
interface Error {
|
||||||
message: string;
|
message: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
bibleQuote?: any;
|
bibleQuote?: { text: string; reference: string } | null;
|
||||||
lang?: string;
|
lang?: string;
|
||||||
}
|
}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
|||||||
else if (!session.user?.groups?.includes('rezepte_users')) {
|
else if (!session.user?.groups?.includes('rezepte_users')) {
|
||||||
error(403, {
|
error(403, {
|
||||||
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
||||||
} as any);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
|||||||
if (event.url.pathname.startsWith('/api/cospend')) {
|
if (event.url.pathname.startsWith('/api/cospend')) {
|
||||||
error(401, {
|
error(401, {
|
||||||
message: 'Anmeldung erforderlich. Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.'
|
message: 'Anmeldung erforderlich. Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.'
|
||||||
} as any);
|
});
|
||||||
}
|
}
|
||||||
// For page routes, redirect to login
|
// For page routes, redirect to login
|
||||||
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
||||||
@@ -51,7 +51,7 @@ async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
|||||||
else if (!session.user?.groups?.includes('cospend')) {
|
else if (!session.user?.groups?.includes('cospend')) {
|
||||||
error(403, {
|
error(403, {
|
||||||
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
|
||||||
} as any);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ async function authorization({ event, resolve }: Parameters<Handle>[0]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bible verse functionality for error pages
|
// Bible verse functionality for error pages
|
||||||
async function getRandomVerse(fetch: typeof globalThis.fetch, pathname: string): Promise<any> {
|
async function getRandomVerse(fetch: typeof globalThis.fetch, pathname: string): Promise<{ text: string; reference: string } | null> {
|
||||||
const isEnglish = pathname.startsWith('/faith/') || pathname.startsWith('/recipes/');
|
const isEnglish = pathname.startsWith('/faith/') || pathname.startsWith('/recipes/');
|
||||||
const endpoint = isEnglish ? '/api/faith/bibel/zufallszitat' : '/api/glaube/bibel/zufallszitat';
|
const endpoint = isEnglish ? '/api/faith/bibel/zufallszitat' : '/api/glaube/bibel/zufallszitat';
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -26,8 +26,37 @@ function parseTimeToISO8601(timeString: string): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRecipeJsonLd(data: any) {
|
import type { RecipeModelType } from '$types/types';
|
||||||
const jsonLd: any = {
|
|
||||||
|
interface HowToStep {
|
||||||
|
"@type": "HowToStep";
|
||||||
|
name: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecipeJsonLd {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
author: { "@type": string; name: string };
|
||||||
|
datePublished?: string;
|
||||||
|
dateModified?: string;
|
||||||
|
recipeCategory: string;
|
||||||
|
keywords?: string;
|
||||||
|
image: { "@type": string; url: string; width: number; height: number };
|
||||||
|
recipeIngredient: string[];
|
||||||
|
recipeInstructions: HowToStep[];
|
||||||
|
url: string;
|
||||||
|
recipeYield?: string;
|
||||||
|
prepTime?: string;
|
||||||
|
cookTime?: string;
|
||||||
|
totalTime?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRecipeJsonLd(data: RecipeModelType) {
|
||||||
|
const jsonLd: RecipeJsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Recipe",
|
"@type": "Recipe",
|
||||||
"name": data.name?.replace(/<[^>]*>/g, ''), // Strip HTML tags
|
"name": data.name?.replace(/<[^>]*>/g, ''), // Strip HTML tags
|
||||||
@@ -47,7 +76,7 @@ export function generateRecipeJsonLd(data: any) {
|
|||||||
"height": 800
|
"height": 800
|
||||||
},
|
},
|
||||||
"recipeIngredient": [] as string[],
|
"recipeIngredient": [] as string[],
|
||||||
"recipeInstructions": [] as any[],
|
"recipeInstructions": [] as HowToStep[],
|
||||||
"url": `https://bocken.org/rezepte/${data.short_name}`
|
"url": `https://bocken.org/rezepte/${data.short_name}`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Extracts duplicated search state logic from multiple pages.
|
* Extracts duplicated search state logic from multiple pages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Recipe = { _id: string; [key: string]: any };
|
type Recipe = { _id: string; [key: string]: unknown };
|
||||||
|
|
||||||
export function createSearchFilter<T extends Recipe>(getRecipes: () => T[]) {
|
export function createSearchFilter<T extends Recipe>(getRecipes: () => T[]) {
|
||||||
let matchedRecipeIds = $state(new Set<string>());
|
let matchedRecipeIds = $state(new Set<string>());
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export async function listOllamaModels(): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.models?.map((m: any) => m.name) || [];
|
return data.models?.map((m: { name: string }) => m.name) || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to list Ollama models:', error);
|
console.error('Failed to list Ollama models:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ export async function invalidateRecipeCaches(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise<void> {
|
export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const invalidations: Promise<any>[] = [];
|
const invalidations: Promise<void>[] = [];
|
||||||
|
|
||||||
// Invalidate balance and debts caches for all affected users
|
// Invalidate balance and debts caches for all affected users
|
||||||
for (const username of usernames) {
|
for (const username of usernames) {
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
* Utility functions for handling user favorites on the server side
|
* Utility functions for handling user favorites on the server side
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function getUserFavorites(fetch: any, locals: any): Promise<string[]> {
|
import type { BriefRecipeType } from '$types/types';
|
||||||
|
import type { Session } from '@auth/sveltekit';
|
||||||
|
|
||||||
|
type BriefRecipeWithFavorite = BriefRecipeType & { isFavorite: boolean };
|
||||||
|
|
||||||
|
export async function getUserFavorites(fetch: typeof globalThis.fetch, locals: App.Locals): Promise<string[]> {
|
||||||
const session = await locals.auth();
|
const session = await locals.auth();
|
||||||
|
|
||||||
if (!session?.user?.nickname) {
|
if (!session?.user?.nickname) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const favRes = await fetch('/api/rezepte/favorites');
|
const favRes = await fetch('/api/rezepte/favorites');
|
||||||
if (favRes.ok) {
|
if (favRes.ok) {
|
||||||
@@ -19,17 +24,17 @@ export async function getUserFavorites(fetch: any, locals: any): Promise<string[
|
|||||||
// Silently fail if favorites can't be loaded
|
// Silently fail if favorites can't be loaded
|
||||||
console.error('Error loading user favorites:', e);
|
console.error('Error loading user favorites:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string[]): any[] {
|
export function addFavoriteStatusToRecipes(recipes: BriefRecipeType[], userFavorites: string[]): BriefRecipeWithFavorite[] {
|
||||||
// Safety check: ensure recipes is an array
|
// Safety check: ensure recipes is an array
|
||||||
if (!Array.isArray(recipes)) {
|
if (!Array.isArray(recipes)) {
|
||||||
console.error('addFavoriteStatusToRecipes: recipes is not an array:', recipes);
|
console.error('addFavoriteStatusToRecipes: recipes is not an array:', recipes);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipes.map(recipe => ({
|
return recipes.map(recipe => ({
|
||||||
...recipe,
|
...recipe,
|
||||||
isFavorite: userFavorites.some(favId => favId.toString() === recipe._id.toString())
|
isFavorite: userFavorites.some(favId => favId.toString() === recipe._id.toString())
|
||||||
@@ -37,18 +42,18 @@ export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadRecipesWithFavorites(
|
export async function loadRecipesWithFavorites(
|
||||||
fetch: any,
|
fetch: typeof globalThis.fetch,
|
||||||
locals: any,
|
locals: App.Locals,
|
||||||
recipeLoader: () => Promise<any>
|
recipeLoader: () => Promise<BriefRecipeType[]>
|
||||||
): Promise<{ recipes: any[], session: any }> {
|
): Promise<{ recipes: BriefRecipeWithFavorite[], session: Session | null }> {
|
||||||
const [recipes, userFavorites, session] = await Promise.all([
|
const [recipes, userFavorites, session] = await Promise.all([
|
||||||
recipeLoader(),
|
recipeLoader(),
|
||||||
getUserFavorites(fetch, locals),
|
getUserFavorites(fetch, locals),
|
||||||
locals.auth()
|
locals.auth()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recipes: addFavoriteStatusToRecipes(recipes, userFavorites),
|
recipes: addFavoriteStatusToRecipes(recipes, userFavorites),
|
||||||
session
|
session
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BriefRecipeType } from '$types/types';
|
import type { BriefRecipeType, RecipeModelType } from '$types/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a recipeLang param refers to the English version.
|
* Check whether a recipeLang param refers to the English version.
|
||||||
@@ -30,7 +30,7 @@ export function briefQueryConfig(recipeLang: string) {
|
|||||||
* For English, extracts from translations.en and adds germanShortName.
|
* For English, extracts from translations.en and adds germanShortName.
|
||||||
* For German, passes through root-level fields.
|
* For German, passes through root-level fields.
|
||||||
*/
|
*/
|
||||||
export function toBrief(recipe: any, recipeLang: string): BriefRecipeType {
|
export function toBrief(recipe: RecipeModelType, recipeLang: string): BriefRecipeType {
|
||||||
if (isEnglish(recipeLang)) {
|
if (isEnglish(recipeLang)) {
|
||||||
return {
|
return {
|
||||||
_id: recipe._id,
|
_id: recipe._id,
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class RecurringPaymentScheduler {
|
|||||||
|
|
||||||
await this.processRecurringPayments();
|
await this.processRecurringPayments();
|
||||||
}, {
|
}, {
|
||||||
timezone: 'Europe/Zurich' // Adjust timezone as needed
|
timezone: 'Europe/Zurich'
|
||||||
} as any);
|
});
|
||||||
|
|
||||||
console.log('[Scheduler] Recurring payments scheduler started (runs every minute)');
|
console.log('[Scheduler] Recurring payments scheduler started (runs every minute)');
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,8 @@ class RecurringPaymentScheduler {
|
|||||||
return {
|
return {
|
||||||
isRunning: this.isRunning,
|
isRunning: this.isRunning,
|
||||||
isScheduled: this.task !== null,
|
isScheduled: this.task !== null,
|
||||||
nextRun: (this.task as any)?.nextDate?.()?.toISOString?.()
|
// node-cron's ScheduledTask type doesn't expose nextDate, but it exists at runtime
|
||||||
|
nextRun: (this.task as unknown as { nextDate?: () => { toISOString: () => string } })?.nextDate?.()?.toISOString?.()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
// Utility functions for identifying and handling settlement payments
|
// Utility functions for identifying and handling settlement payments
|
||||||
|
|
||||||
|
import type { IPayment } from '$models/Payment';
|
||||||
|
import type { IPaymentSplit } from '$models/PaymentSplit';
|
||||||
|
|
||||||
|
type PaymentWithSplits = IPayment & { splits?: IPaymentSplit[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies if a payment is a settlement payment based on category
|
* Identifies if a payment is a settlement payment based on category
|
||||||
*/
|
*/
|
||||||
export function isSettlementPayment(payment: any): boolean {
|
export function isSettlementPayment(payment: PaymentWithSplits | null | undefined): boolean {
|
||||||
if (!payment) return false;
|
if (!payment) return false;
|
||||||
|
|
||||||
// Check if category is settlement
|
// Check if category is settlement
|
||||||
return payment.category === 'settlement';
|
return payment.category === 'settlement';
|
||||||
}
|
}
|
||||||
@@ -20,44 +25,44 @@ export function getSettlementIcon(): string {
|
|||||||
/**
|
/**
|
||||||
* Gets appropriate styling classes for settlement payments
|
* Gets appropriate styling classes for settlement payments
|
||||||
*/
|
*/
|
||||||
export function getSettlementClasses(payment: any): string[] {
|
export function getSettlementClasses(payment: PaymentWithSplits): string[] {
|
||||||
if (!isSettlementPayment(payment)) {
|
if (!isSettlementPayment(payment)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['settlement-payment'];
|
return ['settlement-payment'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets settlement-specific display text
|
* Gets settlement-specific display text
|
||||||
*/
|
*/
|
||||||
export function getSettlementDisplayText(payment: any): string {
|
export function getSettlementDisplayText(payment: PaymentWithSplits): string {
|
||||||
if (!isSettlementPayment(payment)) {
|
if (!isSettlementPayment(payment)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Settlement';
|
return 'Settlement';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the other user in a settlement (the one who didn't pay)
|
* Gets the other user in a settlement (the one who didn't pay)
|
||||||
*/
|
*/
|
||||||
export function getSettlementReceiver(payment: any): string {
|
export function getSettlementReceiver(payment: PaymentWithSplits): string {
|
||||||
if (!isSettlementPayment(payment) || !payment.splits) {
|
if (!isSettlementPayment(payment) || !payment.splits) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the user who has a positive amount (the receiver)
|
// Find the user who has a positive amount (the receiver)
|
||||||
const receiver = payment.splits.find((split: any) => split.amount > 0);
|
const receiver = payment.splits.find((split) => split.amount > 0);
|
||||||
if (receiver && receiver.username) {
|
if (receiver && receiver.username) {
|
||||||
return receiver.username;
|
return receiver.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: find the user who is not the payer
|
// Fallback: find the user who is not the payer
|
||||||
const otherUser = payment.splits.find((split: any) => split.username !== payment.paidBy);
|
const otherUser = payment.splits.find((split) => split.username !== payment.paidBy);
|
||||||
if (otherUser && otherUser.username) {
|
if (otherUser && otherUser.username) {
|
||||||
return otherUser.username;
|
return otherUser.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ const RecipeSchema = new mongoose.Schema(
|
|||||||
RecipeSchema.index({ "translations.en.short_name": 1 });
|
RecipeSchema.index({ "translations.en.short_name": 1 });
|
||||||
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
||||||
|
|
||||||
let _recipeModel: any;
|
import type { RecipeModelType } from '$types/types';
|
||||||
try { _recipeModel = mongoose.model("Recipe"); } catch { _recipeModel = mongoose.model("Recipe", RecipeSchema); }
|
|
||||||
export const Recipe = _recipeModel as mongoose.Model<any>;
|
let _recipeModel: mongoose.Model<RecipeModelType>;
|
||||||
|
try { _recipeModel = mongoose.model<RecipeModelType>("Recipe"); } catch { _recipeModel = mongoose.model<RecipeModelType>("Recipe", RecipeSchema); }
|
||||||
|
export const Recipe = _recipeModel;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const RecurringPaymentSchema = new mongoose.Schema(
|
|||||||
cronExpression: {
|
cronExpression: {
|
||||||
type: String,
|
type: String,
|
||||||
validate: {
|
validate: {
|
||||||
validator: function(this: any, value: string) {
|
validator: function(this: IRecurringPayment, value: string) {
|
||||||
// Only validate if frequency is custom
|
// Only validate if frequency is custom
|
||||||
if (this.frequency === 'custom') {
|
if (this.frequency === 'custom') {
|
||||||
return value != null && value.trim().length > 0;
|
return value != null && value.trim().length > 0;
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ const RosaryStreakSchema = new mongoose.Schema(
|
|||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
interface IRosaryStreak {
|
||||||
let _model: any;
|
username: string;
|
||||||
try { _model = mongoose.model("RosaryStreak"); } catch { _model = mongoose.model("RosaryStreak", RosaryStreakSchema); }
|
length: number;
|
||||||
export const RosaryStreak = _model as mongoose.Model<any>;
|
lastPrayed: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _model: mongoose.Model<IRosaryStreak>;
|
||||||
|
try { _model = mongoose.model<IRosaryStreak>("RosaryStreak"); } catch { _model = mongoose.model<IRosaryStreak>("RosaryStreak", RosaryStreakSchema); }
|
||||||
|
export const RosaryStreak = _model;
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ export const actions = {
|
|||||||
caption: '',
|
caption: '',
|
||||||
color
|
color
|
||||||
}];
|
}];
|
||||||
} catch (imageError: any) {
|
} catch (imageError: unknown) {
|
||||||
console.error('[RecipeAdd] Image processing error:', imageError);
|
console.error('[RecipeAdd] Image processing error:', imageError);
|
||||||
|
const message = imageError instanceof Error ? imageError.message : String(imageError);
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: `Failed to process image: ${imageError.message}`,
|
error: `Failed to process image: ${message}`,
|
||||||
errors: ['Image processing failed'],
|
errors: ['Image processing failed'],
|
||||||
values: Object.fromEntries(formData)
|
values: Object.fromEntries(formData)
|
||||||
});
|
});
|
||||||
@@ -114,16 +115,19 @@ export const actions = {
|
|||||||
|
|
||||||
// Redirect to the new recipe page
|
// Redirect to the new recipe page
|
||||||
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
|
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
|
||||||
} catch (dbError: any) {
|
} catch (dbError: unknown) {
|
||||||
// Re-throw redirects (they're not errors)
|
// Re-throw redirects (they're not errors)
|
||||||
if (dbError?.status >= 300 && dbError?.status < 400) {
|
if (dbError && typeof dbError === 'object' && 'status' in dbError) {
|
||||||
throw dbError;
|
const status = (dbError as { status: number }).status;
|
||||||
|
if (status >= 300 && status < 400) {
|
||||||
|
throw dbError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Database error creating recipe:', dbError);
|
console.error('Database error creating recipe:', dbError);
|
||||||
|
|
||||||
// Check for duplicate key error
|
// Check for duplicate key error
|
||||||
if (dbError.code === 11000) {
|
if (dbError && typeof dbError === 'object' && 'code' in dbError && (dbError as { code: number }).code === 11000) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`,
|
error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`,
|
||||||
errors: ['Duplicate short_name'],
|
errors: ['Duplicate short_name'],
|
||||||
@@ -131,23 +135,28 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbMessage = dbError instanceof Error ? dbError.message : String(dbError);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to create recipe: ${dbError.message || 'Unknown database error'}`,
|
error: `Failed to create recipe: ${dbMessage || 'Unknown database error'}`,
|
||||||
errors: [dbError.message],
|
errors: [dbMessage],
|
||||||
values: Object.fromEntries(formData)
|
values: Object.fromEntries(formData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
// Re-throw redirects (they're not errors)
|
// Re-throw redirects (they're not errors)
|
||||||
if (error?.status >= 300 && error?.status < 400) {
|
if (error && typeof error === 'object' && 'status' in error) {
|
||||||
throw error;
|
const status = (error as { status: number }).status;
|
||||||
|
if (status >= 300 && status < 400) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Error processing recipe submission:', error);
|
console.error('Error processing recipe submission:', error);
|
||||||
|
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to process recipe: ${error.message || 'Unknown error'}`,
|
error: `Failed to process recipe: ${message || 'Unknown error'}`,
|
||||||
errors: [error.message]
|
errors: [message]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ export const actions = {
|
|||||||
caption: existingImagePath ? (recipeData.images?.[0]?.caption || '') : '',
|
caption: existingImagePath ? (recipeData.images?.[0]?.caption || '') : '',
|
||||||
color
|
color
|
||||||
}];
|
}];
|
||||||
} catch (imageError: any) {
|
} catch (imageError: unknown) {
|
||||||
console.error('Image processing error:', imageError);
|
console.error('Image processing error:', imageError);
|
||||||
|
const message = imageError instanceof Error ? imageError.message : String(imageError);
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: `Failed to process image: ${imageError.message}`,
|
error: `Failed to process image: ${message}`,
|
||||||
errors: ['Image processing failed'],
|
errors: ['Image processing failed'],
|
||||||
values: Object.fromEntries(formData)
|
values: Object.fromEntries(formData)
|
||||||
});
|
});
|
||||||
@@ -213,16 +214,16 @@ export const actions = {
|
|||||||
|
|
||||||
// Redirect to the updated recipe page (might have new short_name)
|
// Redirect to the updated recipe page (might have new short_name)
|
||||||
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
|
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
|
||||||
} catch (dbError: any) {
|
} catch (dbError: unknown) {
|
||||||
// Re-throw redirects (they're not errors)
|
// Re-throw redirects (they're not errors)
|
||||||
if (dbError?.status >= 300 && dbError?.status < 400) {
|
if (dbError && typeof dbError === 'object' && 'status' in dbError && (dbError as { status: number }).status >= 300 && (dbError as { status: number }).status < 400) {
|
||||||
throw dbError;
|
throw dbError;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Database error updating recipe:', dbError);
|
console.error('Database error updating recipe:', dbError);
|
||||||
|
|
||||||
// Check for duplicate key error
|
// Check for duplicate key error
|
||||||
if (dbError.code === 11000) {
|
if (dbError && typeof dbError === 'object' && 'code' in dbError && (dbError as { code: number }).code === 11000) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`,
|
error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`,
|
||||||
errors: ['Duplicate short_name'],
|
errors: ['Duplicate short_name'],
|
||||||
@@ -230,23 +231,28 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dbMessage = dbError instanceof Error ? dbError.message : String(dbError);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to update recipe: ${dbError.message || 'Unknown database error'}`,
|
error: `Failed to update recipe: ${dbMessage || 'Unknown database error'}`,
|
||||||
errors: [dbError.message],
|
errors: [dbMessage],
|
||||||
values: Object.fromEntries(formData)
|
values: Object.fromEntries(formData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
// Re-throw redirects (they're not errors)
|
// Re-throw redirects (they're not errors)
|
||||||
if (error?.status >= 300 && error?.status < 400) {
|
if (error && typeof error === 'object' && 'status' in error) {
|
||||||
throw error;
|
const status = (error as { status: number }).status;
|
||||||
|
if (status >= 300 && status < 400) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Error processing recipe update:', error);
|
console.error('Error processing recipe update:', error);
|
||||||
|
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: `Failed to process recipe update: ${error.message || 'Unknown error'}`,
|
error: `Failed to process recipe update: ${message || 'Unknown error'}`,
|
||||||
errors: [error.message]
|
errors: [message]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,12 +125,13 @@ export const POST = (async ({ request, locals }) => {
|
|||||||
unhashedFilename: unhashedFilename,
|
unhashedFilename: unhashedFilename,
|
||||||
color
|
color
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
// Re-throw errors that already have status codes
|
// Re-throw errors that already have status codes
|
||||||
if (err.status) throw err;
|
if (err && typeof err === 'object' && 'status' in err) throw err;
|
||||||
|
|
||||||
// Log and throw generic error for unexpected failures
|
// Log and throw generic error for unexpected failures
|
||||||
console.error('[API:ImgAdd] Upload error:', err);
|
console.error('[API:ImgAdd] Upload error:', err);
|
||||||
throw error(500, `Failed to upload image: ${err.message || 'Unknown error'}`);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw error(500, `Failed to upload image: ${message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}) satisfies RequestHandler;
|
}) satisfies RequestHandler;
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { json, type RequestHandler } from '@sveltejs/kit';
|
|||||||
import { Recipe } from '$models/Recipe';
|
import { Recipe } from '$models/Recipe';
|
||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { RecipeModelType } from '$types/types';
|
import type { RecipeModelType, IngredientItem, InstructionItem } from '$types/types';
|
||||||
import { isEnglish } from '$lib/server/recipeHelpers';
|
import { isEnglish } from '$lib/server/recipeHelpers';
|
||||||
|
|
||||||
|
type RecipeItem = (IngredientItem | InstructionItem) & { baseRecipeRef?: Record<string, unknown>; resolvedRecipe?: Record<string, unknown> };
|
||||||
|
|
||||||
/** Recursively map populated baseRecipeRef to resolvedRecipe field */
|
/** Recursively map populated baseRecipeRef to resolvedRecipe field */
|
||||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
function mapBaseRecipeRefs(items: RecipeItem[]): RecipeItem[] {
|
||||||
return items.map((item: any) => {
|
return items.map((item) => {
|
||||||
if (item.type === 'reference' && item.baseRecipeRef) {
|
if (item.type === 'reference' && item.baseRecipeRef) {
|
||||||
const resolvedRecipe = { ...item.baseRecipeRef };
|
const resolvedRecipe = { ...item.baseRecipeRef };
|
||||||
if (resolvedRecipe.ingredients) {
|
if (resolvedRecipe.ingredients) {
|
||||||
@@ -84,6 +86,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mongoose query builder requires chained .populate() calls
|
||||||
let dbQuery: any = Recipe.findOne(query);
|
let dbQuery: any = Recipe.findOne(query);
|
||||||
for (const p of populatePaths) {
|
for (const p of populatePaths) {
|
||||||
dbQuery = dbQuery.populate(p);
|
dbQuery = dbQuery.populate(p);
|
||||||
@@ -100,7 +103,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = rawRecipe.translations.en;
|
const t = rawRecipe.translations.en;
|
||||||
let recipe: any = {
|
let recipe: Record<string, unknown> = {
|
||||||
_id: rawRecipe._id,
|
_id: rawRecipe._id,
|
||||||
short_name: t.short_name,
|
short_name: t.short_name,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
@@ -128,17 +131,17 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (recipe.ingredients) {
|
if (recipe.ingredients) {
|
||||||
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients);
|
recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients as RecipeItem[]);
|
||||||
}
|
}
|
||||||
if (recipe.instructions) {
|
if (recipe.instructions) {
|
||||||
recipe.instructions = mapBaseRecipeRefs(recipe.instructions);
|
recipe.instructions = mapBaseRecipeRefs(recipe.instructions as RecipeItem[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge English alt/caption with original image paths
|
// Merge English alt/caption with original image paths
|
||||||
const imagesArray = Array.isArray(rawRecipe.images) ? rawRecipe.images : (rawRecipe.images ? [rawRecipe.images] : []);
|
const imagesArray = Array.isArray(rawRecipe.images) ? rawRecipe.images : (rawRecipe.images ? [rawRecipe.images] : []);
|
||||||
if (imagesArray.length > 0) {
|
if (imagesArray.length > 0) {
|
||||||
const translatedImages = t.images || [];
|
const translatedImages = t.images || [];
|
||||||
recipe.images = imagesArray.map((img: any, index: number) => ({
|
recipe.images = imagesArray.map((img: { mediapath: string; alt?: string; caption?: string; color?: string }, index: number) => ({
|
||||||
mediapath: img.mediapath,
|
mediapath: img.mediapath,
|
||||||
alt: translatedImages[index]?.alt || img.alt || '',
|
alt: translatedImages[index]?.alt || img.alt || '',
|
||||||
caption: translatedImages[index]?.caption || img.caption || '',
|
caption: translatedImages[index]?.caption || img.caption || '',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
recipes = JSON.parse(cached);
|
recipes = JSON.parse(cached);
|
||||||
} else {
|
} else {
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
const dbRecipes: any[] = await Recipe.find(approvalFilter, projection).lean();
|
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
|
||||||
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const GET: RequestHandler = async ({ url, params, locals }) => {
|
|||||||
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let dbQuery: any = { ...approvalFilter };
|
let dbQuery: Record<string, unknown> = { ...approvalFilter };
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
dbQuery[`${prefix}category`] = category;
|
dbQuery[`${prefix}category`] = category;
|
||||||
@@ -49,9 +49,10 @@ export const GET: RequestHandler = async ({ url, params, locals }) => {
|
|||||||
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang!));
|
||||||
|
|
||||||
// Handle favorites filter
|
// Handle favorites filter
|
||||||
if (favoritesOnly && (locals as any).session?.user) {
|
const session = await locals.auth();
|
||||||
|
if (favoritesOnly && session?.user) {
|
||||||
const { UserFavorites } = await import('$models/UserFavorites');
|
const { UserFavorites } = await import('$models/UserFavorites');
|
||||||
const userFavorites = await UserFavorites.findOne({ username: (locals as any).session.user.username });
|
const userFavorites = await UserFavorites.findOne({ username: session.user.nickname });
|
||||||
if (userFavorites?.favorites) {
|
if (userFavorites?.favorites) {
|
||||||
const favoriteIds = userFavorites.favorites;
|
const favoriteIds = userFavorites.favorites;
|
||||||
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
|
recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString()));
|
||||||
|
|||||||
@@ -52,25 +52,27 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
translationMetadata,
|
translationMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
console.error('Translation API error:', err);
|
console.error('Translation API error:', err);
|
||||||
|
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
|
||||||
// Handle specific error cases
|
// Handle specific error cases
|
||||||
if (err.message?.includes('DeepL API')) {
|
if (message?.includes('DeepL API')) {
|
||||||
throw error(503, `Translation service error: ${err.message}`);
|
throw error(503, `Translation service error: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.message?.includes('API key not configured')) {
|
if (message?.includes('API key not configured')) {
|
||||||
throw error(500, 'Translation service is not configured. Please add DEEPL_API_KEY to environment variables.');
|
throw error(500, 'Translation service is not configured. Please add DEEPL_API_KEY to environment variables.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-throw SvelteKit errors
|
// Re-throw SvelteKit errors
|
||||||
if (err.status) {
|
if (err && typeof err === 'object' && 'status' in err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic error
|
// Generic error
|
||||||
throw error(500, `Translation failed: ${err.message || 'Unknown error'}`);
|
throw error(500, `Translation failed: ${message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,11 +90,12 @@ export const GET: RequestHandler = async () => {
|
|||||||
service: 'DeepL Translation API',
|
service: 'DeepL Translation API',
|
||||||
status: isConfigured ? 'ready' : 'not configured',
|
status: isConfigured ? 'ready' : 'not configured',
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
return json({
|
return json({
|
||||||
configured: false,
|
configured: false,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: err.message,
|
error: message,
|
||||||
}, { status: 500 });
|
}, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { PaymentSplit } from '$models/PaymentSplit';
|
import { PaymentSplit } from '$models/PaymentSplit';
|
||||||
import { Payment } from '$models/Payment';
|
import type { IPayment } from '$models/Payment';
|
||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
import cache from '$lib/server/cache';
|
import cache from '$lib/server/cache';
|
||||||
|
|
||||||
|
type PopulatedPayment = IPayment & { _id: import('mongoose').Types.ObjectId };
|
||||||
|
|
||||||
interface DebtSummary {
|
interface DebtSummary {
|
||||||
username: string;
|
username: string;
|
||||||
netAmount: number; // positive = you owe them, negative = they owe you
|
netAmount: number; // positive = you owe them, negative = they owe you
|
||||||
@@ -42,7 +44,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
// Get all other users who have splits with payments involving the current user
|
// Get all other users who have splits with payments involving the current user
|
||||||
const paymentIds = userSplits.map(split => (split.paymentId as any)._id);
|
const paymentIds = userSplits.map(split => (split.paymentId as unknown as PopulatedPayment)._id);
|
||||||
const allRelatedSplits = await PaymentSplit.find({
|
const allRelatedSplits = await PaymentSplit.find({
|
||||||
paymentId: { $in: paymentIds },
|
paymentId: { $in: paymentIds },
|
||||||
username: { $ne: currentUser }
|
username: { $ne: currentUser }
|
||||||
@@ -55,12 +57,12 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
|
|
||||||
// Process current user's splits to understand what they owe/are owed
|
// Process current user's splits to understand what they owe/are owed
|
||||||
for (const split of userSplits) {
|
for (const split of userSplits) {
|
||||||
const payment = split.paymentId as any;
|
const payment = split.paymentId as unknown as PopulatedPayment;
|
||||||
if (!payment) continue;
|
if (!payment) continue;
|
||||||
|
|
||||||
// Find other participants in this payment
|
// Find other participants in this payment
|
||||||
const otherSplits = allRelatedSplits.filter(s =>
|
const otherSplits = allRelatedSplits.filter(s =>
|
||||||
(s.paymentId as any)._id.toString() === (split.paymentId as any)._id.toString()
|
(s.paymentId as unknown as PopulatedPayment)._id.toString() === payment._id.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const otherSplit of otherSplits) {
|
for (const otherSplit of otherSplits) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await Payment.aggregate(pipeline as any);
|
const results = await Payment.aggregate(pipeline);
|
||||||
|
|
||||||
// Transform data into chart-friendly format
|
// Transform data into chart-friendly format
|
||||||
const monthsMap = new Map();
|
const monthsMap = new Map();
|
||||||
@@ -91,7 +91,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate data
|
// Populate data
|
||||||
results.forEach((result: any) => {
|
results.forEach((result: { _id: { yearMonth: string; category: string }; totalAmount: number }) => {
|
||||||
const { yearMonth, category } = result._id;
|
const { yearMonth, category } = result._id;
|
||||||
const amount = result.totalAmount;
|
const amount = result.totalAmount;
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
let firstMonthWithData = 0;
|
let firstMonthWithData = 0;
|
||||||
for (let i = 0; i < allMonths.length; i++) {
|
for (let i = 0; i < allMonths.length; i++) {
|
||||||
const monthData = monthsMap.get(allMonths[i]);
|
const monthData = monthsMap.get(allMonths[i]);
|
||||||
const hasData = Object.values(monthData).some((value: any) => value > 0);
|
const hasData = Object.values(monthData).some((value) => (value as number) > 0);
|
||||||
if (hasData) {
|
if (hasData) {
|
||||||
firstMonthWithData = i;
|
firstMonthWithData = i;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { convertToCHF, isValidCurrencyCode } from '$lib/utils/currency';
|
|||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
import cache, { invalidateCospendCaches } from '$lib/server/cache';
|
import cache, { invalidateCospendCaches } from '$lib/server/cache';
|
||||||
|
|
||||||
|
interface SplitInput {
|
||||||
|
username: string;
|
||||||
|
amount: number;
|
||||||
|
proportion?: number;
|
||||||
|
personalAmount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ locals, url }) => {
|
export const GET: RequestHandler = async ({ locals, url }) => {
|
||||||
const auth = await locals.auth();
|
const auth = await locals.auth();
|
||||||
if (!auth || !auth.user?.nickname) {
|
if (!auth || !auth.user?.nickname) {
|
||||||
@@ -79,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
|
|
||||||
// Validate personal + equal split method
|
// Validate personal + equal split method
|
||||||
if (splitMethod === 'personal_equal' && splits) {
|
if (splitMethod === 'personal_equal' && splits) {
|
||||||
const totalPersonal = splits.reduce((sum: number, split: any) => {
|
const totalPersonal = splits.reduce((sum: number, split: SplitInput) => {
|
||||||
return sum + (parseFloat(split.personalAmount) || 0);
|
return sum + (parseFloat(split.personalAmount) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -125,7 +132,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Convert split amounts to CHF if needed
|
// Convert split amounts to CHF if needed
|
||||||
const convertedSplits = splits.map((split: any) => {
|
const convertedSplits = splits.map((split: SplitInput) => {
|
||||||
let convertedAmount = split.amount;
|
let convertedAmount = split.amount;
|
||||||
let convertedPersonalAmount = split.personalAmount;
|
let convertedPersonalAmount = split.personalAmount;
|
||||||
|
|
||||||
@@ -146,14 +153,14 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const splitPromises = convertedSplits.map((split: any) => {
|
const splitPromises = convertedSplits.map((split: { paymentId: unknown; username: string; amount: number; proportion?: number; personalAmount?: number }) => {
|
||||||
return PaymentSplit.create(split);
|
return PaymentSplit.create(split);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(splitPromises);
|
await Promise.all(splitPromises);
|
||||||
|
|
||||||
// Invalidate caches for all affected users
|
// Invalidate caches for all affected users
|
||||||
const affectedUsernames = splits.map((split: any) => split.username);
|
const affectedUsernames = splits.map((split: SplitInput) => split.username);
|
||||||
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
|
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export const GET: RequestHandler = async ({ params, locals }) => {
|
|||||||
await cache.set(cacheKey, JSON.stringify(result), 1800);
|
await cache.set(cacheKey, JSON.stringify(result), 1800);
|
||||||
|
|
||||||
return json(result);
|
return json(result);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
if (e.status === 404) throw e;
|
if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e;
|
||||||
throw error(500, 'Failed to fetch payment');
|
throw error(500, 'Failed to fetch payment');
|
||||||
} finally {
|
} finally {
|
||||||
// Connection will be reused
|
// Connection will be reused
|
||||||
@@ -89,7 +89,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
if (data.splits) {
|
if (data.splits) {
|
||||||
await PaymentSplit.deleteMany({ paymentId: id });
|
await PaymentSplit.deleteMany({ paymentId: id });
|
||||||
|
|
||||||
const splitPromises = data.splits.map((split: any) => {
|
const splitPromises = data.splits.map((split: { username: string; amount: number; proportion?: number; personalAmount?: number }) => {
|
||||||
return PaymentSplit.create({
|
return PaymentSplit.create({
|
||||||
paymentId: id,
|
paymentId: id,
|
||||||
username: split.username,
|
username: split.username,
|
||||||
@@ -100,7 +100,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(splitPromises);
|
await Promise.all(splitPromises);
|
||||||
newUsernames = data.splits.map((split: any) => split.username);
|
newUsernames = data.splits.map((split: { username: string }) => split.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate caches for all users (old and new)
|
// Invalidate caches for all users (old and new)
|
||||||
@@ -108,8 +108,8 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
await invalidateCospendCaches(allAffectedUsers, id);
|
await invalidateCospendCaches(allAffectedUsers, id);
|
||||||
|
|
||||||
return json({ success: true, payment: updatedPayment });
|
return json({ success: true, payment: updatedPayment });
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
if (e.status) throw e;
|
if (e && typeof e === 'object' && 'status' in e) throw e;
|
||||||
throw error(500, 'Failed to update payment');
|
throw error(500, 'Failed to update payment');
|
||||||
} finally {
|
} finally {
|
||||||
// Connection will be reused
|
// Connection will be reused
|
||||||
@@ -148,8 +148,8 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
|
|||||||
await invalidateCospendCaches(affectedUsernames, id);
|
await invalidateCospendCaches(affectedUsernames, id);
|
||||||
|
|
||||||
return json({ success: true });
|
return json({ success: true });
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
if (e.status) throw e;
|
if (e && typeof e === 'object' && 'status' in e) throw e;
|
||||||
throw error(500, 'Failed to delete payment');
|
throw error(500, 'Failed to delete payment');
|
||||||
} finally {
|
} finally {
|
||||||
// Connection will be reused
|
// Connection will be reused
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
import { RecurringPayment } from '$models/RecurringPayment';
|
import { RecurringPayment, type IRecurringPayment } from '$models/RecurringPayment';
|
||||||
import { dbConnect } from '$utils/db';
|
import { dbConnect } from '$utils/db';
|
||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
import { calculateNextExecutionDate, validateCronExpression } from '$lib/utils/recurring';
|
import { calculateNextExecutionDate, validateCronExpression } from '$lib/utils/recurring';
|
||||||
@@ -17,7 +17,7 @@ export const GET: RequestHandler = async ({ locals, url }) => {
|
|||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = {};
|
const query: Record<string, unknown> = {};
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query.isActive = true;
|
query.isActive = true;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
|
|
||||||
// Validate personal + equal split method
|
// Validate personal + equal split method
|
||||||
if (splitMethod === 'personal_equal' && splits) {
|
if (splitMethod === 'personal_equal' && splits) {
|
||||||
const totalPersonal = splits.reduce((sum: number, split: any) => {
|
const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => {
|
||||||
return sum + (parseFloat(split.personalAmount) || 0);
|
return sum + (parseFloat(split.personalAmount) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
...recurringPaymentData,
|
...recurringPaymentData,
|
||||||
frequency,
|
frequency,
|
||||||
cronExpression
|
cronExpression
|
||||||
} as any, recurringPaymentData.startDate);
|
} as IRecurringPayment, recurringPaymentData.startDate);
|
||||||
|
|
||||||
const recurringPayment = await RecurringPayment.create(recurringPaymentData);
|
const recurringPayment = await RecurringPayment.create(recurringPaymentData);
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
throw error(404, 'Recurring payment not found');
|
throw error(404, 'Recurring payment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: any = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (title !== undefined) updateData.title = title;
|
if (title !== undefined) updateData.title = title;
|
||||||
if (description !== undefined) updateData.description = description;
|
if (description !== undefined) updateData.description = description;
|
||||||
@@ -113,7 +113,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
|
|
||||||
// Validate personal + equal split method
|
// Validate personal + equal split method
|
||||||
if (splitMethod === 'personal_equal' && splits && amount) {
|
if (splitMethod === 'personal_equal' && splits && amount) {
|
||||||
const totalPersonal = splits.reduce((sum: number, split: any) => {
|
const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => {
|
||||||
return sum + (parseFloat(split.personalAmount) || 0);
|
return sum + (parseFloat(split.personalAmount) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
path: publicPath
|
path: publicPath
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
if (err.status) throw err;
|
if (err && typeof err === 'object' && 'status' in err) throw err;
|
||||||
console.error('Upload error:', err);
|
console.error('Upload error:', err);
|
||||||
throw error(500, 'Failed to upload file');
|
throw error(500, 'Failed to upload file');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
const offset = parseInt(url.searchParams.get('offset') || '0');
|
const offset = parseInt(url.searchParams.get('offset') || '0');
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
let query: any = { isActive: true };
|
let query: Record<string, unknown> = { isActive: true };
|
||||||
|
|
||||||
// Text search
|
// Text search
|
||||||
if (search) {
|
if (search) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
return json({ error: 'At least one exercise is required' }, { status: 400 });
|
return json({ error: 'At least one exercise is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: any = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
if (name) updateData.name = name;
|
if (name) updateData.name = name;
|
||||||
if (exercises) updateData.exercises = exercises;
|
if (exercises) updateData.exercises = exercises;
|
||||||
if (startTime) updateData.startTime = new Date(startTime);
|
if (startTime) updateData.startTime = new Date(startTime);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
|||||||
|
|
||||||
const includePublic = url.searchParams.get('include_public') === 'true';
|
const includePublic = url.searchParams.get('include_public') === 'true';
|
||||||
|
|
||||||
let query: any = {
|
let query: Record<string, unknown> = {
|
||||||
$or: [
|
$or: [
|
||||||
{ createdBy: session.user.nickname }
|
{ createdBy: session.user.nickname }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build query based on filter
|
// Build query based on filter
|
||||||
let query: any = { images: { $exists: true, $ne: [] } };
|
let query: Record<string, unknown> = { images: { $exists: true, $ne: [] } };
|
||||||
|
|
||||||
if (filter === 'missing') {
|
if (filter === 'missing') {
|
||||||
// Find recipes with images but missing alt text
|
// Find recipes with images but missing alt text
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { filter = 'missing', limit = 50 } = body;
|
const { filter = 'missing', limit = 50 } = body;
|
||||||
|
|
||||||
let query: any = { images: { $exists: true, $ne: [] } };
|
let query: Record<string, unknown> = { images: { $exists: true, $ne: [] } };
|
||||||
|
|
||||||
if (filter === 'missing') {
|
if (filter === 'missing') {
|
||||||
query = {
|
query = {
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ export const actions: Actions = {
|
|||||||
// Success - redirect to dashboard
|
// Success - redirect to dashboard
|
||||||
throw redirect(303, '/cospend');
|
throw redirect(303, '/cospend');
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (error.status === 303) throw error; // Re-throw redirect
|
if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === 303) throw error; // Re-throw redirect
|
||||||
|
|
||||||
console.error('Error creating payment:', error);
|
console.error('Error creating payment:', error);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
|
|||||||
@@ -114,13 +114,14 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
// Redirect back to dashboard on success
|
// Redirect back to dashboard on success
|
||||||
throw redirect(303, '/cospend');
|
throw redirect(303, '/cospend');
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (error.status === 303) {
|
if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === 303) {
|
||||||
throw error; // Re-throw redirect
|
throw error; // Re-throw redirect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
return fail(500, {
|
return fail(500, {
|
||||||
error: error.message,
|
error: message,
|
||||||
values: {
|
values: {
|
||||||
settlementType,
|
settlementType,
|
||||||
fromUser,
|
fromUser,
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ export type IngredientReference = {
|
|||||||
name: string;
|
name: string;
|
||||||
short_name: string;
|
short_name: string;
|
||||||
ingredients?: IngredientSection[];
|
ingredients?: IngredientSection[];
|
||||||
translations?: any;
|
translations?: {
|
||||||
|
en?: {
|
||||||
|
ingredients?: IngredientItem[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +75,11 @@ export type InstructionReference = {
|
|||||||
name: string;
|
name: string;
|
||||||
short_name: string;
|
short_name: string;
|
||||||
instructions?: InstructionSection[];
|
instructions?: InstructionSection[];
|
||||||
translations?: any;
|
translations?: {
|
||||||
|
en?: {
|
||||||
|
instructions?: InstructionItem[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* for SvelteKit form actions with progressive enhancement support.
|
* for SvelteKit form actions with progressive enhancement support.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IngredientItem, InstructionItem, TranslatedRecipeType, TranslationMetadata } from '$types/types';
|
||||||
|
|
||||||
export interface RecipeFormData {
|
export interface RecipeFormData {
|
||||||
// Basic fields
|
// Basic fields
|
||||||
name: string;
|
name: string;
|
||||||
@@ -22,8 +24,8 @@ export interface RecipeFormData {
|
|||||||
note?: string;
|
note?: string;
|
||||||
|
|
||||||
// Complex nested structures
|
// Complex nested structures
|
||||||
ingredients: any[];
|
ingredients: IngredientItem[];
|
||||||
instructions: any[];
|
instructions: InstructionItem[];
|
||||||
|
|
||||||
// Additional info
|
// Additional info
|
||||||
add_info: {
|
add_info: {
|
||||||
@@ -65,9 +67,9 @@ export interface RecipeFormData {
|
|||||||
|
|
||||||
// Translation data (optional)
|
// Translation data (optional)
|
||||||
translations?: {
|
translations?: {
|
||||||
en?: any;
|
en?: TranslatedRecipeType;
|
||||||
};
|
};
|
||||||
translationMetadata?: any;
|
translationMetadata?: TranslationMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +114,7 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData {
|
|||||||
const note = formData.get('note')?.toString();
|
const note = formData.get('note')?.toString();
|
||||||
|
|
||||||
// Complex nested structures (JSON-encoded)
|
// Complex nested structures (JSON-encoded)
|
||||||
let ingredients: any[] = [];
|
let ingredients: IngredientItem[] = [];
|
||||||
const ingredientsData = formData.get('ingredients_json')?.toString();
|
const ingredientsData = formData.get('ingredients_json')?.toString();
|
||||||
if (ingredientsData) {
|
if (ingredientsData) {
|
||||||
try {
|
try {
|
||||||
@@ -122,7 +124,7 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let instructions: any[] = [];
|
let instructions: InstructionItem[] = [];
|
||||||
const instructionsData = formData.get('instructions_json')?.toString();
|
const instructionsData = formData.get('instructions_json')?.toString();
|
||||||
if (instructionsData) {
|
if (instructionsData) {
|
||||||
try {
|
try {
|
||||||
@@ -265,7 +267,7 @@ export function validateRecipeData(data: RecipeFormData): string[] {
|
|||||||
* Detects which fields have changed between two recipe objects
|
* Detects which fields have changed between two recipe objects
|
||||||
* Used for edit forms to enable partial translation updates
|
* Used for edit forms to enable partial translation updates
|
||||||
*/
|
*/
|
||||||
export function detectChangedFields(original: any, current: any): string[] {
|
export function detectChangedFields(original: Record<string, unknown>, current: Record<string, unknown>): string[] {
|
||||||
const changedFields: string[] = [];
|
const changedFields: string[] = [];
|
||||||
|
|
||||||
// Simple field comparison
|
// Simple field comparison
|
||||||
@@ -347,8 +349,8 @@ export function parseSeasonData(formData: FormData): number[] {
|
|||||||
* Serializes complex recipe data for storage
|
* Serializes complex recipe data for storage
|
||||||
* Ensures all required fields are present and properly typed
|
* Ensures all required fields are present and properly typed
|
||||||
*/
|
*/
|
||||||
export function serializeRecipeForDatabase(data: RecipeFormData): any {
|
export function serializeRecipeForDatabase(data: RecipeFormData): Record<string, unknown> {
|
||||||
const recipe: any = {
|
const recipe: Record<string, unknown> = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
short_name: data.short_name,
|
short_name: data.short_name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
|||||||
@@ -459,8 +459,8 @@ class DeepLTranslationService {
|
|||||||
const changed: string[] = [];
|
const changed: string[] = [];
|
||||||
|
|
||||||
for (const field of fieldsToCheck) {
|
for (const field of fieldsToCheck) {
|
||||||
const oldValue = JSON.stringify((oldRecipe as any)[field] || '');
|
const oldValue = JSON.stringify(oldRecipe[field as keyof RecipeModelType] || '');
|
||||||
const newValue = JSON.stringify((newRecipe as any)[field] || '');
|
const newValue = JSON.stringify(newRecipe[field as keyof RecipeModelType] || '');
|
||||||
if (oldValue !== newValue) {
|
if (oldValue !== newValue) {
|
||||||
changed.push(field);
|
changed.push(field);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user