UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

445 lines (383 loc) 14.6 kB
import mongoose from 'mongoose'; const bookingWorkflowSchema = new mongoose.Schema({ // Workflow Template Information name: { type: String, required: [true, 'Workflow name is required'], trim: true }, description: String, // Booking Type this workflow applies to bookingType: { type: String, enum: ['studio_session', 'makeover_session', 'equipment_rental', 'event_coverage', ], required: [true, 'Booking type is required'] }, // Workflow Configuration version: { type: String, default: '1.0' }, isActive: { type: Boolean, default: true }, isDefault: { type: Boolean, default: false }, // default workflow for this booking type // Workflow Stages Definition stages: [{ // Stage identification id: { type: String, required: true }, // unique within workflow name: { type: String, required: true }, description: String, order: { type: Number, required: true }, // Stage configuration isRequired: { type: Boolean, default: true }, canSkip: { type: Boolean, default: false }, autoComplete: { type: Boolean, default: false }, // auto-complete based on conditions // Time constraints estimatedDuration: Number, // minutes maxDuration: Number, // minutes // Dependencies dependencies: [String], // stage IDs that must be completed first // Conditions for stage activation/completion activationConditions: [{ field: String, // booking field to check operator: { type: String, enum: ['equals', 'not_equals', 'greater_than', 'less_than', 'contains'] }, value: mongoose.Schema.Types.Mixed, required: { type: Boolean, default: true } }], completionConditions: [{ field: String, operator: { type: String, enum: ['equals', 'not_equals', 'greater_than', 'less_than', 'contains'] }, value: mongoose.Schema.Types.Mixed, required: { type: Boolean, default: true } }], // Actions to perform when stage starts/completes actions: { onStart: [{ type: { type: String, enum: ['notification', 'email', 'status_update', 'assignment', 'reminder'] }, config: mongoose.Schema.Types.Mixed }], onComplete: [{ type: { type: String, enum: ['notification', 'email', 'status_update', 'assignment', 'reminder'] }, config: mongoose.Schema.Types.Mixed }] }, // Checklist items for this stage checklist: [{ id: String, item: { type: String, required: true }, description: String, isRequired: { type: Boolean, default: true }, category: String, // grouping checklist items estimatedTime: Number, // minutes assignedRole: String, // which role should complete this order: Number }], // Required roles/permissions for this stage requiredRoles: [String], // Stage-specific data collection dataFields: [{ name: String, type: { type: String, enum: ['text', 'number', 'boolean', 'date', 'select', 'multiselect'] }, required: { type: Boolean, default: false }, options: [String], // for select/multiselect validation: { min: Number, max: Number, pattern: String } }] }], // State Transitions transitions: [{ from: String, // stage ID or status to: String, // stage ID or status conditions: [{ field: String, operator: String, value: mongoose.Schema.Types.Mixed }], actions: [{ type: String, config: mongoose.Schema.Types.Mixed }] }], // Notification Templates notifications: [{ id: String, name: String, trigger: { type: String, enum: ['stage_start', 'stage_complete', 'workflow_start', 'workflow_complete', 'delay', 'error'] }, recipients: [{ type: String, enum: ['client', 'assigned_staff', 'all_admins', 'specific_role'] }], template: { subject: String, body: String, variables: [String] // available template variables }, channels: [{ type: String, enum: ['email', 'sms', 'push', 'in_app'] }] }], // SLA and Performance Metrics sla: { totalDuration: Number, // expected total workflow duration in hours criticalStages: [String], // stage IDs that are critical for SLA escalationRules: [{ condition: String, // when to escalate delayHours: Number, escalateTo: String, // role or specific user action: String }] }, // Workflow Analytics Configuration analytics: { trackMetrics: [String], // which metrics to track kpis: [{ name: String, calculation: String, // how to calculate this KPI target: Number, unit: String }] }, // Customization per booking conditions conditionalWorkflows: [{ conditions: [{ field: String, operator: String, value: mongoose.Schema.Types.Mixed }], modifications: { addStages: [mongoose.Schema.Types.Mixed], removeStages: [String], modifyStages: [{ stageId: String, changes: mongoose.Schema.Types.Mixed }] } }], // Integration Settings integrations: [{ service: String, // external service name trigger: String, // when to call the service config: mongoose.Schema.Types.Mixed, errorHandling: String }], // Admin and Metadata createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, lastModifiedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, tags: [String], notes: String }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // Workflow Instance Schema (for active bookings) const workflowInstanceSchema = new mongoose.Schema({ // Reference to booking and workflow template booking: { type: mongoose.Schema.Types.ObjectId, ref: 'Booking', required: true }, workflowTemplate: { type: mongoose.Schema.Types.ObjectId, ref: 'BookingWorkflow', required: true }, // Current state currentStage: String, // current stage ID status: { type: String, enum: ['not_started', 'in_progress', 'completed', 'cancelled', 'on_hold', 'error'], default: 'not_started' }, // Stage execution history stageHistory: [{ stageId: String, stageName: String, status: { type: String, enum: ['pending', 'in_progress', 'completed', 'skipped', 'error'] }, startedAt: Date, completedAt: Date, completedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, duration: Number, // actual duration in minutes notes: String, // Checklist completion checklist: [{ itemId: String, item: String, completed: { type: Boolean, default: false }, completedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, completedAt: Date, notes: String, timeSpent: Number // minutes }], // Stage-specific data collected data: mongoose.Schema.Types.Mixed, // Issues or delays issues: [{ description: String, reportedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, reportedAt: { type: Date, default: Date.now }, severity: { type: String, enum: ['low', 'medium', 'high', 'critical'] }, resolved: { type: Boolean, default: false }, resolvedAt: Date, resolvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, resolution: String }] }], // Overall workflow metrics metrics: { startedAt: Date, completedAt: Date, totalDuration: Number, // actual total duration in minutes delayedStages: Number, skippedStages: Number, issueCount: Number, slaStatus: { type: String, enum: ['on_track', 'at_risk', 'breached'] } }, // Escalations escalations: [{ reason: String, escalatedAt: { type: Date, default: Date.now }, escalatedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, resolved: { type: Boolean, default: false }, resolvedAt: Date, resolution: String }], // Notifications sent notificationHistory: [{ notificationId: String, sentAt: { type: Date, default: Date.now }, channel: String, recipient: String, status: { type: String, enum: ['sent', 'delivered', 'failed'] }, error: String }] }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // Indexes for BookingWorkflow bookingWorkflowSchema.index({ bookingType: 1, isActive: 1 }); bookingWorkflowSchema.index({ isDefault: 1, bookingType: 1 }); bookingWorkflowSchema.index({ name: 'text', description: 'text' }); // Indexes for WorkflowInstance workflowInstanceSchema.index({ booking: 1 }); workflowInstanceSchema.index({ status: 1, currentStage: 1 }); workflowInstanceSchema.index({ 'metrics.slaStatus': 1 }); workflowInstanceSchema.index({ createdAt: -1 }); // Virtuals for BookingWorkflow bookingWorkflowSchema.virtual('stageCount').get(function () { return this.stages.length; }); bookingWorkflowSchema.virtual('estimatedDuration').get(function () { return this.stages.reduce((total, stage) => total + (stage.estimatedDuration || 0), 0); }); // Virtuals for WorkflowInstance workflowInstanceSchema.virtual('completionPercentage').get(function () { if (!this.stageHistory.length) return 0; const completedStages = this.stageHistory.filter(stage => stage.status === 'completed').length; return Math.round((completedStages / this.stageHistory.length) * 100); }); workflowInstanceSchema.virtual('isDelayed').get(function () { return this.metrics.slaStatus === 'at_risk' || this.metrics.slaStatus === 'breached'; }); // Methods for BookingWorkflow bookingWorkflowSchema.methods.createInstance = function (bookingId) { const WorkflowInstance = mongoose.model('WorkflowInstance'); return new WorkflowInstance({ booking: bookingId, workflowTemplate: this._id, currentStage: this.stages[0]?.id, stageHistory: this.stages.map(stage => ({ stageId: stage.id, stageName: stage.name, status: 'pending', checklist: stage.checklist.map(item => ({ itemId: item.id, item: item.item, completed: false })) })) }); }; bookingWorkflowSchema.methods.validateStageOrder = function () { const orders = this.stages.map(stage => stage.order); const uniqueOrders = [...new Set(orders)]; return orders.length === uniqueOrders.length; }; // Methods for WorkflowInstance workflowInstanceSchema.methods.getCurrentStage = function () { return this.stageHistory.find(stage => stage.stageId === this.currentStage); }; workflowInstanceSchema.methods.completeStage = function (stageId, completedBy, notes = '', data = {}) { const stage = this.stageHistory.find(s => s.stageId === stageId); if (!stage) { throw new Error('Stage not found'); } stage.status = 'completed'; stage.completedAt = new Date(); stage.completedBy = completedBy; stage.notes = notes; stage.data = data; if (stage.startedAt) { stage.duration = Math.round((stage.completedAt - stage.startedAt) / (1000 * 60)); // minutes } // Move to next stage this.moveToNextStage(); return this.save(); }; workflowInstanceSchema.methods.moveToNextStage = function () { const currentStageIndex = this.stageHistory.findIndex(s => s.stageId === this.currentStage); const nextStage = this.stageHistory[currentStageIndex + 1]; if (nextStage) { this.currentStage = nextStage.stageId; nextStage.status = 'in_progress'; nextStage.startedAt = new Date(); } else { // Workflow completed this.status = 'completed'; this.metrics.completedAt = new Date(); if (this.metrics.startedAt) { this.metrics.totalDuration = Math.round((this.metrics.completedAt - this.metrics.startedAt) / (1000 * 60)); } } }; workflowInstanceSchema.methods.skipStage = function (stageId, reason, skippedBy) { const stage = this.stageHistory.find(s => s.stageId === stageId); if (!stage) { throw new Error('Stage not found'); } stage.status = 'skipped'; stage.completedAt = new Date(); stage.completedBy = skippedBy; stage.notes = `Skipped: ${reason}`; this.metrics.skippedStages = (this.metrics.skippedStages || 0) + 1; this.moveToNextStage(); return this.save(); }; workflowInstanceSchema.methods.reportIssue = function (stageId, description, reportedBy, severity = 'medium') { const stage = this.stageHistory.find(s => s.stageId === stageId); if (!stage) { throw new Error('Stage not found'); } stage.issues.push({ description, reportedBy, severity }); this.metrics.issueCount = (this.metrics.issueCount || 0) + 1; // Update SLA status if critical issue if (severity === 'critical') { this.metrics.slaStatus = 'breached'; } return this.save(); }; // Static methods bookingWorkflowSchema.statics.getDefaultWorkflow = function (bookingType) { return this.findOne({ bookingType, isDefault: true, isActive: true }); }; workflowInstanceSchema.statics.getDelayedWorkflows = function () { return this.find({ status: 'in_progress', 'metrics.slaStatus': { $in: ['at_risk', 'breached'] } }).populate('booking workflowTemplate'); }; // Export both models export const BookingWorkflow = mongoose.model('BookingWorkflow', bookingWorkflowSchema); export const WorkflowInstance = mongoose.model('WorkflowInstance', workflowInstanceSchema); export default BookingWorkflow;