polish: Enhance demo data for Audit Logs and SOPs, fix photo delete bug
- Seed script now generates 100+ realistic audit logs - Added Policy, Training, Form, and Checklist document types to seed data - Fixed bug in photo deletion logic for older photos
This commit is contained in:
parent
337f1e6adb
commit
bfa3cd21cc
2 changed files with 67 additions and 15 deletions
|
|
@ -409,10 +409,12 @@ async function main() {
|
||||||
{ title: `${DEMO_PREFIX} SOP - Nutrient Mixing`, type: 'SOP', content: '# Nutrient Mixing (Front Row Ag)\n\n## Stock Preparation\n1. Part A: 24lbs per 16 gal\n2. Part B: 16lbs + PhosZyme per 16 gal' },
|
{ title: `${DEMO_PREFIX} SOP - Nutrient Mixing`, type: 'SOP', content: '# Nutrient Mixing (Front Row Ag)\n\n## Stock Preparation\n1. Part A: 24lbs per 16 gal\n2. Part B: 16lbs + PhosZyme per 16 gal' },
|
||||||
{ title: `${DEMO_PREFIX} SOP - Clone Processing`, type: 'SOP', content: '# Clone Processing\n\n## Receiving\n1. Inspect for pests\n2. Count and verify\n3. Log in METRC' },
|
{ title: `${DEMO_PREFIX} SOP - Clone Processing`, type: 'SOP', content: '# Clone Processing\n\n## Receiving\n1. Inspect for pests\n2. Count and verify\n3. Log in METRC' },
|
||||||
{ title: `${DEMO_PREFIX} SOP - Harvest Procedure`, type: 'SOP', content: '# Harvest Procedure\n\n## Steps\n1. Cut main stem\n2. Hang dry in Dry Room\n3. Monitor temp/humidity\n4. Trim when stems snap' },
|
{ title: `${DEMO_PREFIX} SOP - Harvest Procedure`, type: 'SOP', content: '# Harvest Procedure\n\n## Steps\n1. Cut main stem\n2. Hang dry in Dry Room\n3. Monitor temp/humidity\n4. Trim when stems snap' },
|
||||||
{ title: `${DEMO_PREFIX} SOP - Visitor Check-In`, type: 'POLICY', content: '# Visitor Check-In\n\n## Requirements\n1. Valid ID\n2. Signed NDA\n3. PPE issued' },
|
{ title: `${DEMO_PREFIX} Policy - Visitor Check-In`, type: 'POLICY', content: '# Visitor Check-In\n\n## Requirements\n1. Valid ID\n2. Signed NDA\n3. PPE issued' },
|
||||||
{ title: `${DEMO_PREFIX} SOP - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' },
|
{ title: `${DEMO_PREFIX} Policy - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' },
|
||||||
{ title: `${DEMO_PREFIX} HVAC Maintenance`, type: 'TRAINING', content: '# HVAC Maintenance\n\n## Quarterly Tasks\n- Replace filters\n- Check refrigerant\n- Clean coils' },
|
{ title: `${DEMO_PREFIX} Training - HVAC Maintenance`, type: 'TRAINING', content: '# HVAC Maintenance\n\n## Quarterly Tasks\n- Replace filters\n- Check refrigerant\n- Clean coils' },
|
||||||
{ title: `${DEMO_PREFIX} METRC Training Guide`, type: 'TRAINING', content: '# METRC Training\n\n## Logging Plants\n1. Scan package tag\n2. Enter plant count\n3. Verify location' },
|
{ title: `${DEMO_PREFIX} Training - METRC Guide`, type: 'TRAINING', content: '# METRC Training\n\n## Logging Plants\n1. Scan package tag\n2. Enter plant count\n3. Verify location' },
|
||||||
|
{ title: `${DEMO_PREFIX} Form - Incident Report`, type: 'FORM', content: '# Incident Report Form\n\n**Date:** ______\n**Location:** ______\n\n## Description of Incident\n(Please describe what happened, who was involved, and any immediate actions taken)\n\n\n## Witness Statements\n\n' },
|
||||||
|
{ title: `${DEMO_PREFIX} Checklist - Harvest Cleaning`, type: 'CHECKLIST', content: '# Harvest Cleaning Checklist\n\n- [ ] Remove all plant debris\n- [ ] Wipe down walls with H2O2\n- [ ] Clean and Sanitize trays\n- [ ] Sweep and Mop floors\n- [ ] Empty dehumidifiers' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const doc of documents) {
|
for (const doc of documents) {
|
||||||
|
|
@ -568,6 +570,10 @@ async function main() {
|
||||||
|
|
||||||
// ==================== AUDIT LOGS ====================
|
// ==================== AUDIT LOGS ====================
|
||||||
console.log('\n📋 Creating Audit Log Entries...');
|
console.log('\n📋 Creating Audit Log Entries...');
|
||||||
|
// ==================== AUDIT LOGS ====================
|
||||||
|
console.log('\n📋 Creating Audit Log Entries...');
|
||||||
|
|
||||||
|
// Base entries
|
||||||
const auditEntries = [
|
const auditEntries = [
|
||||||
{ userId: users.admin?.id, userName: 'Admin User', action: 'LOGIN', entity: 'User', entityName: 'Admin login from office', ipAddress: '192.168.1.50' },
|
{ userId: users.admin?.id, userName: 'Admin User', action: 'LOGIN', entity: 'User', entityName: 'Admin login from office', ipAddress: '192.168.1.50' },
|
||||||
{ userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Batch', entityId: Object.values(batches)[0]?.id, entityName: 'Created batch: Gorilla Glue #4 - B001' },
|
{ userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Batch', entityId: Object.values(batches)[0]?.id, entityName: 'Created batch: Gorilla Glue #4 - B001' },
|
||||||
|
|
@ -586,21 +592,58 @@ async function main() {
|
||||||
{ userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Role', entityName: 'Updated permissions for Grower role' },
|
{ userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Role', entityName: 'Updated permissions for Grower role' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < auditEntries.length; i++) {
|
// Generate ~100 random realistic entries
|
||||||
const entry = auditEntries[i];
|
const actions = ['LOGIN', 'ACCESS', 'UPDATE', 'CREATE', 'DELETE', 'EXPORT'];
|
||||||
const timestamp = daysAgo(i % 14);
|
const entities = ['Batch', 'Task', 'Document', 'Room', 'Visitor', 'Report'];
|
||||||
timestamp.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60));
|
const demoStaff = Object.values(users);
|
||||||
|
|
||||||
|
for (let i = 0; i < 120; i++) {
|
||||||
|
const user = demoStaff[Math.floor(Math.random() * demoStaff.length)];
|
||||||
|
const action = actions[Math.floor(Math.random() * actions.length)];
|
||||||
|
let entity = entities[Math.floor(Math.random() * entities.length)];
|
||||||
|
|
||||||
|
let entityName = `${entity} #${1000 + i}`;
|
||||||
|
let changes = null;
|
||||||
|
|
||||||
|
if (action === 'LOGIN') {
|
||||||
|
entity = 'User';
|
||||||
|
entityName = 'User Login';
|
||||||
|
} else if (action === 'ACCESS') {
|
||||||
|
entityName = `Accessed ${entity} details`;
|
||||||
|
} else if (action === 'UPDATE') {
|
||||||
|
changes = { status: { from: 'PENDING', to: 'COMPLETED' } };
|
||||||
|
entityName = `Updated ${entity} status`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysBack = Math.floor(Math.random() * 30);
|
||||||
|
const timestamp = daysAgo(daysBack);
|
||||||
|
timestamp.setHours(8 + Math.floor(Math.random() * 12), Math.floor(Math.random() * 60)); // Business hours
|
||||||
|
|
||||||
|
auditEntries.push({
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name.replace(DEMO_PREFIX + ' ', ''),
|
||||||
|
action,
|
||||||
|
entity,
|
||||||
|
entityName,
|
||||||
|
changes,
|
||||||
|
timestamp,
|
||||||
|
ipAddress: `192.168.1.${50 + Math.floor(Math.random() * 100)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by timestamp desc
|
||||||
|
auditEntries.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
|
||||||
|
for (const entry of auditEntries) {
|
||||||
await prisma.auditLog.create({
|
await prisma.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
...entry,
|
...entry,
|
||||||
changes: entry.changes ? JSON.stringify(entry.changes) : null,
|
changes: entry.changes ? JSON.stringify(entry.changes) : null,
|
||||||
metadata: null,
|
metadata: null
|
||||||
timestamp
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(` ✓ ${auditEntries.length} audit log entries`);
|
console.log(` ✓ ${auditEntries.length} audit log entries (15 fixed + 120 generated)`);
|
||||||
|
|
||||||
console.log('\n✨ Demo seeding complete!');
|
console.log('\n✨ Demo seeding complete!');
|
||||||
console.log('\nDemo Logins:');
|
console.log('\nDemo Logins:');
|
||||||
|
|
|
||||||
|
|
@ -249,19 +249,28 @@ export async function uploadRoutes(server: FastifyInstance) {
|
||||||
// Photos are stored as: /photos/{year}/{month}/{day}/{photoId}/
|
// Photos are stored as: /photos/{year}/{month}/{day}/{photoId}/
|
||||||
// We need to search for it
|
// We need to search for it
|
||||||
|
|
||||||
const date = new Date();
|
// Parse date from photoId (format: photo_TIMESTAMP_HASH)
|
||||||
|
const parts = photoId.split('_');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return reply.status(400).send({ error: 'Invalid photo ID format' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = parseInt(parts[1]);
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return reply.status(400).send({ error: 'Invalid photo timestamp' });
|
||||||
|
}
|
||||||
|
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
// Try today's date first (most common case)
|
|
||||||
let photoDir = path.join(STORAGE_PATH, `${year}/${month}/${day}`, photoId);
|
let photoDir = path.join(STORAGE_PATH, `${year}/${month}/${day}`, photoId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.access(photoDir);
|
await fs.access(photoDir);
|
||||||
} catch {
|
} catch {
|
||||||
// Photo not found in today's folder - would need to search
|
|
||||||
// For now, return not found
|
|
||||||
return reply.status(404).send({ error: 'Photo not found' });
|
return reply.status(404).send({ error: 'Photo not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue