747 lines
22 KiB
TypeScript
747 lines
22 KiB
TypeScript
/**
|
|
* Voxyz Proposal Service
|
|
* Core service for mission proposal creation, validation, and management
|
|
* Phase 10: Autonomous Architecture
|
|
*/
|
|
|
|
import {
|
|
MissionProposal,
|
|
MissionProposalInsert,
|
|
CapGateCheckResult,
|
|
CapGateViolation,
|
|
AIAnalysisSummary,
|
|
ProposalStatus,
|
|
ProposalServiceConfig
|
|
} from '@/lib/supabase/voxyz.types';
|
|
import { getServiceSupabase } from '@/lib/supabase/client';
|
|
import { Task } from '@/lib/data/tasks';
|
|
import { Project } from '@/lib/data/projects';
|
|
|
|
const DEFAULT_CONFIG: ProposalServiceConfig = {
|
|
maxProposalsPerDay: 3,
|
|
minProposalsPerDay: 1,
|
|
defaultProposalExpiryHours: 24,
|
|
priorityScoreThreshold: 50,
|
|
autoApproveThreshold: 85,
|
|
};
|
|
|
|
/**
|
|
* Priority scoring factors and their weights
|
|
*/
|
|
const PRIORITY_FACTORS = {
|
|
deadlineUrgency: 0.30,
|
|
taskPriority: 0.25,
|
|
projectImportance: 0.20,
|
|
blockedStatus: 0.15,
|
|
staleTask: 0.10,
|
|
};
|
|
|
|
/**
|
|
* Calculate priority score for a proposal
|
|
*/
|
|
export function calculatePriorityScore(
|
|
task: Task,
|
|
project: Project | undefined,
|
|
context: ProposalContext
|
|
): number {
|
|
let score = 50; // Base score
|
|
const factors: { factor: string; weight: number; contribution: number }[] = [];
|
|
|
|
// 1. Deadline urgency (30%)
|
|
const deadlineScore = calculateDeadlineScore(task, context);
|
|
const deadlineContribution = deadlineScore * PRIORITY_FACTORS.deadlineUrgency;
|
|
score += deadlineContribution;
|
|
factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineContribution });
|
|
|
|
// 2. Task priority (25%)
|
|
const priorityScore = calculateTaskPriorityScore(task);
|
|
const priorityContribution = priorityScore * PRIORITY_FACTORS.taskPriority;
|
|
score += priorityContribution;
|
|
factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityContribution });
|
|
|
|
// 3. Project importance (20%)
|
|
const projectScore = calculateProjectImportanceScore(project);
|
|
const projectContribution = projectScore * PRIORITY_FACTORS.projectImportance;
|
|
score += projectContribution;
|
|
factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectContribution });
|
|
|
|
// 4. Blocked status (15%)
|
|
const blockedScore = calculateBlockedScore(task, context);
|
|
const blockedContribution = blockedScore * PRIORITY_FACTORS.blockedStatus;
|
|
score += blockedContribution;
|
|
factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedContribution });
|
|
|
|
// 5. Stale task (10%)
|
|
const staleScore = calculateStaleScore(task, context);
|
|
const staleContribution = staleScore * PRIORITY_FACTORS.staleTask;
|
|
score += staleContribution;
|
|
factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleContribution });
|
|
|
|
return Math.min(100, Math.max(0, Math.round(score)));
|
|
}
|
|
|
|
/**
|
|
* Calculate deadline urgency score (0-100)
|
|
*/
|
|
function calculateDeadlineScore(task: Task, context: ProposalContext): number {
|
|
if (!task.dueDate) return 0;
|
|
|
|
const dueDate = new Date(task.dueDate);
|
|
const now = context.currentTime;
|
|
const hoursUntilDue = (dueDate.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
|
|
if (hoursUntilDue < 0) return 100; // Overdue
|
|
if (hoursUntilDue < 24) return 90; // Due within 24 hours
|
|
if (hoursUntilDue < 72) return 70; // Due within 3 days
|
|
if (hoursUntilDue < 168) return 50; // Due within a week
|
|
if (hoursUntilDue < 336) return 30; // Due within 2 weeks
|
|
return 10; // Due later
|
|
}
|
|
|
|
/**
|
|
* Calculate task priority score (0-100)
|
|
*/
|
|
function calculateTaskPriorityScore(task: Task): number {
|
|
const priorityMap: Record<string, number> = {
|
|
'urgent': 100,
|
|
'high': 75,
|
|
'medium': 50,
|
|
'low': 25,
|
|
};
|
|
return priorityMap[task.priority] || 50;
|
|
}
|
|
|
|
/**
|
|
* Calculate project importance score (0-100)
|
|
*/
|
|
function calculateProjectImportanceScore(project: Project | undefined): number {
|
|
if (!project) return 50;
|
|
|
|
// Check for high-priority keywords in project
|
|
const highPriorityKeywords = ['ios', 'revenue', 'critical', 'milestone'];
|
|
const projectNameLower = project.name.toLowerCase();
|
|
|
|
for (const keyword of highPriorityKeywords) {
|
|
if (projectNameLower.includes(keyword)) {
|
|
return 80;
|
|
}
|
|
}
|
|
|
|
return 50;
|
|
}
|
|
|
|
/**
|
|
* Calculate blocked status score (0-100)
|
|
*/
|
|
function calculateBlockedScore(task: Task, context: ProposalContext): number {
|
|
if (task.status === 'blocked') {
|
|
// Check how long it's been blocked
|
|
const blockedHours = task.comments?.reduce<number>((hours, comment) => {
|
|
const item = comment as { text?: unknown; createdAt?: unknown };
|
|
if (typeof item.text === 'string' && item.text.toLowerCase().includes('blocked') && typeof item.createdAt === 'string') {
|
|
const commentTime = new Date(item.createdAt);
|
|
const blockedHours = (context.currentTime.getTime() - commentTime.getTime()) / (1000 * 60 * 60);
|
|
return Math.max(hours, blockedHours);
|
|
}
|
|
return hours;
|
|
}, 0) || 0;
|
|
|
|
if (blockedHours > 48) return 100;
|
|
if (blockedHours > 24) return 80;
|
|
return 60;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Calculate stale task score (0-100)
|
|
*/
|
|
function calculateStaleScore(task: Task, context: ProposalContext): number {
|
|
const lastUpdate = new Date(task.updatedAt);
|
|
const daysSinceUpdate = (context.currentTime.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
if (task.status === 'in-progress' || task.status === 'todo') {
|
|
if (daysSinceUpdate > 14) return 100;
|
|
if (daysSinceUpdate > 7) return 75;
|
|
if (daysSinceUpdate > 3) return 50;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Generate AI analysis summary for a proposal
|
|
*/
|
|
export function generateAIAnalysis(
|
|
task: Task,
|
|
project: Project | undefined,
|
|
priorityScore: number,
|
|
factors: { factor: string; weight: number; contribution: number }[]
|
|
): AIAnalysisSummary {
|
|
const urgencyScore = calculateDeadlineScore(task, { currentTime: new Date() });
|
|
const impactScore = Math.round((priorityScore + urgencyScore) / 2);
|
|
|
|
// Determine effort estimate based on task complexity
|
|
const effortEstimate = estimateEffort(task);
|
|
|
|
// Generate reasoning
|
|
const reasoning = generateReasoning(task, project, factors);
|
|
|
|
// Identify risks
|
|
const risks = identifyRisks(task, project);
|
|
|
|
// Identify opportunities
|
|
const opportunities = identifyOpportunities(task, project);
|
|
|
|
return {
|
|
priority_factors: factors,
|
|
urgency_score: urgencyScore,
|
|
impact_score: impactScore,
|
|
effort_estimate: effortEstimate,
|
|
confidence: calculateConfidence(task, factors),
|
|
reasoning,
|
|
risks,
|
|
opportunities,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Estimate effort required for a task
|
|
*/
|
|
function estimateEffort(task: Task): 'low' | 'medium' | 'high' {
|
|
const description = (task.description || '').toLowerCase();
|
|
const title = task.title.toLowerCase();
|
|
|
|
// High effort indicators
|
|
const highEffortKeywords = ['refactor', 'architecture', 'redesign', 'migration', 'integrate', 'implement'];
|
|
for (const keyword of highEffortKeywords) {
|
|
if (description.includes(keyword) || title.includes(keyword)) {
|
|
return 'high';
|
|
}
|
|
}
|
|
|
|
// Medium effort indicators
|
|
const mediumEffortKeywords = ['update', 'modify', 'enhance', 'improve', 'fix', 'add'];
|
|
for (const keyword of mediumEffortKeywords) {
|
|
if (description.includes(keyword) || title.includes(keyword)) {
|
|
return 'medium';
|
|
}
|
|
}
|
|
|
|
return 'low';
|
|
}
|
|
|
|
/**
|
|
* Generate reasoning for the proposal
|
|
*/
|
|
function generateReasoning(
|
|
task: Task,
|
|
project: Project | undefined,
|
|
factors: { factor: string; weight: number; contribution: number }[]
|
|
): string {
|
|
const parts: string[] = [];
|
|
|
|
// Sort factors by contribution
|
|
const sortedFactors = [...factors].sort((a, b) => b.contribution - a.contribution);
|
|
|
|
// Add top factor
|
|
const topFactor = sortedFactors[0];
|
|
if (topFactor) {
|
|
parts.push(`Primary driver: ${topFactor.factor.replace('_', ' ')} (+${Math.round(topFactor.contribution)} points)`);
|
|
}
|
|
|
|
// Add deadline context
|
|
if (task.dueDate) {
|
|
const dueDate = new Date(task.dueDate);
|
|
const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
if (daysUntil <= 3) {
|
|
parts.push(`Urgent: Due in ${daysUntil} day${daysUntil !== 1 ? 's' : ''}`);
|
|
}
|
|
}
|
|
|
|
// Add project context
|
|
if (project) {
|
|
parts.push(`Part of ${project.name}`);
|
|
}
|
|
|
|
return parts.join('. ');
|
|
}
|
|
|
|
/**
|
|
* Identify risks for the task
|
|
*/
|
|
function identifyRisks(task: Task, project: Project | undefined): string[] {
|
|
const risks: string[] = [];
|
|
|
|
if (task.status === 'blocked') {
|
|
risks.push('Task is currently blocked');
|
|
}
|
|
|
|
if (task.dueDate) {
|
|
const dueDate = new Date(task.dueDate);
|
|
const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
if (daysUntil < 0) {
|
|
risks.push('Task is overdue');
|
|
} else if (daysUntil <= 2) {
|
|
risks.push('Very tight deadline');
|
|
}
|
|
}
|
|
|
|
if (task.priority === 'urgent') {
|
|
risks.push('High priority task may have dependencies');
|
|
}
|
|
|
|
return risks;
|
|
}
|
|
|
|
/**
|
|
* Identify opportunities for the task
|
|
*/
|
|
function identifyOpportunities(task: Task, project: Project | undefined): string[] {
|
|
const opportunities: string[] = [];
|
|
|
|
if (project?.name.toLowerCase().includes('ios')) {
|
|
opportunities.push('Contributes to iOS portfolio growth');
|
|
}
|
|
|
|
if (task.tags.some(tag => tag.toLowerCase().includes('revenue'))) {
|
|
opportunities.push('May impact revenue directly');
|
|
}
|
|
|
|
if (task.tags.some(tag => tag.toLowerCase().includes('milestone'))) {
|
|
opportunities.push('Advances key milestone');
|
|
}
|
|
|
|
return opportunities;
|
|
}
|
|
|
|
/**
|
|
* Calculate confidence in the proposal
|
|
*/
|
|
function calculateConfidence(task: Task, factors: { factor: string; weight: number; contribution: number }[]): number {
|
|
let confidence = 70; // Base confidence
|
|
|
|
// Boost confidence if we have clear signals
|
|
const hasDeadline = !!task.dueDate;
|
|
const hasClearPriority = task.priority === 'urgent' || task.priority === 'high';
|
|
const hasDescription = (task.description || '').length > 50;
|
|
|
|
if (hasDeadline) confidence += 10;
|
|
if (hasClearPriority) confidence += 10;
|
|
if (hasDescription) confidence += 5;
|
|
|
|
// Reduce confidence if factors are contradictory
|
|
const factorValues = factors.map(f => f.contribution);
|
|
const variance = calculateVariance(factorValues);
|
|
if (variance > 400) confidence -= 10; // High variance = conflicting signals
|
|
|
|
return Math.min(95, Math.max(50, confidence));
|
|
}
|
|
|
|
/**
|
|
* Calculate variance of an array
|
|
*/
|
|
function calculateVariance(values: number[]): number {
|
|
if (values.length === 0) return 0;
|
|
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
|
|
return squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
|
|
}
|
|
|
|
/**
|
|
* Create a mission proposal from a task
|
|
*/
|
|
export function createProposalFromTask(
|
|
task: Task,
|
|
project: Project | undefined,
|
|
context: ProposalContext,
|
|
config: ProposalServiceConfig = DEFAULT_CONFIG
|
|
): MissionProposalInsert {
|
|
const priorityScore = calculatePriorityScore(task, project, context);
|
|
const factors = getPriorityFactors(task, project, context);
|
|
const aiAnalysis = generateAIAnalysis(task, project, priorityScore, factors);
|
|
|
|
// Estimate duration based on effort
|
|
const durationMap: Record<string, number> = {
|
|
'low': 60,
|
|
'medium': 180,
|
|
'high': 360,
|
|
};
|
|
const estimatedDuration = durationMap[aiAnalysis.effort_estimate || 'medium'] || 120;
|
|
|
|
// Calculate suggested time slot
|
|
const suggestedStartTime = calculateSuggestedStartTime(context, estimatedDuration);
|
|
const suggestedEndTime = suggestedStartTime
|
|
? new Date(suggestedStartTime.getTime() + estimatedDuration * 60000)
|
|
: null;
|
|
|
|
// Generate rationale
|
|
const rationale = aiAnalysis.reasoning || `High priority task: ${task.title}`;
|
|
|
|
return {
|
|
proposal_date: context.currentTime.toISOString().split('T')[0],
|
|
status: 'pending',
|
|
priority_score: priorityScore,
|
|
passes_cap_gates: false, // Will be validated later
|
|
cap_gate_violations: [],
|
|
source_task_ids: [task.id],
|
|
source_project_ids: project ? [project.id] : [],
|
|
title: task.title,
|
|
description: task.description ?? null,
|
|
rationale,
|
|
ai_analysis: aiAnalysis,
|
|
suggested_start_time: suggestedStartTime?.toISOString() || null,
|
|
suggested_end_time: suggestedEndTime?.toISOString() || null,
|
|
estimated_duration_minutes: estimatedDuration,
|
|
approved_at: null,
|
|
approved_by: null,
|
|
rejected_at: null,
|
|
rejection_reason: null,
|
|
calendar_event_id: null,
|
|
calendar_synced_at: null,
|
|
executed_at: null,
|
|
execution_notes: null,
|
|
expires_at: new Date(context.currentTime.getTime() + config.defaultProposalExpiryHours * 60 * 60 * 1000).toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get priority factors for a task
|
|
*/
|
|
function getPriorityFactors(
|
|
task: Task,
|
|
project: Project | undefined,
|
|
context: ProposalContext
|
|
): { factor: string; weight: number; contribution: number }[] {
|
|
const factors: { factor: string; weight: number; contribution: number }[] = [];
|
|
|
|
const deadlineScore = calculateDeadlineScore(task, context);
|
|
factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineScore * PRIORITY_FACTORS.deadlineUrgency });
|
|
|
|
const priorityScore = calculateTaskPriorityScore(task);
|
|
factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityScore * PRIORITY_FACTORS.taskPriority });
|
|
|
|
const projectScore = calculateProjectImportanceScore(project);
|
|
factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectScore * PRIORITY_FACTORS.projectImportance });
|
|
|
|
const blockedScore = calculateBlockedScore(task, context);
|
|
factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedScore * PRIORITY_FACTORS.blockedStatus });
|
|
|
|
const staleScore = calculateStaleScore(task, context);
|
|
factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleScore * PRIORITY_FACTORS.staleTask });
|
|
|
|
return factors;
|
|
}
|
|
|
|
/**
|
|
* Calculate suggested start time for a proposal
|
|
*/
|
|
function calculateSuggestedStartTime(context: ProposalContext, durationMinutes: number): Date | null {
|
|
// Start with tomorrow at 9 AM
|
|
const tomorrow = new Date(context.currentTime);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
tomorrow.setHours(9, 0, 0, 0);
|
|
|
|
// TODO: Consider calendar availability from context
|
|
|
|
return tomorrow;
|
|
}
|
|
|
|
/**
|
|
* Proposal context for calculations
|
|
*/
|
|
export interface ProposalContext {
|
|
currentTime: Date;
|
|
activeTasks?: Task[];
|
|
recentProposals?: MissionProposal[];
|
|
calendarAvailability?: unknown[];
|
|
}
|
|
|
|
/**
|
|
* Validate a proposal against cap gates
|
|
*/
|
|
export async function validateProposalAgainstCapGates(
|
|
proposal: MissionProposalInsert,
|
|
existingProposals: MissionProposal[],
|
|
config: ProposalServiceConfig = DEFAULT_CONFIG
|
|
): Promise<CapGateCheckResult> {
|
|
const violations: CapGateViolation[] = [];
|
|
const checks: CapGateCheckResult['checks'] = [];
|
|
|
|
// Check 1: Daily mission limit
|
|
const todayProposals = existingProposals.filter(p =>
|
|
p.proposal_date === proposal.proposal_date &&
|
|
p.status !== 'rejected' &&
|
|
p.status !== 'expired'
|
|
);
|
|
|
|
const dailyLimitGate = {
|
|
gate: {
|
|
id: 'daily_limit',
|
|
name: 'Daily Mission Limit',
|
|
description: 'Maximum number of missions per day',
|
|
gate_type: 'daily_mission_limit' as const,
|
|
max_value: config.maxProposalsPerDay,
|
|
min_value: config.minProposalsPerDay,
|
|
unit: 'count',
|
|
current_value: todayProposals.length,
|
|
last_evaluated_at: new Date().toISOString(),
|
|
policy_id: null,
|
|
is_active: true,
|
|
is_blocking: false,
|
|
evaluation_window_hours: 24,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
created_by: null,
|
|
updated_by: null,
|
|
},
|
|
passed: todayProposals.length < config.maxProposalsPerDay,
|
|
currentValue: todayProposals.length,
|
|
};
|
|
|
|
if (!dailyLimitGate.passed) {
|
|
dailyLimitGate.gate.is_blocking = true;
|
|
violations.push({
|
|
gate_name: dailyLimitGate.gate.name,
|
|
gate_type: dailyLimitGate.gate.gate_type,
|
|
current_value: dailyLimitGate.currentValue,
|
|
max_value: config.maxProposalsPerDay,
|
|
violation_message: `Daily mission limit of ${config.maxProposalsPerDay} reached`,
|
|
});
|
|
}
|
|
|
|
checks.push(dailyLimitGate);
|
|
|
|
// Check 2: Workload capacity
|
|
const totalWorkload = todayProposals.reduce((sum, p) =>
|
|
sum + (p.estimated_duration_minutes || 120), 0
|
|
);
|
|
const proposedWorkload = totalWorkload + (proposal.estimated_duration_minutes || 120);
|
|
const maxWorkloadMinutes = 480; // 8 hours
|
|
|
|
const workloadGate = {
|
|
gate: {
|
|
id: 'workload_capacity',
|
|
name: 'Workload Capacity',
|
|
description: 'Daily workload capacity in minutes',
|
|
gate_type: 'workload_capacity' as const,
|
|
max_value: maxWorkloadMinutes,
|
|
min_value: 0,
|
|
unit: 'minutes',
|
|
current_value: proposedWorkload,
|
|
last_evaluated_at: new Date().toISOString(),
|
|
policy_id: null,
|
|
is_active: true,
|
|
is_blocking: false,
|
|
evaluation_window_hours: 24,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
created_by: null,
|
|
updated_by: null,
|
|
},
|
|
passed: proposedWorkload <= maxWorkloadMinutes,
|
|
currentValue: proposedWorkload,
|
|
};
|
|
|
|
if (!workloadGate.passed) {
|
|
workloadGate.gate.is_blocking = true;
|
|
violations.push({
|
|
gate_name: workloadGate.gate.name,
|
|
gate_type: workloadGate.gate.gate_type,
|
|
current_value: proposedWorkload,
|
|
max_value: maxWorkloadMinutes,
|
|
violation_message: `Workload would exceed ${maxWorkloadMinutes} minutes (${Math.round(proposedWorkload)} proposed)`,
|
|
});
|
|
}
|
|
|
|
checks.push(workloadGate);
|
|
|
|
// Check 3: Priority threshold
|
|
if (proposal.priority_score < config.priorityScoreThreshold) {
|
|
violations.push({
|
|
gate_name: 'Priority Threshold',
|
|
gate_type: 'prioritization',
|
|
current_value: proposal.priority_score,
|
|
max_value: config.priorityScoreThreshold,
|
|
violation_message: `Priority score ${proposal.priority_score} below threshold ${config.priorityScoreThreshold}`,
|
|
});
|
|
}
|
|
|
|
return {
|
|
allPassed: violations.length === 0,
|
|
checks,
|
|
violations,
|
|
summary: violations.length === 0
|
|
? 'All cap gates passed'
|
|
: `${violations.length} cap gate violation${violations.length !== 1 ? 's' : ''}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Save a proposal to the database
|
|
*/
|
|
export async function saveProposal(
|
|
proposal: MissionProposalInsert,
|
|
capGateResult: CapGateCheckResult
|
|
): Promise<MissionProposal | null> {
|
|
const supabase = getServiceSupabase();
|
|
|
|
const proposalWithCapGates: MissionProposalInsert = {
|
|
...proposal,
|
|
passes_cap_gates: capGateResult.allPassed,
|
|
cap_gate_violations: capGateResult.violations,
|
|
};
|
|
|
|
const { data, error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.insert(proposalWithCapGates)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error saving proposal:', error);
|
|
return null;
|
|
}
|
|
|
|
return data as MissionProposal;
|
|
}
|
|
|
|
/**
|
|
* Get proposals for a specific date
|
|
*/
|
|
export async function getProposalsForDate(date: Date): Promise<MissionProposal[]> {
|
|
const supabase = getServiceSupabase();
|
|
const dateString = date.toISOString().split('T')[0];
|
|
|
|
const { data, error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.select('*')
|
|
.eq('proposal_date', dateString)
|
|
.order('priority_score', { ascending: false });
|
|
|
|
if (error) {
|
|
console.error('Error fetching proposals:', error);
|
|
return [];
|
|
}
|
|
|
|
return (data || []) as MissionProposal[];
|
|
}
|
|
|
|
/**
|
|
* Approve a proposal
|
|
*/
|
|
export async function approveProposal(
|
|
proposalId: string,
|
|
userId: string
|
|
): Promise<boolean> {
|
|
const supabase = getServiceSupabase();
|
|
|
|
const { error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.update({
|
|
status: 'approved',
|
|
approved_at: new Date().toISOString(),
|
|
approved_by: userId,
|
|
})
|
|
.eq('id', proposalId);
|
|
|
|
if (error) {
|
|
console.error('Error approving proposal:', error);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reject a proposal
|
|
*/
|
|
export async function rejectProposal(
|
|
proposalId: string,
|
|
reason: string
|
|
): Promise<boolean> {
|
|
const supabase = getServiceSupabase();
|
|
|
|
const { error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.update({
|
|
status: 'rejected',
|
|
rejected_at: new Date().toISOString(),
|
|
rejection_reason: reason,
|
|
})
|
|
.eq('id', proposalId);
|
|
|
|
if (error) {
|
|
console.error('Error rejecting proposal:', error);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Mark proposal as completed
|
|
*/
|
|
export async function completeProposal(
|
|
proposalId: string,
|
|
notes?: string
|
|
): Promise<boolean> {
|
|
const supabase = getServiceSupabase();
|
|
|
|
const { error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.update({
|
|
status: 'completed',
|
|
executed_at: new Date().toISOString(),
|
|
execution_notes: notes || null,
|
|
})
|
|
.eq('id', proposalId);
|
|
|
|
if (error) {
|
|
console.error('Error completing proposal:', error);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get top proposals for today
|
|
*/
|
|
export async function getTopProposalsForToday(limit: number = 3): Promise<MissionProposal[]> {
|
|
const today = new Date();
|
|
const proposals = await getProposalsForDate(today);
|
|
|
|
// Filter to pending and approved proposals
|
|
const activeProposals = proposals.filter(p =>
|
|
p.status === 'pending' || p.status === 'approved'
|
|
);
|
|
|
|
// Sort by priority score
|
|
activeProposals.sort((a, b) => b.priority_score - a.priority_score);
|
|
|
|
return activeProposals.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Expire old proposals
|
|
*/
|
|
export async function expireOldProposals(): Promise<number> {
|
|
const supabase = getServiceSupabase();
|
|
const now = new Date().toISOString();
|
|
|
|
const { data, error } = await supabase
|
|
.from('ops_mission_proposals')
|
|
.update({ status: 'expired' })
|
|
.lt('expires_at', now)
|
|
.eq('status', 'pending')
|
|
.select('id');
|
|
|
|
if (error) {
|
|
console.error('Error expiring proposals:', error);
|
|
return 0;
|
|
}
|
|
|
|
return data?.length || 0;
|
|
}
|