This commit is contained in:
xCyanGrizzly
2026-02-18 14:26:36 +01:00
commit 3a5726e82b
167 changed files with 104081 additions and 0 deletions

77145
src/data/catalog/paints.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
import { prisma } from "@/lib/prisma";
interface LowStockItem {
id: string;
name: string;
type: "filament" | "resin" | "paint";
colorHex: string;
remaining: number;
total: number;
percent: number;
}
interface RecentUsage {
id: string;
itemType: string;
amount: number;
unit: string;
notes: string | null;
createdAt: Date;
itemName: string;
}
export interface DashboardStats {
totalItems: number;
inventoryValue: number;
lowStockCount: number;
recentActivityCount: number;
lowStockItems: LowStockItem[];
recentUsage: RecentUsage[];
}
export async function getDashboardStats(
userId: string,
lowStockThreshold: number
): Promise<DashboardStats> {
const [filaments, resins, paints, usageLogs24h, recentLogs] = await Promise.all([
prisma.filament.findMany({
where: { userId, archived: false },
select: {
id: true,
name: true,
colorHex: true,
spoolWeight: true,
usedWeight: true,
cost: true,
},
}),
prisma.resin.findMany({
where: { userId, archived: false },
select: {
id: true,
name: true,
colorHex: true,
bottleSize: true,
usedML: true,
cost: true,
},
}),
prisma.paint.findMany({
where: { userId, archived: false },
select: {
id: true,
name: true,
colorHex: true,
volumeML: true,
usedML: true,
cost: true,
},
}),
prisma.usageLog.count({
where: {
userId,
createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
}),
prisma.usageLog.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 10,
include: {
filament: { select: { name: true } },
resin: { select: { name: true } },
paint: { select: { name: true } },
},
}),
]);
const totalItems = filaments.length + resins.length + paints.length;
const inventoryValue =
filaments.reduce((sum: number, f: { cost: number | null }) => sum + (f.cost ?? 0), 0) +
resins.reduce((sum: number, r: { cost: number | null }) => sum + (r.cost ?? 0), 0) +
paints.reduce((sum: number, p: { cost: number | null }) => sum + (p.cost ?? 0), 0);
const lowStockItems: LowStockItem[] = [];
for (const f of filaments) {
const remaining = f.spoolWeight - f.usedWeight;
const percent = f.spoolWeight > 0 ? (remaining / f.spoolWeight) * 100 : 0;
if (percent <= lowStockThreshold && percent > 0) {
lowStockItems.push({
id: f.id,
name: f.name,
type: "filament",
colorHex: f.colorHex,
remaining,
total: f.spoolWeight,
percent,
});
}
}
for (const r of resins) {
const remaining = r.bottleSize - r.usedML;
const percent = r.bottleSize > 0 ? (remaining / r.bottleSize) * 100 : 0;
if (percent <= lowStockThreshold && percent > 0) {
lowStockItems.push({
id: r.id,
name: r.name,
type: "resin",
colorHex: r.colorHex,
remaining,
total: r.bottleSize,
percent,
});
}
}
for (const p of paints) {
const remaining = p.volumeML - p.usedML;
const percent = p.volumeML > 0 ? (remaining / p.volumeML) * 100 : 0;
if (percent <= lowStockThreshold && percent > 0) {
lowStockItems.push({
id: p.id,
name: p.name,
type: "paint",
colorHex: p.colorHex,
remaining,
total: p.volumeML,
percent,
});
}
}
lowStockItems.sort((a, b) => a.percent - b.percent);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const recentUsage: RecentUsage[] = recentLogs.map((log: any) => ({
id: log.id,
itemType: log.itemType,
amount: log.amount,
unit: log.unit,
notes: log.notes,
createdAt: log.createdAt,
itemName:
log.filament?.name ?? log.resin?.name ?? log.paint?.name ?? "Unknown",
}));
return {
totalItems,
inventoryValue,
lowStockCount: lowStockItems.length,
recentActivityCount: usageLogs24h,
lowStockItems,
recentUsage,
};
}

View File

@@ -0,0 +1,81 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/generated/prisma";
import type { DataTableSearchParams } from "@/types/table.types";
interface FilamentSearchParams extends DataTableSearchParams {
material?: string | string[];
vendor?: string | string[];
location?: string | string[];
}
export async function getFilaments(userId: string, params: FilamentSearchParams) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const materials = Array.isArray(params.material)
? params.material
: params.material
? [params.material]
: [];
const vendorIds = Array.isArray(params.vendor)
? params.vendor
: params.vendor
? [params.vendor]
: [];
const locationIds = Array.isArray(params.location)
? params.location
: params.location
? [params.location]
: [];
const where: Prisma.FilamentWhereInput = {
userId,
archived: params.archived === "true" ? undefined : false,
...(params.search && {
OR: [
{ name: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ brand: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ color: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
],
}),
...(materials.length > 0 && { material: { in: materials } }),
...(vendorIds.length > 0 && { vendorId: { in: vendorIds } }),
...(locationIds.length > 0 && { locationId: { in: locationIds } }),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.filament.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
tags: { include: { tag: { select: { id: true, name: true } } } },
},
}),
prisma.filament.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getFilamentById(id: string, userId: string) {
return prisma.filament.findFirst({
where: { id, userId },
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
tags: { include: { tag: true } },
},
});
}

View File

