270 lines
8.8 KiB
TypeScript
270 lines
8.8 KiB
TypeScript
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { config as loadEnv } from 'dotenv';
|
|
|
|
// Load repo-root .env so DATABASE_URL is visible when this script runs from
|
|
// any CWD (pnpm invokes it from packages/db).
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
loadEnv({ path: path.resolve(here, '../../../.env') });
|
|
|
|
const { PrismaClient, UserRole } = await import('@prisma/client');
|
|
const { hashSecret } = await import('../src/crypto.js');
|
|
|
|
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' },
|
|
{ email: 'op2@demo.local', pin: '2222' },
|
|
{ email: 'op3@demo.local', pin: '3333' },
|
|
];
|
|
|
|
const WORKSTATIONS = [
|
|
{ code: 'CTR04', name: 'Controlo 04', area: 'Montagem' },
|
|
{ code: 'QVN_RTL_2', name: 'Retificação Visual 2', area: 'Qualidade' },
|
|
{ code: 'MTG_01', name: 'Montagem 01', area: 'Montagem' },
|
|
];
|
|
|
|
async function main() {
|
|
// Idempotent: if a prior run created the demo tenant, wipe it and recreate.
|
|
// Cascade deletes on the relations handle the children.
|
|
const existing = await prisma.tenant.findFirst({ where: { name: DEMO_TENANT_NAME } });
|
|
if (existing) {
|
|
await prisma.tenant.delete({ where: { id: existing.id } });
|
|
}
|
|
|
|
const tenant = await prisma.tenant.create({
|
|
data: { name: DEMO_TENANT_NAME },
|
|
});
|
|
|
|
await prisma.user.create({
|
|
data: {
|
|
tenantId: tenant.id,
|
|
email: DEMO_ADMIN_EMAIL,
|
|
role: UserRole.ADMIN,
|
|
passwordHash: await hashSecret(DEMO_ADMIN_PASSWORD),
|
|
},
|
|
});
|
|
|
|
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: {
|
|
tenantId: tenant.id,
|
|
email: op.email,
|
|
role: UserRole.OPERATOR,
|
|
passwordHash: await hashSecret(op.pin),
|
|
},
|
|
});
|
|
}
|
|
|
|
await prisma.workstation.createMany({
|
|
data: WORKSTATIONS.map((ws) => ({ tenantId: tenant.id, ...ws })),
|
|
});
|
|
|
|
// Seed sample maintenance requests so the shift report isn't empty.
|
|
const [wsList, adminUser, op1User] = await Promise.all([
|
|
prisma.workstation.findMany({ where: { tenantId: tenant.id } }),
|
|
prisma.user.findFirst({ where: { tenantId: tenant.id, email: DEMO_ADMIN_EMAIL } }),
|
|
prisma.user.findFirst({ where: { tenantId: tenant.id, email: 'op1@demo.local' } }),
|
|
]);
|
|
|
|
if (wsList.length && adminUser && op1User) {
|
|
const now = new Date();
|
|
const ago = (minutes: number) => new Date(now.getTime() - minutes * 60_000);
|
|
|
|
// 6 sample requests spread across "today"
|
|
const samples = [
|
|
// RESOLVED — created 4h ago, claimed 7 min later, resolved 40 min after that
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[0]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Sensor de pressão com leitura incorreta.',
|
|
status: 'RESOLVED' as const,
|
|
createdAt: ago(240),
|
|
claimedAt: ago(233),
|
|
claimedByUserId: adminUser.id,
|
|
resolvedAt: ago(193),
|
|
resolvedByUserId: adminUser.id,
|
|
resolutionNote: 'Sensor substituído e calibrado.',
|
|
clientRequestId: '00000000-0000-0000-0000-000000000001',
|
|
},
|
|
// RESOLVED — created 3h ago, claimed 12 min later, resolved 55 min after
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[1 % wsList.length]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Correia de transporte partida na zona B.',
|
|
status: 'RESOLVED' as const,
|
|
createdAt: ago(180),
|
|
claimedAt: ago(168),
|
|
claimedByUserId: adminUser.id,
|
|
resolvedAt: ago(113),
|
|
resolvedByUserId: adminUser.id,
|
|
resolutionNote: 'Correia substituída.',
|
|
clientRequestId: '00000000-0000-0000-0000-000000000002',
|
|
},
|
|
// RESOLVED — created 2h ago, claimed 5 min later, resolved 20 min after
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[2 % wsList.length]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Falha no painel de controlo, sem resposta ao toque.',
|
|
status: 'RESOLVED' as const,
|
|
createdAt: ago(120),
|
|
claimedAt: ago(115),
|
|
claimedByUserId: adminUser.id,
|
|
resolvedAt: ago(95),
|
|
resolvedByUserId: adminUser.id,
|
|
resolutionNote: 'Reinício do painel resolveu a falha.',
|
|
clientRequestId: '00000000-0000-0000-0000-000000000003',
|
|
},
|
|
// CLAIMED — created 90 min ago, claimed 15 min later, not yet resolved
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[0]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Vibração excessiva no eixo principal.',
|
|
status: 'CLAIMED' as const,
|
|
createdAt: ago(90),
|
|
claimedAt: ago(75),
|
|
claimedByUserId: adminUser.id,
|
|
clientRequestId: '00000000-0000-0000-0000-000000000004',
|
|
},
|
|
// OPEN — created 45 min ago
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[1 % wsList.length]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Fuga de óleo hidráulico visível no piso.',
|
|
status: 'OPEN' as const,
|
|
createdAt: ago(45),
|
|
clientRequestId: '00000000-0000-0000-0000-000000000005',
|
|
},
|
|
// OPEN — created 10 min ago
|
|
{
|
|
tenantId: tenant.id,
|
|
workstationId: wsList[2 % wsList.length]!.id,
|
|
reportedByUserId: op1User.id,
|
|
description: 'Alarme sonoro ativo sem causa identificada.',
|
|
status: 'OPEN' as const,
|
|
createdAt: ago(10),
|
|
clientRequestId: '00000000-0000-0000-0000-000000000006',
|
|
},
|
|
];
|
|
|
|
for (const s of samples) {
|
|
await prisma.maintenanceRequest.create({ data: s });
|
|
}
|
|
|
|
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(
|
|
`Seed complete — tenant=${tenant.id} (${tenant.name})`,
|
|
);
|
|
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(' | ')}`,
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch((err) => {
|
|
console.error('Seed failed:', err);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|