MY QUALITY - First iteration
This commit is contained in:
@@ -11,7 +11,7 @@ import { logger } from './logger';
|
||||
export type SessionUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: 'ADMIN' | 'SUPERVISOR' | 'OPERATOR';
|
||||
role: 'ADMIN' | 'SUPERVISOR' | 'QUALITY' | 'OPERATOR';
|
||||
tenantId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { workstationRouter } from './workstation';
|
||||
import { userRouter } from './user';
|
||||
import { storageRouter } from './storage';
|
||||
import { maintenanceRequestRouter } from './maintenance-request';
|
||||
import { operatorSessionRouter } from './operator-session';
|
||||
import { qualityDefectRouter } from './quality-defect';
|
||||
|
||||
export const appRouter = router({
|
||||
ping: pingRouter,
|
||||
@@ -11,6 +13,8 @@ export const appRouter = router({
|
||||
user: userRouter,
|
||||
storage: storageRouter,
|
||||
maintenanceRequest: maintenanceRequestRouter,
|
||||
operatorSession: operatorSessionRouter,
|
||||
qualityDefect: qualityDefectRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import { protectedProcedure, router } from '../trpc';
|
||||
|
||||
const SESSION_INCLUDE = {
|
||||
workstation: { select: { id: true, code: true, name: true, area: true } },
|
||||
} as const;
|
||||
|
||||
// Prisma's interactive-transaction client does not support $extends, so
|
||||
// tenantId is injected manually into each where/data clause inside $transaction.
|
||||
|
||||
export const operatorSessionRouter = router({
|
||||
/** The authenticated user's active session (badge-in), or null if not badged in. */
|
||||
current: protectedProcedure.query(({ ctx }) => {
|
||||
return ctx.db.operatorSession.findFirst({
|
||||
where: { userId: ctx.user.id, endedAt: null },
|
||||
include: SESSION_INCLUDE,
|
||||
orderBy: { startedAt: 'desc' },
|
||||
});
|
||||
}),
|
||||
|
||||
/** Badge-in at a workstation. Ends any previous active session for this user. */
|
||||
start: protectedProcedure
|
||||
.input(z.object({ workstationId: z.string().cuid() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const tid = ctx.tenantId;
|
||||
const ws = await ctx.db.workstation.findFirst({
|
||||
where: { id: input.workstationId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!ws) throw new TRPCError({ code: 'NOT_FOUND', message: 'Workstation not found.' });
|
||||
|
||||
return ctx.prisma.$transaction(async (tx) => {
|
||||
await tx.operatorSession.updateMany({
|
||||
where: { tenantId: tid, userId: ctx.user.id, endedAt: null },
|
||||
data: { endedAt: new Date() },
|
||||
});
|
||||
return tx.operatorSession.create({
|
||||
data: { tenantId: tid, userId: ctx.user.id, workstationId: input.workstationId },
|
||||
include: SESSION_INCLUDE,
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
/** Badge-out: end the active session, if any. Idempotent. */
|
||||
end: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
await ctx.db.operatorSession.updateMany({
|
||||
where: { userId: ctx.user.id, endedAt: null },
|
||||
data: { endedAt: new Date() },
|
||||
});
|
||||
return { ok: true };
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import { protectedProcedure, requireRole, router } from '../trpc';
|
||||
|
||||
const photoKeySchema = z
|
||||
.string()
|
||||
.regex(/^tenants\/[a-z0-9-]+\/quality\/[a-z0-9-]+\.(jpg|jpeg|png|webp)$/);
|
||||
|
||||
const statusSchema = z.enum(['OPEN', 'ACKNOWLEDGED', 'CORRECTED']);
|
||||
|
||||
const DEFECT_INCLUDE = {
|
||||
workstation: { select: { id: true, code: true, name: true, area: true } },
|
||||
createdBy: { select: { id: true, email: true } },
|
||||
acknowledgedBy: { select: { id: true, email: true } },
|
||||
correctedBy: { select: { id: true, email: true } },
|
||||
} as const;
|
||||
|
||||
// Prisma's interactive-transaction client does not support $extends, so
|
||||
// tenantId is injected manually into each where/data clause inside $transaction.
|
||||
|
||||
export const qualityDefectRouter = router({
|
||||
/** QCP raises a defect against a workstation. */
|
||||
create: requireRole('QUALITY', 'ADMIN')
|
||||
.input(
|
||||
z.object({
|
||||
workstationId: z.string().cuid(),
|
||||
defectType: z.string().trim().min(1).max(100),
|
||||
location: z.string().trim().max(200).optional(),
|
||||
description: z.string().trim().min(3).max(1000),
|
||||
rfsCode: z.string().trim().max(100).optional(),
|
||||
photoKey: photoKeySchema.optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const tid = ctx.tenantId;
|
||||
return ctx.prisma.$transaction(async (tx) => {
|
||||
const defect = await tx.qualityDefect.create({
|
||||
data: {
|
||||
tenantId: tid,
|
||||
workstationId: input.workstationId,
|
||||
createdByUserId: ctx.user.id,
|
||||
defectType: input.defectType,
|
||||
location: input.location,
|
||||
description: input.description,
|
||||
rfsCode: input.rfsCode,
|
||||
photoKey: input.photoKey,
|
||||
},
|
||||
include: DEFECT_INCLUDE,
|
||||
});
|
||||
await tx.domainEvent.create({
|
||||
data: {
|
||||
tenantId: tid,
|
||||
aggregateType: 'QualityDefect',
|
||||
aggregateId: defect.id,
|
||||
eventType: 'created',
|
||||
payload: { workstationId: input.workstationId, createdByUserId: ctx.user.id },
|
||||
},
|
||||
});
|
||||
return defect;
|
||||
});
|
||||
}),
|
||||
|
||||
/** QCP / admin queue of defects across the plant. */
|
||||
queue: requireRole('QUALITY', 'ADMIN', 'SUPERVISOR')
|
||||
.input(
|
||||
z.object({
|
||||
statuses: z.array(statusSchema).optional(),
|
||||
limit: z.number().int().min(1).max(100).default(50),
|
||||
}),
|
||||
)
|
||||
.query(({ ctx, input }) => {
|
||||
return ctx.db.qualityDefect.findMany({
|
||||
where: { ...(input.statuses?.length ? { status: { in: input.statuses } } : {}) },
|
||||
include: DEFECT_INCLUDE,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: input.limit,
|
||||
});
|
||||
}),
|
||||
|
||||
/** Operator: defects routed to my active session's workstation. Empty if not badged in. */
|
||||
forMyStation: protectedProcedure
|
||||
.input(z.object({ statuses: z.array(statusSchema).optional() }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const session = await ctx.db.operatorSession.findFirst({
|
||||
where: { userId: ctx.user.id, endedAt: null },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
select: { workstationId: true },
|
||||
});
|
||||
if (!session) return [];
|
||||
const statuses = input?.statuses ?? (['OPEN', 'ACKNOWLEDGED'] as const);
|
||||
return ctx.db.qualityDefect.findMany({
|
||||
where: { workstationId: session.workstationId, status: { in: [...statuses] } },
|
||||
include: DEFECT_INCLUDE,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}),
|
||||
|
||||
/** Operator acknowledges a defect (OPEN -> ACKNOWLEDGED). */
|
||||
acknowledge: protectedProcedure
|
||||
.input(z.object({ id: z.string().cuid() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const tid = ctx.tenantId;
|
||||
return ctx.prisma.$transaction(async (tx) => {
|
||||
const existing = await tx.qualityDefect.findFirst({
|
||||
where: { id: input.id, tenantId: tid },
|
||||
select: { status: true },
|
||||
});
|
||||
if (!existing) throw new TRPCError({ code: 'NOT_FOUND' });
|
||||
if (existing.status !== 'OPEN') {
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: `Cannot acknowledge: status is ${existing.status}, expected OPEN.`,
|
||||
});
|
||||
}
|
||||
const updated = await tx.qualityDefect.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: 'ACKNOWLEDGED',
|
||||
acknowledgedByUserId: ctx.user.id,
|
||||
acknowledgedAt: new Date(),
|
||||
},
|
||||
include: DEFECT_INCLUDE,
|
||||
});
|
||||
await tx.domainEvent.create({
|
||||
data: {
|
||||
tenantId: tid,
|
||||
aggregateType: 'QualityDefect',
|
||||
aggregateId: input.id,
|
||||
eventType: 'acknowledged',
|
||||
payload: { acknowledgedByUserId: ctx.user.id },
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}),
|
||||
|
||||
/** Operator marks a defect corrected (ACKNOWLEDGED -> CORRECTED). */
|
||||
correct: protectedProcedure
|
||||
.input(z.object({ id: z.string().cuid(), correctionNote: z.string().trim().max(2000).optional() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const tid = ctx.tenantId;
|
||||
return ctx.prisma.$transaction(async (tx) => {
|
||||
const existing = await tx.qualityDefect.findFirst({
|
||||
where: { id: input.id, tenantId: tid },
|
||||
select: { status: true },
|
||||
});
|
||||
if (!existing) throw new TRPCError({ code: 'NOT_FOUND' });
|
||||
if (existing.status !== 'ACKNOWLEDGED') {
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: `Cannot correct: status is ${existing.status}, expected ACKNOWLEDGED.`,
|
||||
});
|
||||
}
|
||||
const updated = await tx.qualityDefect.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: 'CORRECTED',
|
||||
correctedByUserId: ctx.user.id,
|
||||
correctedAt: new Date(),
|
||||
correctionNote: input.correctionNote,
|
||||
},
|
||||
include: DEFECT_INCLUDE,
|
||||
});
|
||||
await tx.domainEvent.create({
|
||||
data: {
|
||||
tenantId: tid,
|
||||
aggregateType: 'QualityDefect',
|
||||
aggregateId: input.id,
|
||||
eventType: 'corrected',
|
||||
payload: {
|
||||
correctedByUserId: ctx.user.id,
|
||||
correctionNote: input.correctionNote ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -12,7 +12,7 @@ const EXT_MAP: Record<(typeof CONTENT_TYPES)[number], string> = {
|
||||
|
||||
const photoKeySchema = z
|
||||
.string()
|
||||
.regex(/^tenants\/[a-z0-9-]+\/maintenance\/[a-z0-9-]+\.(jpg|jpeg|png|webp)$/);
|
||||
.regex(/^tenants\/[a-z0-9-]+\/(maintenance|quality)\/[a-z0-9-]+\.(jpg|jpeg|png|webp)$/);
|
||||
|
||||
export const storageRouter = router({
|
||||
signPhotoUpload: protectedProcedure
|
||||
@@ -20,11 +20,14 @@ export const storageRouter = router({
|
||||
z.object({
|
||||
contentType: z.enum(CONTENT_TYPES),
|
||||
byteSize: z.number().int().min(1).max(10 * 1024 * 1024),
|
||||
// Logical bucket inside the tenant prefix. Defaults to 'maintenance' so
|
||||
// existing MAI CALL callers need no change; MY QUALITY uses 'quality'.
|
||||
category: z.enum(['maintenance', 'quality']).default('maintenance'),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const ext = EXT_MAP[input.contentType];
|
||||
const photoKey = `tenants/${ctx.tenantId}/maintenance/${randomUUID()}.${ext}`;
|
||||
const photoKey = `tenants/${ctx.tenantId}/${input.category}/${randomUUID()}.${ext}`;
|
||||
const storage = makeStorage();
|
||||
const { url: uploadUrl, expiresAt } = await storage.signPut(
|
||||
photoKey,
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "QualityDefectStatus" AS ENUM ('OPEN', 'ACKNOWLEDGED', 'CORRECTED');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "UserRole" ADD VALUE 'QUALITY';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OperatorSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"workstationId" TEXT NOT NULL,
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"endedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "OperatorSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "QualityDefect" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"workstationId" TEXT NOT NULL,
|
||||
"createdByUserId" TEXT NOT NULL,
|
||||
"defectType" TEXT NOT NULL,
|
||||
"location" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"rfsCode" TEXT,
|
||||
"photoKey" TEXT,
|
||||
"status" "QualityDefectStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"acknowledgedByUserId" TEXT,
|
||||
"acknowledgedAt" TIMESTAMP(3),
|
||||
"correctedByUserId" TEXT,
|
||||
"correctedAt" TIMESTAMP(3),
|
||||
"correctionNote" TEXT,
|
||||
|
||||
CONSTRAINT "QualityDefect_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorSession_tenantId_idx" ON "OperatorSession"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorSession_tenantId_userId_endedAt_idx" ON "OperatorSession"("tenantId", "userId", "endedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OperatorSession_tenantId_workstationId_endedAt_idx" ON "OperatorSession"("tenantId", "workstationId", "endedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "QualityDefect_tenantId_status_createdAt_idx" ON "QualityDefect"("tenantId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "QualityDefect_tenantId_workstationId_status_idx" ON "QualityDefect"("tenantId", "workstationId", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OperatorSession" ADD CONSTRAINT "OperatorSession_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OperatorSession" ADD CONSTRAINT "OperatorSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OperatorSession" ADD CONSTRAINT "OperatorSession_workstationId_fkey" FOREIGN KEY ("workstationId") REFERENCES "Workstation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QualityDefect" ADD CONSTRAINT "QualityDefect_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QualityDefect" ADD CONSTRAINT "QualityDefect_workstationId_fkey" FOREIGN KEY ("workstationId") REFERENCES "Workstation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QualityDefect" ADD CONSTRAINT "QualityDefect_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QualityDefect" ADD CONSTRAINT "QualityDefect_acknowledgedByUserId_fkey" FOREIGN KEY ("acknowledgedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QualityDefect" ADD CONSTRAINT "QualityDefect_correctedByUserId_fkey" FOREIGN KEY ("correctedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -16,6 +16,7 @@ datasource db {
|
||||
enum UserRole {
|
||||
ADMIN
|
||||
SUPERVISOR
|
||||
QUALITY
|
||||
OPERATOR
|
||||
}
|
||||
|
||||
@@ -25,6 +26,12 @@ enum MaintenanceRequestStatus {
|
||||
RESOLVED
|
||||
}
|
||||
|
||||
enum QualityDefectStatus {
|
||||
OPEN
|
||||
ACKNOWLEDGED
|
||||
CORRECTED
|
||||
}
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
@@ -34,6 +41,8 @@ model Tenant {
|
||||
workstations Workstation[]
|
||||
events DomainEvent[]
|
||||
maintenanceRequests MaintenanceRequest[]
|
||||
operatorSessions OperatorSession[]
|
||||
qualityDefects QualityDefect[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -52,6 +61,11 @@ model User {
|
||||
claimedRequests MaintenanceRequest[] @relation("claimed")
|
||||
resolvedRequests MaintenanceRequest[] @relation("resolved")
|
||||
|
||||
sessions OperatorSession[]
|
||||
createdDefects QualityDefect[] @relation("defectCreated")
|
||||
acknowledgedDefects QualityDefect[] @relation("defectAcknowledged")
|
||||
correctedDefects QualityDefect[] @relation("defectCorrected")
|
||||
|
||||
@@unique([tenantId, email])
|
||||
@@index([tenantId])
|
||||
}
|
||||
@@ -65,6 +79,8 @@ model Workstation {
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
maintenanceRequests MaintenanceRequest[]
|
||||
operatorSessions OperatorSession[]
|
||||
qualityDefects QualityDefect[]
|
||||
|
||||
@@unique([tenantId, code])
|
||||
@@index([tenantId])
|
||||
@@ -115,3 +131,58 @@ model MaintenanceRequest {
|
||||
@@index([tenantId, status, createdAt])
|
||||
@@index([tenantId, reportedByUserId])
|
||||
}
|
||||
|
||||
/// MY QUALITY — an operator's active binding to a workstation ("badge-in").
|
||||
/// At most one active session (endedAt == null) per user; starting a new one
|
||||
/// ends the previous. Quality defects route to whoever has the active session
|
||||
/// at the targeted workstation.
|
||||
model OperatorSession {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
userId String
|
||||
workstationId String
|
||||
startedAt DateTime @default(now())
|
||||
endedAt DateTime?
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
workstation Workstation @relation(fields: [workstationId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, userId, endedAt])
|
||||
@@index([tenantId, workstationId, endedAt])
|
||||
}
|
||||
|
||||
/// MY QUALITY — a quality defect raised by QCP against a workstation, routed to
|
||||
/// the operator currently bound there. Mirrors MaintenanceRequest but in the
|
||||
/// opposite direction (quality -> operator). State: OPEN -> ACKNOWLEDGED ->
|
||||
/// CORRECTED.
|
||||
model QualityDefect {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
workstationId String
|
||||
createdByUserId String
|
||||
defectType String
|
||||
location String?
|
||||
description String
|
||||
rfsCode String?
|
||||
photoKey String?
|
||||
status QualityDefectStatus @default(OPEN)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
acknowledgedByUserId String?
|
||||
acknowledgedAt DateTime?
|
||||
|
||||
correctedByUserId String?
|
||||
correctedAt DateTime?
|
||||
correctionNote String?
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
workstation Workstation @relation(fields: [workstationId], references: [id])
|
||||
createdBy User @relation("defectCreated", fields: [createdByUserId], references: [id])
|
||||
acknowledgedBy User? @relation("defectAcknowledged", fields: [acknowledgedByUserId], references: [id])
|
||||
correctedBy User? @relation("defectCorrected", fields: [correctedByUserId], references: [id])
|
||||
|
||||
@@index([tenantId, status, createdAt])
|
||||
@@index([tenantId, workstationId, status])
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ const prisma = new PrismaClient();
|
||||
const DEMO_TENANT_NAME = 'Demo Factory';
|
||||
const DEMO_ADMIN_EMAIL = 'admin@demo.local';
|
||||
const DEMO_ADMIN_PASSWORD = 'admin1234';
|
||||
const DEMO_QCP_EMAIL = 'qcp@demo.local';
|
||||
const DEMO_QCP_PASSWORD = 'qcp1234';
|
||||
|
||||
const OPERATORS = [
|
||||
{ email: 'op1@demo.local', pin: '1111' },
|
||||
@@ -49,6 +51,15 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
email: DEMO_QCP_EMAIL,
|
||||
role: UserRole.QUALITY,
|
||||
passwordHash: await hashSecret(DEMO_QCP_PASSWORD),
|
||||
},
|
||||
});
|
||||
|
||||
for (const op of OPERATORS) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
@@ -161,6 +172,77 @@ async function main() {
|
||||
}
|
||||
|
||||
console.warn(` pedidos de exemplo: ${samples.length} criados`);
|
||||
|
||||
// MY QUALITY — op1 is badged-in at the first workstation, and QCP has
|
||||
// raised a few defects there so the operator's alerts and the QCP queue
|
||||
// are non-empty on first boot.
|
||||
const station = wsList[0]!;
|
||||
const qcpUser = await prisma.user.findFirst({
|
||||
where: { tenantId: tenant.id, email: DEMO_QCP_EMAIL },
|
||||
});
|
||||
|
||||
await prisma.operatorSession.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
userId: op1User.id,
|
||||
workstationId: station.id,
|
||||
startedAt: ago(120),
|
||||
},
|
||||
});
|
||||
|
||||
if (qcpUser) {
|
||||
const defects = [
|
||||
// OPEN — operator hasn't seen it yet
|
||||
{
|
||||
tenantId: tenant.id,
|
||||
workstationId: station.id,
|
||||
createdByUserId: qcpUser.id,
|
||||
defectType: 'Aperto não conforme',
|
||||
location: 'Banco dianteiro esquerdo',
|
||||
description: 'Binário fora de especificação no parafuso da calha.',
|
||||
rfsCode: 'RFS-1042',
|
||||
status: 'OPEN' as const,
|
||||
createdAt: ago(8),
|
||||
},
|
||||
// ACKNOWLEDGED — operator saw it, correcting
|
||||
{
|
||||
tenantId: tenant.id,
|
||||
workstationId: station.id,
|
||||
createdByUserId: qcpUser.id,
|
||||
defectType: 'Clip em falta',
|
||||
location: 'Painel de porta traseira direita',
|
||||
description: 'Clip de fixação do painel ausente.',
|
||||
rfsCode: 'RFS-1043',
|
||||
status: 'ACKNOWLEDGED' as const,
|
||||
createdAt: ago(35),
|
||||
acknowledgedByUserId: op1User.id,
|
||||
acknowledgedAt: ago(30),
|
||||
},
|
||||
// CORRECTED — closed loop
|
||||
{
|
||||
tenantId: tenant.id,
|
||||
workstationId: station.id,
|
||||
createdByUserId: qcpUser.id,
|
||||
defectType: 'Risco na pintura',
|
||||
location: 'Capot',
|
||||
description: 'Risco superficial detetado no controlo visual.',
|
||||
rfsCode: 'RFS-1041',
|
||||
status: 'CORRECTED' as const,
|
||||
createdAt: ago(90),
|
||||
acknowledgedByUserId: op1User.id,
|
||||
acknowledgedAt: ago(85),
|
||||
correctedByUserId: op1User.id,
|
||||
correctedAt: ago(70),
|
||||
correctionNote: 'Polimento efetuado, defeito eliminado.',
|
||||
},
|
||||
];
|
||||
|
||||
for (const d of defects) {
|
||||
await prisma.qualityDefect.create({ data: d });
|
||||
}
|
||||
|
||||
console.warn(` defeitos de exemplo: ${defects.length} criados (op1 em ${station.code})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
@@ -169,6 +251,9 @@ async function main() {
|
||||
console.warn(
|
||||
` admin: ${DEMO_ADMIN_EMAIL} / ${DEMO_ADMIN_PASSWORD}`,
|
||||
);
|
||||
console.warn(
|
||||
` qcp: ${DEMO_QCP_EMAIL} / ${DEMO_QCP_PASSWORD}`,
|
||||
);
|
||||
console.warn(
|
||||
` operadores: ${OPERATORS.map((o) => `${o.email}=${o.pin}`).join(' | ')}`,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export { prisma, type DbClient } from './client';
|
||||
export { tenantScoped, type TenantScopedClient } from './tenant-extension';
|
||||
export { Prisma, UserRole, MaintenanceRequestStatus } from '@prisma/client';
|
||||
export type { User, Tenant, Workstation, DomainEvent, MaintenanceRequest } from '@prisma/client';
|
||||
export { Prisma, UserRole, MaintenanceRequestStatus, QualityDefectStatus } from '@prisma/client';
|
||||
export type {
|
||||
User,
|
||||
Tenant,
|
||||
Workstation,
|
||||
DomainEvent,
|
||||
MaintenanceRequest,
|
||||
OperatorSession,
|
||||
QualityDefect,
|
||||
} from '@prisma/client';
|
||||
export { hashSecret, verifySecret } from './crypto';
|
||||
|
||||
@@ -83,7 +83,14 @@ import type { PrismaClient } from '@prisma/client';
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
const TENANT_SCOPED_MODELS = ['User', 'Workstation', 'DomainEvent', 'MaintenanceRequest'] as const;
|
||||
const TENANT_SCOPED_MODELS = [
|
||||
'User',
|
||||
'Workstation',
|
||||
'DomainEvent',
|
||||
'MaintenanceRequest',
|
||||
'OperatorSession',
|
||||
'QualityDefect',
|
||||
] as const;
|
||||
type TenantScopedModel = (typeof TENANT_SCOPED_MODELS)[number];
|
||||
|
||||
function isTenantScoped(model: string | undefined): model is TenantScopedModel {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/config": "workspace:*",
|
||||
"@types/node": "22.19.19",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/config/tsconfig/library.json",
|
||||
"extends": "@repo/config/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
}
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user