Improve expenses receipts UX and Coolify MinIO deployment.

Extract receipt UI components, add view/edit/create dialog modes with list receipt previews, add docker-compose.coolify.yml and clearer COOLIFY/S3 path-style guidance for Application + MinIO setups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 00:47:59 -04:00
co-authored by Cursor
parent 5e6e51337a
commit c267b3e1c8
15 changed files with 1236 additions and 361 deletions
+54 -3
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { eq, and, desc } from "drizzle-orm";
import { eq, and, desc, inArray } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import {
expenses,
@@ -132,16 +132,67 @@ export const expensesRouter = createTRPCRouter({
conditions.push(eq(expenses.businessId, input.businessId));
}
return await ctx.db.query.expenses.findMany({
const rows = await ctx.db.query.expenses.findMany({
where: and(...conditions),
with: {
client: true,
business: true,
invoice: true,
receipts: true,
},
orderBy: [desc(expenses.date)],
});
const expenseIds = rows.map((e) => e.id);
if (expenseIds.length === 0) return [];
const receiptMeta = await ctx.db
.select({
expenseId: expenseReceipts.expenseId,
id: expenseReceipts.id,
mimeType: expenseReceipts.mimeType,
originalFilename: expenseReceipts.originalFilename,
createdAt: expenseReceipts.createdAt,
})
.from(expenseReceipts)
.where(inArray(expenseReceipts.expenseId, expenseIds))
.orderBy(desc(expenseReceipts.createdAt));
const receiptStats = new Map<
string,
{
receiptCount: number;
receiptPreview: {
id: string;
mimeType: string;
originalFilename: string;
} | null;
}
>();
for (const receipt of receiptMeta) {
const existing = receiptStats.get(receipt.expenseId);
if (existing) {
existing.receiptCount += 1;
} else {
receiptStats.set(receipt.expenseId, {
receiptCount: 1,
receiptPreview: {
id: receipt.id,
mimeType: receipt.mimeType,
originalFilename: receipt.originalFilename,
},
});
}
}
return rows.map((expense) => {
const stats = receiptStats.get(expense.id);
return {
...expense,
receiptCount: stats?.receiptCount ?? 0,
receiptPreview: stats?.receiptPreview ?? null,
};
});
}),
getById: protectedProcedure