feat: fix channel scanning bugs, add package tags, and kickstarters tab

Bug fixes:
- Fix channels not being scanned by paginating TDLib getChats (was only
  loading first batch, additional channels were unknown to TDLib)
- Add per-channel getChat pre-load as safety net before scanning
- Fix preview pictures not loading by checking previewData instead of
  previewMsgId for hasPreview flag
- Prevent previewMsgId from being set when preview download fails

Package Tags:
- Add tags Text[] column to Package with migration backfilling from
  channel categories
- Worker auto-inherits source channel category as initial tag
- Tag filter dropdown and Tags column in STL Files table
- Server actions for individual and bulk tag editing

Kickstarters Tab:
- New KickstarterHost, Kickstarter, and KickstarterPackage models
- Full CRUD with delivery status, payment status, host management
- Package linking (many-to-many with existing packages)
- Sidebar entry with Gift icon
- Table with search, filters, modal forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:17:44 +01:00
parent e2dd3bb9d0
commit 5fd341dfc4
23 changed files with 1375 additions and 32 deletions

View File

@@ -0,0 +1,97 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import type { DataTableSearchParams } from "@/types/table.types";
interface KickstarterSearchParams extends DataTableSearchParams {
delivery?: string;
payment?: string;
host?: string;
}
export async function getKickstarters(
userId: string,
params: KickstarterSearchParams
) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const where: Prisma.KickstarterWhereInput = {
userId,
...(params.search && {
OR: [
{
name: {
contains: params.search,
mode: "insensitive" as Prisma.QueryMode,
},
},
{
notes: {
contains: params.search,
mode: "insensitive" as Prisma.QueryMode,
},
},
],
}),
...(params.delivery && {
deliveryStatus: params.delivery as Prisma.EnumDeliveryStatusFilter,
}),
...(params.payment && {
paymentStatus: params.payment as Prisma.EnumPaymentStatusFilter,
}),
...(params.host && { hostId: params.host }),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.kickstarter.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
host: { select: { id: true, name: true } },
_count: { select: { packages: true } },
},
}),
prisma.kickstarter.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getKickstarterById(id: string, userId: string) {
return prisma.kickstarter.findFirst({
where: { id, userId },
include: {
host: { select: { id: true, name: true } },
packages: {
include: {
package: {
select: {
id: true,
fileName: true,
fileSize: true,
archiveType: true,
creator: true,
},
},
},
},
},
});
}
export async function getKickstarterHosts() {
return prisma.kickstarterHost.findMany({
orderBy: { name: "asc" },
include: { _count: { select: { kickstarters: true } } },
});
}