@@ -0,0 +1,59 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/generated/prisma";
import type { DataTableSearchParams } from "@/types/table.types";
export async function getLocations(userId: string, params: DataTableSearchParams) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const where: Prisma.LocationWhereInput = {
userId,
archived: params.archived === "true" ? undefined : false,
...(params.search && {
OR: [
{ name: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ description: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
],
}),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.location.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
_count: { select: { filaments: true, resins: true, paints: true } },
},
}),
prisma.location.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getLocationById(id: string, userId: string) {
return prisma.location.findFirst({
where: { id, userId },
include: {
_count: { select: { filaments: true, resins: true, paints: true } },
},
});
}
export async function getLocationOptions(userId: string) {
return prisma.location.findMany({
where: { userId, archived: false },
select: { id: true, name: true },
orderBy: { name: "asc" },
});
}

81
src/data/paint.queries.ts Normal file
View File

@@ -0,0 +1,81 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/generated/prisma";
import type { DataTableSearchParams } from "@/types/table.types";
interface PaintSearchParams extends DataTableSearchParams {
finish?: string | string[];
vendor?: string | string[];
location?: string | string[];
}
export async function getPaints(userId: string, params: PaintSearchParams) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const finishes = Array.isArray(params.finish)
? params.finish
: params.finish
? [params.finish]
: [];
const vendorIds = Array.isArray(params.vendor)
? params.vendor
: params.vendor
? [params.vendor]
: [];
const locationIds = Array.isArray(params.location)
? params.location
: params.location
? [params.location]
: [];
const where: Prisma.PaintWhereInput = {
userId,
archived: params.archived === "true" ? undefined : false,
...(params.search && {
OR: [
{ name: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ brand: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ color: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ line: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
],
}),
...(finishes.length > 0 && { finish: { in: finishes } }),
...(vendorIds.length > 0 && { vendorId: { in: vendorIds } }),
...(locationIds.length > 0 && { locationId: { in: locationIds } }),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.paint.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
},
}),
prisma.paint.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getPaintById(id: string, userId: string) {
return prisma.paint.findFirst({
where: { id, userId },
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
tags: { include: { tag: true } },
},
});
}

80
src/data/resin.queries.ts Normal file
View File

@@ -0,0 +1,80 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/generated/prisma";
import type { DataTableSearchParams } from "@/types/table.types";
interface ResinSearchParams extends DataTableSearchParams {
resinType?: string | string[];
vendor?: string | string[];
location?: string | string[];
}
export async function getResins(userId: string, params: ResinSearchParams) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const resinTypes = Array.isArray(params.resinType)
? params.resinType
: params.resinType
? [params.resinType]
: [];
const vendorIds = Array.isArray(params.vendor)
? params.vendor
: params.vendor
? [params.vendor]
: [];
const locationIds = Array.isArray(params.location)
? params.location
: params.location
? [params.location]
: [];
const where: Prisma.ResinWhereInput = {
userId,
archived: params.archived === "true" ? undefined : false,
...(params.search && {
OR: [
{ name: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ brand: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ color: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
],
}),
...(resinTypes.length > 0 && { resinType: { in: resinTypes } }),
...(vendorIds.length > 0 && { vendorId: { in: vendorIds } }),
...(locationIds.length > 0 && { locationId: { in: locationIds } }),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.resin.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
},
}),
prisma.resin.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getResinById(id: string, userId: string) {
return prisma.resin.findFirst({
where: { id, userId },
include: {
vendor: { select: { id: true, name: true } },
location: { select: { id: true, name: true } },
tags: { include: { tag: true } },
},
});
}

View File

@@ -0,0 +1,43 @@
import { prisma } from "@/lib/prisma";
export async function getUserSettings(userId: string) {
let settings = await prisma.userSettings.findUnique({
where: { userId },
});
if (!settings) {
settings = await prisma.userSettings.create({
data: {
userId,
lowStockThreshold: 20,
currency: "EUR",
theme: "dark",
units: "metric",
},
});
}
return settings;
}
export async function updateUserSettings(
userId: string,
data: {
lowStockThreshold?: number;
currency?: string;
theme?: string;
units?: string;
}
) {
return prisma.userSettings.upsert({
where: { userId },
update: data,
create: {
userId,
lowStockThreshold: data.lowStockThreshold ?? 20,
currency: data.currency ?? "EUR",
theme: data.theme ?? "dark",
units: data.units ?? "metric",
},
});
}

View File

@@ -0,0 +1,59 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/generated/prisma";
import type { DataTableSearchParams } from "@/types/table.types";
export async function getVendors(userId: string, params: DataTableSearchParams) {
const page = Number(params.page) || 1;
const perPage = Number(params.perPage) || 20;
const skip = (page - 1) * perPage;
const where: Prisma.VendorWhereInput = {
userId,
archived: params.archived === "true" ? undefined : false,
...(params.search && {
OR: [
{ name: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
{ notes: { contains: params.search, mode: "insensitive" as Prisma.QueryMode } },
],
}),
};
const sortField = params.sort || "createdAt";
const sortOrder = params.order || "desc";
const [data, totalCount] = await Promise.all([
prisma.vendor.findMany({
where,
orderBy: { [sortField]: sortOrder },
skip,
take: perPage,
include: {
_count: { select: { filaments: true, resins: true, paints: true } },
},
}),
prisma.vendor.count({ where }),
]);
return {
data,
pageCount: Math.ceil(totalCount / perPage),
totalCount,
};
}
export async function getVendorById(id: string, userId: string) {
return prisma.vendor.findFirst({
where: { id, userId },
include: {
_count: { select: { filaments: true, resins: true, paints: true } },
},
});
}
export async function getVendorOptions(userId: string) {
return prisma.vendor.findMany({
where: { userId, archived: false },
select: { id: true, name: true },
orderBy: { name: "asc" },
});
}