Compare commits
	
		
			5 Commits
		
	
	
		
			developmen
			...
			developmen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8c2276de79 | ||
| 
						 | 
					123dfc606c | ||
| 795c04fc5a | |||
| 
						 | 
					2cfec627bf | ||
| 
						 | 
					83dde53555 | 
@@ -1,2 +0,0 @@
 | 
			
		||||
[net]
 | 
			
		||||
git-fetch-with-cli = true
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
# Environment Variables Template
 | 
			
		||||
# Copy this file to '.env' and customize with your own values
 | 
			
		||||
# This file should NOT be committed to version control
 | 
			
		||||
 | 
			
		||||
# Server Configuration
 | 
			
		||||
# APP__SERVER__HOST=127.0.0.1
 | 
			
		||||
# APP__SERVER__PORT=9999
 | 
			
		||||
 | 
			
		||||
# Stripe Configuration (Test Keys)
 | 
			
		||||
# Get your test keys from: https://dashboard.stripe.com/test/apikeys
 | 
			
		||||
# APP__STRIPE__PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE
 | 
			
		||||
# APP__STRIPE__SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE
 | 
			
		||||
# APP__STRIPE__WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET_HERE
 | 
			
		||||
 | 
			
		||||
# For production, use live keys:
 | 
			
		||||
# APP__STRIPE__PUBLISHABLE_KEY=pk_live_YOUR_LIVE_PUBLISHABLE_KEY
 | 
			
		||||
# APP__STRIPE__SECRET_KEY=sk_live_YOUR_LIVE_SECRET_KEY
 | 
			
		||||
# APP__STRIPE__WEBHOOK_SECRET=whsec_YOUR_LIVE_WEBHOOK_SECRET
 | 
			
		||||
 | 
			
		||||
# Database Configuration (if needed)
 | 
			
		||||
# DATABASE_URL=postgresql://user:password@localhost/dbname
 | 
			
		||||
							
								
								
									
										53
									
								
								actix_mvc_app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										53
									
								
								actix_mvc_app/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,53 +0,0 @@
 | 
			
		||||
# Rust build artifacts
 | 
			
		||||
/target/
 | 
			
		||||
Cargo.lock
 | 
			
		||||
 | 
			
		||||
# Environment files
 | 
			
		||||
.env
 | 
			
		||||
.env.local
 | 
			
		||||
.env.production
 | 
			
		||||
 | 
			
		||||
# Local configuration files
 | 
			
		||||
config/local.toml
 | 
			
		||||
config/production.toml
 | 
			
		||||
 | 
			
		||||
# Database files
 | 
			
		||||
data/*.db
 | 
			
		||||
data/*.sqlite
 | 
			
		||||
data/*.json
 | 
			
		||||
 | 
			
		||||
# Log files
 | 
			
		||||
logs/
 | 
			
		||||
*.log
 | 
			
		||||
 | 
			
		||||
# IDE files
 | 
			
		||||
.vscode/
 | 
			
		||||
.idea/
 | 
			
		||||
*.swp
 | 
			
		||||
*.swo
 | 
			
		||||
 | 
			
		||||
# OS files
 | 
			
		||||
.DS_Store
 | 
			
		||||
Thumbs.db
 | 
			
		||||
 | 
			
		||||
# Temporary files
 | 
			
		||||
tmp/
 | 
			
		||||
temp/
 | 
			
		||||
 | 
			
		||||
# SSL certificates (keep examples)
 | 
			
		||||
nginx/ssl/*.pem
 | 
			
		||||
nginx/ssl/*.key
 | 
			
		||||
!nginx/ssl/README.md
 | 
			
		||||
 | 
			
		||||
# Docker volumes
 | 
			
		||||
docker-data/
 | 
			
		||||
 | 
			
		||||
# Backup files
 | 
			
		||||
*.bak
 | 
			
		||||
*.backup
 | 
			
		||||
 | 
			
		||||
# Keep important development files
 | 
			
		||||
!ai_prompt/
 | 
			
		||||
!PRODUCTION_DEPLOYMENT.md
 | 
			
		||||
!STRIPE_SETUP.md
 | 
			
		||||
!payment_plan.md
 | 
			
		||||
							
								
								
									
										1231
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1231
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,14 +3,6 @@ name = "actix_mvc_app"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[lib]
 | 
			
		||||
name = "actix_mvc_app"
 | 
			
		||||
path = "src/lib.rs"
 | 
			
		||||
 | 
			
		||||
[[bin]]
 | 
			
		||||
name = "actix_mvc_app"
 | 
			
		||||
path = "src/main.rs"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
actix-multipart = "0.6.1"
 | 
			
		||||
futures-util = "0.3.30"
 | 
			
		||||
@@ -23,8 +15,6 @@ env_logger = "0.11.2"
 | 
			
		||||
log = "0.4.21"
 | 
			
		||||
dotenv = "0.15.0"
 | 
			
		||||
chrono = { version = "0.4.35", features = ["serde"] }
 | 
			
		||||
heromodels = { path = "../../db/heromodels" }
 | 
			
		||||
heromodels_core = { path = "../../db/heromodels_core" }
 | 
			
		||||
config = "0.14.0"
 | 
			
		||||
num_cpus = "1.16.0"
 | 
			
		||||
futures = "0.3.30"
 | 
			
		||||
@@ -37,24 +27,3 @@ redis = { version = "0.23.0", features = ["tokio-comp"] }
 | 
			
		||||
jsonwebtoken = "8.3.0"
 | 
			
		||||
pulldown-cmark = "0.13.0"
 | 
			
		||||
urlencoding = "2.1.3"
 | 
			
		||||
 | 
			
		||||
tokio = { version = "1.0", features = ["full"] }
 | 
			
		||||
async-stripe = { version = "0.41", features = ["runtime-tokio-hyper"] }
 | 
			
		||||
reqwest = { version = "0.12.20", features = ["json"] }
 | 
			
		||||
 | 
			
		||||
# Security dependencies for webhook verification
 | 
			
		||||
hmac = "0.12.1"
 | 
			
		||||
sha2 = "0.10.8"
 | 
			
		||||
hex = "0.4.3"
 | 
			
		||||
 | 
			
		||||
# Validation dependencies
 | 
			
		||||
regex = "1.10.2"
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
# Testing dependencies
 | 
			
		||||
tokio-test = "0.4.3"
 | 
			
		||||
 | 
			
		||||
[patch."https://git.ourworld.tf/herocode/db.git"]
 | 
			
		||||
rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" }
 | 
			
		||||
rhai_wrapper = { path = "../../rhaj/rhai_wrapper" }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
# Multi-stage build for production
 | 
			
		||||
FROM rust:1.75-slim as builder
 | 
			
		||||
 | 
			
		||||
# Install system dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
    pkg-config \
 | 
			
		||||
    libssl-dev \
 | 
			
		||||
    libpq-dev \
 | 
			
		||||
    curl \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Create app directory
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# Copy dependency files
 | 
			
		||||
COPY Cargo.toml Cargo.lock ./
 | 
			
		||||
 | 
			
		||||
# Create a dummy main.rs to build dependencies
 | 
			
		||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
 | 
			
		||||
 | 
			
		||||
# Build dependencies (this layer will be cached)
 | 
			
		||||
RUN cargo build --release && rm -rf src
 | 
			
		||||
 | 
			
		||||
# Copy source code
 | 
			
		||||
COPY src ./src
 | 
			
		||||
COPY tests ./tests
 | 
			
		||||
 | 
			
		||||
# Build the application
 | 
			
		||||
RUN cargo build --release
 | 
			
		||||
 | 
			
		||||
# Runtime stage
 | 
			
		||||
FROM debian:bookworm-slim
 | 
			
		||||
 | 
			
		||||
# Install runtime dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
    ca-certificates \
 | 
			
		||||
    libssl3 \
 | 
			
		||||
    libpq5 \
 | 
			
		||||
    curl \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# Create app user
 | 
			
		||||
RUN useradd -m -u 1001 appuser
 | 
			
		||||
 | 
			
		||||
# Create app directory
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# Copy binary from builder stage
 | 
			
		||||
COPY --from=builder /app/target/release/actix_mvc_app /app/actix_mvc_app
 | 
			
		||||
 | 
			
		||||
# Copy static files and templates
 | 
			
		||||
COPY src/views ./src/views
 | 
			
		||||
COPY static ./static
 | 
			
		||||
 | 
			
		||||
# Create data and logs directories
 | 
			
		||||
RUN mkdir -p data logs && chown -R appuser:appuser /app
 | 
			
		||||
 | 
			
		||||
# Switch to app user
 | 
			
		||||
USER appuser
 | 
			
		||||
 | 
			
		||||
# Expose port
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
 | 
			
		||||
# Health check
 | 
			
		||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
 | 
			
		||||
    CMD curl -f http://localhost:8080/health || exit 1
 | 
			
		||||
 | 
			
		||||
# Run the application
 | 
			
		||||
CMD ["./actix_mvc_app"]
 | 
			
		||||
@@ -1,180 +0,0 @@
 | 
			
		||||
# Production Checklist ✅
 | 
			
		||||
 | 
			
		||||
## 🧹 Code Cleanup Status
 | 
			
		||||
 | 
			
		||||
### ✅ **Completed**
 | 
			
		||||
- [x] Removed build artifacts (`cargo clean`)
 | 
			
		||||
- [x] Updated .gitignore to keep `ai_prompt/` folder
 | 
			
		||||
- [x] Created proper .gitignore for actix_mvc_app
 | 
			
		||||
- [x] Cleaned up debug console.log statements (kept error logs)
 | 
			
		||||
- [x] Commented out verbose debug logging
 | 
			
		||||
- [x] Maintained essential error handling logs
 | 
			
		||||
 | 
			
		||||
### 🔧 **Configuration**
 | 
			
		||||
- [x] Environment variables properly configured
 | 
			
		||||
- [x] Stripe keys configured (test/production)
 | 
			
		||||
- [x] Database connection settings
 | 
			
		||||
- [x] CORS settings for production domains
 | 
			
		||||
- [x] SSL/TLS configuration ready
 | 
			
		||||
 | 
			
		||||
### 🛡️ **Security**
 | 
			
		||||
- [x] Stripe webhook signature verification
 | 
			
		||||
- [x] Input validation on all forms
 | 
			
		||||
- [x] SQL injection prevention (using ORM)
 | 
			
		||||
- [x] XSS protection (template escaping)
 | 
			
		||||
- [x] CSRF protection implemented
 | 
			
		||||
- [x] Rate limiting configured
 | 
			
		||||
 | 
			
		||||
### 📊 **Database**
 | 
			
		||||
- [x] Database corruption recovery implemented
 | 
			
		||||
- [x] Proper error handling for DB operations
 | 
			
		||||
- [x] Company status transitions working
 | 
			
		||||
- [x] Payment integration with company creation
 | 
			
		||||
- [x] Data validation and constraints
 | 
			
		||||
 | 
			
		||||
### 💳 **Payment System**
 | 
			
		||||
- [x] Stripe Elements integration
 | 
			
		||||
- [x] Payment intent creation
 | 
			
		||||
- [x] Webhook handling for payment confirmation
 | 
			
		||||
- [x] Company activation on successful payment
 | 
			
		||||
- [x] Error handling for failed payments
 | 
			
		||||
- [x] Test card validation working
 | 
			
		||||
 | 
			
		||||
### 🎨 **User Interface**
 | 
			
		||||
- [x] Multi-step form validation
 | 
			
		||||
- [x] Real-time form saving to localStorage
 | 
			
		||||
- [x] Payment section hidden until ready
 | 
			
		||||
- [x] Comprehensive error messages
 | 
			
		||||
- [x] Loading states and progress indicators
 | 
			
		||||
- [x] Mobile-responsive design
 | 
			
		||||
 | 
			
		||||
## 🚀 **Pre-Deployment Steps**
 | 
			
		||||
 | 
			
		||||
### **1. Environment Setup**
 | 
			
		||||
```bash
 | 
			
		||||
# Set production environment variables
 | 
			
		||||
export RUST_ENV=production
 | 
			
		||||
export STRIPE_PUBLISHABLE_KEY=pk_live_...
 | 
			
		||||
export STRIPE_SECRET_KEY=sk_live_...
 | 
			
		||||
export STRIPE_WEBHOOK_SECRET=whsec_...
 | 
			
		||||
export DATABASE_URL=production_db_url
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### **2. Build for Production**
 | 
			
		||||
```bash
 | 
			
		||||
cargo build --release
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### **3. Database Migration**
 | 
			
		||||
```bash
 | 
			
		||||
# Ensure database is properly initialized
 | 
			
		||||
# Run any pending migrations
 | 
			
		||||
# Verify data integrity
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### **4. SSL Certificate**
 | 
			
		||||
```bash
 | 
			
		||||
# Ensure SSL certificates are properly configured
 | 
			
		||||
# Test HTTPS endpoints
 | 
			
		||||
# Verify webhook endpoints are accessible
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### **5. Final Testing**
 | 
			
		||||
- [ ] Test complete registration flow
 | 
			
		||||
- [ ] Test payment processing with real cards
 | 
			
		||||
- [ ] Test webhook delivery
 | 
			
		||||
- [ ] Test error scenarios
 | 
			
		||||
- [ ] Test mobile responsiveness
 | 
			
		||||
- [ ] Load testing for concurrent users
 | 
			
		||||
 | 
			
		||||
## 📋 **Deployment Commands**
 | 
			
		||||
 | 
			
		||||
### **Docker Deployment**
 | 
			
		||||
```bash
 | 
			
		||||
# Build production image
 | 
			
		||||
docker build -f Dockerfile.prod -t company-registration:latest .
 | 
			
		||||
 | 
			
		||||
# Run with production config
 | 
			
		||||
docker-compose -f docker-compose.prod.yml up -d
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### **Direct Deployment**
 | 
			
		||||
```bash
 | 
			
		||||
# Start production server
 | 
			
		||||
RUST_ENV=production ./target/release/actix_mvc_app
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 🔍 **Post-Deployment Verification**
 | 
			
		||||
 | 
			
		||||
### **Health Checks**
 | 
			
		||||
- [ ] Application starts successfully
 | 
			
		||||
- [ ] Database connections working
 | 
			
		||||
- [ ] Stripe connectivity verified
 | 
			
		||||
- [ ] All endpoints responding
 | 
			
		||||
- [ ] SSL certificates valid
 | 
			
		||||
- [ ] Webhook endpoints accessible
 | 
			
		||||
 | 
			
		||||
### **Functional Testing**
 | 
			
		||||
- [ ] Complete a test registration
 | 
			
		||||
- [ ] Process a test payment
 | 
			
		||||
- [ ] Verify company creation
 | 
			
		||||
- [ ] Check email notifications (if implemented)
 | 
			
		||||
- [ ] Test error scenarios
 | 
			
		||||
 | 
			
		||||
### **Monitoring**
 | 
			
		||||
- [ ] Application logs are being captured
 | 
			
		||||
- [ ] Error tracking is working
 | 
			
		||||
- [ ] Performance metrics available
 | 
			
		||||
- [ ] Database monitoring active
 | 
			
		||||
 | 
			
		||||
## 📁 **Important Files for Production**
 | 
			
		||||
 | 
			
		||||
### **Keep These Files**
 | 
			
		||||
- `ai_prompt/` - Development assistance
 | 
			
		||||
- `payment_plan.md` - Development roadmap
 | 
			
		||||
- `PRODUCTION_DEPLOYMENT.md` - Deployment guide
 | 
			
		||||
- `STRIPE_SETUP.md` - Payment configuration
 | 
			
		||||
- `config/` - Configuration files
 | 
			
		||||
- `src/` - Source code
 | 
			
		||||
- `static/` - Static assets
 | 
			
		||||
- `tests/` - Test files
 | 
			
		||||
 | 
			
		||||
### **Generated/Temporary Files (Ignored)**
 | 
			
		||||
- `target/` - Build artifacts
 | 
			
		||||
- `data/*.json` - Test data
 | 
			
		||||
- `logs/` - Log files
 | 
			
		||||
- `tmp/` - Temporary files
 | 
			
		||||
- `.env` - Environment files
 | 
			
		||||
 | 
			
		||||
## 🎯 **Ready for Production**
 | 
			
		||||
 | 
			
		||||
The application is now clean and ready for production deployment with:
 | 
			
		||||
 | 
			
		||||
✅ **Core Features Working**
 | 
			
		||||
- Multi-step company registration
 | 
			
		||||
- Stripe payment processing
 | 
			
		||||
- Database integration
 | 
			
		||||
- Error handling and recovery
 | 
			
		||||
- Security measures implemented
 | 
			
		||||
 | 
			
		||||
✅ **Code Quality**
 | 
			
		||||
- Debug logs cleaned up
 | 
			
		||||
- Proper error handling
 | 
			
		||||
- Input validation
 | 
			
		||||
- Security best practices
 | 
			
		||||
 | 
			
		||||
✅ **Documentation**
 | 
			
		||||
- Setup guides available
 | 
			
		||||
- Configuration documented
 | 
			
		||||
- Deployment instructions ready
 | 
			
		||||
- Development roadmap planned
 | 
			
		||||
 | 
			
		||||
## 🚀 **Next Steps After Deployment**
 | 
			
		||||
 | 
			
		||||
1. **Monitor initial usage** and performance
 | 
			
		||||
2. **Implement email notifications** (Option A from payment_plan.md)
 | 
			
		||||
3. **Build company dashboard** (Option B from payment_plan.md)
 | 
			
		||||
4. **Add document generation** (Option C from payment_plan.md)
 | 
			
		||||
5. **Enhance user authentication** (Option D from payment_plan.md)
 | 
			
		||||
 | 
			
		||||
The foundation is solid - ready to build the next features! 🎉
 | 
			
		||||
@@ -1,410 +0,0 @@
 | 
			
		||||
# Production Deployment Guide
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
This guide covers deploying the Freezone Company Registration System to production with proper security, monitoring, and reliability.
 | 
			
		||||
 | 
			
		||||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
- Docker and Docker Compose installed
 | 
			
		||||
- SSL certificates for HTTPS
 | 
			
		||||
- Stripe production account with API keys
 | 
			
		||||
- Domain name configured
 | 
			
		||||
- Server with at least 4GB RAM and 2 CPU cores
 | 
			
		||||
 | 
			
		||||
## Environment Variables
 | 
			
		||||
 | 
			
		||||
Create a `.env.prod` file with the following variables:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Application
 | 
			
		||||
RUST_ENV=production
 | 
			
		||||
RUST_LOG=info
 | 
			
		||||
 | 
			
		||||
# Database
 | 
			
		||||
POSTGRES_DB=freezone_prod
 | 
			
		||||
POSTGRES_USER=freezone_user
 | 
			
		||||
POSTGRES_PASSWORD=your_secure_db_password
 | 
			
		||||
DATABASE_URL=postgresql://freezone_user:your_secure_db_password@db:5432/freezone_prod
 | 
			
		||||
 | 
			
		||||
# Redis
 | 
			
		||||
REDIS_URL=redis://:your_redis_password@redis:6379
 | 
			
		||||
REDIS_PASSWORD=your_secure_redis_password
 | 
			
		||||
 | 
			
		||||
# Stripe (Production Keys)
 | 
			
		||||
STRIPE_SECRET_KEY=sk_live_your_production_secret_key
 | 
			
		||||
STRIPE_WEBHOOK_SECRET=whsec_your_production_webhook_secret
 | 
			
		||||
 | 
			
		||||
# Session Security
 | 
			
		||||
SESSION_SECRET=your_64_character_session_secret_key_for_production_use_only
 | 
			
		||||
 | 
			
		||||
# Monitoring
 | 
			
		||||
GRAFANA_PASSWORD=your_secure_grafana_password
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Security Checklist
 | 
			
		||||
 | 
			
		||||
### Before Deployment
 | 
			
		||||
 | 
			
		||||
- [ ] **SSL/TLS Certificates**: Obtain valid SSL certificates for your domain
 | 
			
		||||
- [ ] **Environment Variables**: All production secrets are set and secure
 | 
			
		||||
- [ ] **Database Security**: Database passwords are strong and unique
 | 
			
		||||
- [ ] **Stripe Configuration**: Production Stripe keys are configured
 | 
			
		||||
- [ ] **Session Security**: Session secret is 64+ characters and random
 | 
			
		||||
- [ ] **Firewall Rules**: Only necessary ports are open (80, 443, 22)
 | 
			
		||||
- [ ] **User Permissions**: Application runs as non-root user
 | 
			
		||||
 | 
			
		||||
### Stripe Configuration
 | 
			
		||||
 | 
			
		||||
1. **Production Account**: Ensure you're using Stripe production keys
 | 
			
		||||
2. **Webhook Endpoints**: Configure webhook endpoint in Stripe dashboard:
 | 
			
		||||
   - URL: `https://yourdomain.com/payment/webhook`
 | 
			
		||||
   - Events: `payment_intent.succeeded`, `payment_intent.payment_failed`
 | 
			
		||||
3. **Webhook Secret**: Copy the webhook signing secret to environment variables
 | 
			
		||||
 | 
			
		||||
### Database Security
 | 
			
		||||
 | 
			
		||||
1. **Connection Security**: Use SSL connections to database
 | 
			
		||||
2. **User Permissions**: Create dedicated database user with minimal permissions
 | 
			
		||||
3. **Backup Strategy**: Implement automated database backups
 | 
			
		||||
4. **Access Control**: Restrict database access to application only
 | 
			
		||||
 | 
			
		||||
## Deployment Steps
 | 
			
		||||
 | 
			
		||||
### 1. Server Preparation
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Update system
 | 
			
		||||
sudo apt update && sudo apt upgrade -y
 | 
			
		||||
 | 
			
		||||
# Install Docker
 | 
			
		||||
curl -fsSL https://get.docker.com -o get-docker.sh
 | 
			
		||||
sudo sh get-docker.sh
 | 
			
		||||
 | 
			
		||||
# Install Docker Compose
 | 
			
		||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
 | 
			
		||||
sudo chmod +x /usr/local/bin/docker-compose
 | 
			
		||||
 | 
			
		||||
# Create application directory
 | 
			
		||||
sudo mkdir -p /opt/freezone
 | 
			
		||||
sudo chown $USER:$USER /opt/freezone
 | 
			
		||||
cd /opt/freezone
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. Application Deployment
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Clone repository
 | 
			
		||||
git clone https://github.com/your-org/freezone-registration.git .
 | 
			
		||||
 | 
			
		||||
# Copy environment file
 | 
			
		||||
cp .env.prod.example .env.prod
 | 
			
		||||
# Edit .env.prod with your production values
 | 
			
		||||
 | 
			
		||||
# Create necessary directories
 | 
			
		||||
mkdir -p data logs nginx/ssl static
 | 
			
		||||
 | 
			
		||||
# Copy SSL certificates to nginx/ssl/
 | 
			
		||||
# - cert.pem (certificate)
 | 
			
		||||
# - key.pem (private key)
 | 
			
		||||
 | 
			
		||||
# Build and start services
 | 
			
		||||
docker-compose -f docker-compose.prod.yml up -d --build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. SSL Configuration
 | 
			
		||||
 | 
			
		||||
Create `nginx/nginx.conf`:
 | 
			
		||||
 | 
			
		||||
```nginx
 | 
			
		||||
events {
 | 
			
		||||
    worker_connections 1024;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
http {
 | 
			
		||||
    upstream app {
 | 
			
		||||
        server app:8080;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    server {
 | 
			
		||||
        listen 80;
 | 
			
		||||
        server_name yourdomain.com;
 | 
			
		||||
        return 301 https://$server_name$request_uri;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    server {
 | 
			
		||||
        listen 443 ssl http2;
 | 
			
		||||
        server_name yourdomain.com;
 | 
			
		||||
 | 
			
		||||
        ssl_certificate /etc/nginx/ssl/cert.pem;
 | 
			
		||||
        ssl_certificate_key /etc/nginx/ssl/key.pem;
 | 
			
		||||
        ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
        ssl_ciphers HIGH:!aNULL:!MD5;
 | 
			
		||||
 | 
			
		||||
        location / {
 | 
			
		||||
            proxy_pass http://app;
 | 
			
		||||
            proxy_set_header Host $host;
 | 
			
		||||
            proxy_set_header X-Real-IP $remote_addr;
 | 
			
		||||
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
            proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        location /health {
 | 
			
		||||
            proxy_pass http://app/health;
 | 
			
		||||
            access_log off;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 4. Monitoring Setup
 | 
			
		||||
 | 
			
		||||
The deployment includes:
 | 
			
		||||
 | 
			
		||||
- **Prometheus**: Metrics collection (port 9090)
 | 
			
		||||
- **Grafana**: Dashboards and alerting (port 3000)
 | 
			
		||||
- **Loki**: Log aggregation (port 3100)
 | 
			
		||||
- **Promtail**: Log shipping
 | 
			
		||||
 | 
			
		||||
Access Grafana at `https://yourdomain.com:3000` with admin credentials.
 | 
			
		||||
 | 
			
		||||
## Health Checks
 | 
			
		||||
 | 
			
		||||
The application provides several health check endpoints:
 | 
			
		||||
 | 
			
		||||
- `/health` - Overall system health
 | 
			
		||||
- `/health/detailed` - Detailed component status
 | 
			
		||||
- `/health/ready` - Readiness for load balancers
 | 
			
		||||
- `/health/live` - Liveness check
 | 
			
		||||
 | 
			
		||||
## Monitoring and Alerting
 | 
			
		||||
 | 
			
		||||
### Key Metrics to Monitor
 | 
			
		||||
 | 
			
		||||
1. **Application Health**
 | 
			
		||||
   - Response time
 | 
			
		||||
   - Error rate
 | 
			
		||||
   - Request volume
 | 
			
		||||
   - Memory usage
 | 
			
		||||
 | 
			
		||||
2. **Payment Processing**
 | 
			
		||||
   - Payment success rate
 | 
			
		||||
   - Payment processing time
 | 
			
		||||
   - Failed payment count
 | 
			
		||||
   - Webhook processing time
 | 
			
		||||
 | 
			
		||||
3. **Database Performance**
 | 
			
		||||
   - Connection pool usage
 | 
			
		||||
   - Query response time
 | 
			
		||||
   - Database size
 | 
			
		||||
   - Active connections
 | 
			
		||||
 | 
			
		||||
4. **System Resources**
 | 
			
		||||
   - CPU usage
 | 
			
		||||
   - Memory usage
 | 
			
		||||
   - Disk space
 | 
			
		||||
   - Network I/O
 | 
			
		||||
 | 
			
		||||
### Alerting Rules
 | 
			
		||||
 | 
			
		||||
Configure alerts for:
 | 
			
		||||
 | 
			
		||||
- Application downtime (> 1 minute)
 | 
			
		||||
- High error rate (> 5%)
 | 
			
		||||
- Payment failures (> 2%)
 | 
			
		||||
- Database connection issues
 | 
			
		||||
- High memory usage (> 80%)
 | 
			
		||||
- Disk space low (< 10%)
 | 
			
		||||
 | 
			
		||||
## Backup Strategy
 | 
			
		||||
 | 
			
		||||
### Database Backups
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Daily backup script
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
BACKUP_DIR="/opt/freezone/backups"
 | 
			
		||||
DATE=$(date +%Y%m%d_%H%M%S)
 | 
			
		||||
 | 
			
		||||
docker exec freezone-db pg_dump -U freezone_user freezone_prod > $BACKUP_DIR/db_backup_$DATE.sql
 | 
			
		||||
 | 
			
		||||
# Keep only last 30 days
 | 
			
		||||
find $BACKUP_DIR -name "db_backup_*.sql" -mtime +30 -delete
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Application Data Backups
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Backup registration data and logs
 | 
			
		||||
tar -czf /opt/freezone/backups/app_data_$(date +%Y%m%d).tar.gz \
 | 
			
		||||
    /opt/freezone/data \
 | 
			
		||||
    /opt/freezone/logs
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Maintenance
 | 
			
		||||
 | 
			
		||||
### Regular Tasks
 | 
			
		||||
 | 
			
		||||
1. **Weekly**
 | 
			
		||||
   - Review application logs
 | 
			
		||||
   - Check system resource usage
 | 
			
		||||
   - Verify backup integrity
 | 
			
		||||
   - Update security patches
 | 
			
		||||
 | 
			
		||||
2. **Monthly**
 | 
			
		||||
   - Review payment processing metrics
 | 
			
		||||
   - Update dependencies
 | 
			
		||||
   - Performance optimization review
 | 
			
		||||
   - Security audit
 | 
			
		||||
 | 
			
		||||
### Log Rotation
 | 
			
		||||
 | 
			
		||||
Configure log rotation in `/etc/logrotate.d/freezone`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
/opt/freezone/logs/*.log {
 | 
			
		||||
    daily
 | 
			
		||||
    rotate 30
 | 
			
		||||
    compress
 | 
			
		||||
    delaycompress
 | 
			
		||||
    missingok
 | 
			
		||||
    notifempty
 | 
			
		||||
    create 644 appuser appuser
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Troubleshooting
 | 
			
		||||
 | 
			
		||||
### Common Issues
 | 
			
		||||
 | 
			
		||||
1. **Application Won't Start**
 | 
			
		||||
   - Check environment variables
 | 
			
		||||
   - Verify database connectivity
 | 
			
		||||
   - Check SSL certificate paths
 | 
			
		||||
 | 
			
		||||
2. **Payment Processing Fails**
 | 
			
		||||
   - Verify Stripe API keys
 | 
			
		||||
   - Check webhook configuration
 | 
			
		||||
   - Review payment logs
 | 
			
		||||
 | 
			
		||||
3. **Database Connection Issues**
 | 
			
		||||
   - Check database container status
 | 
			
		||||
   - Verify connection string
 | 
			
		||||
   - Check network connectivity
 | 
			
		||||
 | 
			
		||||
### Log Locations
 | 
			
		||||
 | 
			
		||||
- Application logs: `/opt/freezone/logs/`
 | 
			
		||||
- Docker logs: `docker-compose logs [service]`
 | 
			
		||||
- Nginx logs: `docker-compose logs nginx`
 | 
			
		||||
- Database logs: `docker-compose logs db`
 | 
			
		||||
 | 
			
		||||
### Emergency Procedures
 | 
			
		||||
 | 
			
		||||
1. **Application Rollback**
 | 
			
		||||
   ```bash
 | 
			
		||||
   # Stop current deployment
 | 
			
		||||
   docker-compose -f docker-compose.prod.yml down
 | 
			
		||||
   
 | 
			
		||||
   # Restore from backup
 | 
			
		||||
   git checkout previous-stable-tag
 | 
			
		||||
   docker-compose -f docker-compose.prod.yml up -d --build
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
2. **Database Recovery**
 | 
			
		||||
   ```bash
 | 
			
		||||
   # Restore from backup
 | 
			
		||||
   docker exec -i freezone-db psql -U freezone_user freezone_prod < backup.sql
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
## Security Maintenance
 | 
			
		||||
 | 
			
		||||
### Regular Security Tasks
 | 
			
		||||
 | 
			
		||||
1. **Update Dependencies**
 | 
			
		||||
   ```bash
 | 
			
		||||
   # Update Rust dependencies
 | 
			
		||||
   cargo update
 | 
			
		||||
   
 | 
			
		||||
   # Rebuild with security patches
 | 
			
		||||
   docker-compose -f docker-compose.prod.yml build --no-cache
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
2. **SSL Certificate Renewal**
 | 
			
		||||
   ```bash
 | 
			
		||||
   # Using Let's Encrypt (example)
 | 
			
		||||
   certbot renew --nginx
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
3. **Security Scanning**
 | 
			
		||||
   ```bash
 | 
			
		||||
   # Scan for vulnerabilities
 | 
			
		||||
   cargo audit
 | 
			
		||||
   
 | 
			
		||||
   # Docker image scanning
 | 
			
		||||
   docker scan freezone-registration-app
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
## Performance Optimization
 | 
			
		||||
 | 
			
		||||
### Application Tuning
 | 
			
		||||
 | 
			
		||||
1. **Database Connection Pool**
 | 
			
		||||
   - Monitor connection usage
 | 
			
		||||
   - Adjust pool size based on load
 | 
			
		||||
 | 
			
		||||
2. **Redis Configuration**
 | 
			
		||||
   - Configure memory limits
 | 
			
		||||
   - Enable persistence if needed
 | 
			
		||||
 | 
			
		||||
3. **Nginx Optimization**
 | 
			
		||||
   - Enable gzip compression
 | 
			
		||||
   - Configure caching headers
 | 
			
		||||
   - Optimize worker processes
 | 
			
		||||
 | 
			
		||||
### Scaling Considerations
 | 
			
		||||
 | 
			
		||||
1. **Horizontal Scaling**
 | 
			
		||||
   - Load balancer configuration
 | 
			
		||||
   - Session store externalization
 | 
			
		||||
   - Database read replicas
 | 
			
		||||
 | 
			
		||||
2. **Vertical Scaling**
 | 
			
		||||
   - Monitor resource usage
 | 
			
		||||
   - Increase container resources
 | 
			
		||||
   - Optimize database queries
 | 
			
		||||
 | 
			
		||||
## Support and Maintenance
 | 
			
		||||
 | 
			
		||||
For production support:
 | 
			
		||||
 | 
			
		||||
1. **Monitoring**: Use Grafana dashboards for real-time monitoring
 | 
			
		||||
2. **Alerting**: Configure alerts for critical issues
 | 
			
		||||
3. **Logging**: Centralized logging with Loki/Grafana
 | 
			
		||||
4. **Documentation**: Keep deployment documentation updated
 | 
			
		||||
 | 
			
		||||
## Compliance and Auditing
 | 
			
		||||
 | 
			
		||||
### PCI DSS Compliance
 | 
			
		||||
 | 
			
		||||
- Secure payment processing with Stripe
 | 
			
		||||
- No storage of sensitive payment data
 | 
			
		||||
- Regular security assessments
 | 
			
		||||
- Access logging and monitoring
 | 
			
		||||
 | 
			
		||||
### Data Protection
 | 
			
		||||
 | 
			
		||||
- Secure data transmission (HTTPS)
 | 
			
		||||
- Data encryption at rest
 | 
			
		||||
- Regular backups
 | 
			
		||||
- Access control and audit trails
 | 
			
		||||
 | 
			
		||||
### Audit Trail
 | 
			
		||||
 | 
			
		||||
The application logs all critical events:
 | 
			
		||||
- Payment processing
 | 
			
		||||
- User actions
 | 
			
		||||
- Administrative changes
 | 
			
		||||
- Security events
 | 
			
		||||
 | 
			
		||||
Review audit logs regularly and maintain for compliance requirements.
 | 
			
		||||
@@ -1,100 +0,0 @@
 | 
			
		||||
# Stripe Integration Setup Guide
 | 
			
		||||
 | 
			
		||||
This guide explains how to configure Stripe payment processing for the company registration system.
 | 
			
		||||
 | 
			
		||||
## 🔧 Configuration Options
 | 
			
		||||
 | 
			
		||||
The application supports multiple ways to configure Stripe API keys:
 | 
			
		||||
 | 
			
		||||
### 1. Configuration Files (Recommended for Development)
 | 
			
		||||
 | 
			
		||||
#### Default Configuration
 | 
			
		||||
The application includes default test keys in `config/default.toml`:
 | 
			
		||||
```toml
 | 
			
		||||
[stripe]
 | 
			
		||||
publishable_key = "pk_test_..."
 | 
			
		||||
secret_key = "sk_test_..."
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Local Configuration
 | 
			
		||||
Create `config/local.toml` to override defaults:
 | 
			
		||||
```toml
 | 
			
		||||
[stripe]
 | 
			
		||||
publishable_key = "pk_test_YOUR_KEY_HERE"
 | 
			
		||||
secret_key = "sk_test_YOUR_KEY_HERE"
 | 
			
		||||
webhook_secret = "whsec_YOUR_WEBHOOK_SECRET"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. Environment Variables (Recommended for Production)
 | 
			
		||||
 | 
			
		||||
Set environment variables with the `APP__` prefix:
 | 
			
		||||
```bash
 | 
			
		||||
export APP__STRIPE__PUBLISHABLE_KEY="pk_test_YOUR_KEY_HERE"
 | 
			
		||||
export APP__STRIPE__SECRET_KEY="sk_test_YOUR_KEY_HERE"
 | 
			
		||||
export APP__STRIPE__WEBHOOK_SECRET="whsec_YOUR_WEBHOOK_SECRET"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Or create a `.env` file:
 | 
			
		||||
```bash
 | 
			
		||||
APP__STRIPE__PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE
 | 
			
		||||
APP__STRIPE__SECRET_KEY=sk_test_YOUR_KEY_HERE
 | 
			
		||||
APP__STRIPE__WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 🔑 Getting Your Stripe Keys
 | 
			
		||||
 | 
			
		||||
### Test Keys (Development)
 | 
			
		||||
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/test/apikeys)
 | 
			
		||||
2. Copy your **Publishable key** (starts with `pk_test_`)
 | 
			
		||||
3. Copy your **Secret key** (starts with `sk_test_`)
 | 
			
		||||
 | 
			
		||||
### Live Keys (Production)
 | 
			
		||||
1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
 | 
			
		||||
2. Copy your **Publishable key** (starts with `pk_live_`)
 | 
			
		||||
3. Copy your **Secret key** (starts with `sk_live_`)
 | 
			
		||||
 | 
			
		||||
⚠️ **Never commit live keys to version control!**
 | 
			
		||||
 | 
			
		||||
## 🔒 Security Best Practices
 | 
			
		||||
 | 
			
		||||
1. **Never commit sensitive keys** - Use `.gitignore` to exclude:
 | 
			
		||||
   - `.env`
 | 
			
		||||
   - `config/local.toml`
 | 
			
		||||
   - `config/production.toml`
 | 
			
		||||
 | 
			
		||||
2. **Use test keys in development** - Test keys are safe and don't process real payments
 | 
			
		||||
 | 
			
		||||
3. **Use environment variables in production** - More secure than config files
 | 
			
		||||
 | 
			
		||||
4. **Rotate keys regularly** - Generate new keys periodically
 | 
			
		||||
 | 
			
		||||
## 🚀 Quick Start
 | 
			
		||||
 | 
			
		||||
1. **Copy the example files:**
 | 
			
		||||
   ```bash
 | 
			
		||||
   cp config/local.toml.example config/local.toml
 | 
			
		||||
   cp .env.example .env
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
2. **Add your Stripe test keys** to either file
 | 
			
		||||
 | 
			
		||||
3. **Start the application:**
 | 
			
		||||
   ```bash
 | 
			
		||||
   cargo run
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
4. **Test the payment flow** at `http://127.0.0.1:9999/company`
 | 
			
		||||
 | 
			
		||||
## 📋 Configuration Priority
 | 
			
		||||
 | 
			
		||||
The application loads configuration in this order (later overrides earlier):
 | 
			
		||||
1. Default values in code
 | 
			
		||||
2. `config/default.toml`
 | 
			
		||||
3. `config/local.toml`
 | 
			
		||||
4. Environment variables
 | 
			
		||||
 | 
			
		||||
## 🔍 Troubleshooting
 | 
			
		||||
 | 
			
		||||
- **Keys not working?** Check the Stripe Dashboard for correct keys
 | 
			
		||||
- **Webhook errors?** Ensure webhook secret matches your Stripe endpoint
 | 
			
		||||
- **Configuration not loading?** Check file paths and environment variable names
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
# Default configuration for the application
 | 
			
		||||
# This file contains safe defaults and test keys
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
host = "127.0.0.1"
 | 
			
		||||
port = 9999
 | 
			
		||||
# workers = 4  # Uncomment to set specific number of workers
 | 
			
		||||
 | 
			
		||||
[templates]
 | 
			
		||||
dir = "./src/views"
 | 
			
		||||
 | 
			
		||||
[stripe]
 | 
			
		||||
# Stripe Test Keys (Safe for development)
 | 
			
		||||
# These are test keys from Stripe's documentation - they don't process real payments
 | 
			
		||||
publishable_key = "pk_test_51RdWkUC6v6GB0mBYmMbmKyXQfeRX0obM0V5rQCFGT35A1EP8WQJ5xw2vuWurqeGjdwaxls0B8mqdYpGSHcOlYOtQ000BvLkKCq"
 | 
			
		||||
secret_key = "sk_test_51RdWkUC6v6GB0mBYbbs4RULaNRq9CzqV88pM1EMU9dJ9TAj8obLAFsvfGWPq4Ed8nL36kbE7vK2oHvAQ35UrlJm100FlecQxmN"
 | 
			
		||||
# webhook_secret = "whsec_test_..."  # Uncomment and set when setting up webhooks
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
# Local configuration template
 | 
			
		||||
# Copy this file to 'local.toml' and customize with your own keys
 | 
			
		||||
# This file should NOT be committed to version control
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
# host = "0.0.0.0"  # Uncomment to bind to all interfaces
 | 
			
		||||
# port = 8080       # Uncomment to use different port
 | 
			
		||||
 | 
			
		||||
[stripe]
 | 
			
		||||
# Replace with your own Stripe test keys from https://dashboard.stripe.com/test/apikeys
 | 
			
		||||
# publishable_key = "pk_test_YOUR_PUBLISHABLE_KEY_HERE"
 | 
			
		||||
# secret_key = "sk_test_YOUR_SECRET_KEY_HERE"
 | 
			
		||||
# webhook_secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
 | 
			
		||||
 | 
			
		||||
# For production, use live keys:
 | 
			
		||||
# publishable_key = "pk_live_YOUR_LIVE_PUBLISHABLE_KEY"
 | 
			
		||||
# secret_key = "sk_live_YOUR_LIVE_SECRET_KEY"
 | 
			
		||||
# webhook_secret = "whsec_YOUR_LIVE_WEBHOOK_SECRET"
 | 
			
		||||
@@ -1,170 +0,0 @@
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  app:
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: Dockerfile.prod
 | 
			
		||||
    container_name: freezone-registration-app
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    environment:
 | 
			
		||||
      - RUST_ENV=production
 | 
			
		||||
      - RUST_LOG=info
 | 
			
		||||
      - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
 | 
			
		||||
      - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
 | 
			
		||||
      - SESSION_SECRET=${SESSION_SECRET}
 | 
			
		||||
      - DATABASE_URL=${DATABASE_URL}
 | 
			
		||||
      - REDIS_URL=${REDIS_URL}
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8080:8080"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
      - ./logs:/app/logs
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - redis
 | 
			
		||||
      - db
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
 | 
			
		||||
      interval: 30s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 3
 | 
			
		||||
      start_period: 40s
 | 
			
		||||
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis:7-alpine
 | 
			
		||||
    container_name: freezone-redis
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
 | 
			
		||||
    environment:
 | 
			
		||||
      - REDIS_PASSWORD=${REDIS_PASSWORD}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - redis_data:/data
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
 | 
			
		||||
      interval: 30s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 3
 | 
			
		||||
 | 
			
		||||
  db:
 | 
			
		||||
    image: postgres:15-alpine
 | 
			
		||||
    container_name: freezone-db
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    environment:
 | 
			
		||||
      - POSTGRES_DB=${POSTGRES_DB}
 | 
			
		||||
      - POSTGRES_USER=${POSTGRES_USER}
 | 
			
		||||
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - postgres_data:/var/lib/postgresql/data
 | 
			
		||||
      - ./db/init:/docker-entrypoint-initdb.d
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
 | 
			
		||||
      interval: 30s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 3
 | 
			
		||||
 | 
			
		||||
  nginx:
 | 
			
		||||
    image: nginx:alpine
 | 
			
		||||
    container_name: freezone-nginx
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    ports:
 | 
			
		||||
      - "80:80"
 | 
			
		||||
      - "443:443"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
 | 
			
		||||
      - ./nginx/ssl:/etc/nginx/ssl:ro
 | 
			
		||||
      - ./static:/var/www/static:ro
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - app
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
 | 
			
		||||
      interval: 30s
 | 
			
		||||
      timeout: 10s
 | 
			
		||||
      retries: 3
 | 
			
		||||
 | 
			
		||||
  prometheus:
 | 
			
		||||
    image: prom/prometheus:latest
 | 
			
		||||
    container_name: freezone-prometheus
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command:
 | 
			
		||||
      - '--config.file=/etc/prometheus/prometheus.yml'
 | 
			
		||||
      - '--storage.tsdb.path=/prometheus'
 | 
			
		||||
      - '--web.console.libraries=/etc/prometheus/console_libraries'
 | 
			
		||||
      - '--web.console.templates=/etc/prometheus/consoles'
 | 
			
		||||
      - '--storage.tsdb.retention.time=200h'
 | 
			
		||||
      - '--web.enable-lifecycle'
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
 | 
			
		||||
      - prometheus_data:/prometheus
 | 
			
		||||
    ports:
 | 
			
		||||
      - "9090:9090"
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
 | 
			
		||||
  grafana:
 | 
			
		||||
    image: grafana/grafana:latest
 | 
			
		||||
    container_name: freezone-grafana
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    environment:
 | 
			
		||||
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
 | 
			
		||||
      - GF_USERS_ALLOW_SIGN_UP=false
 | 
			
		||||
    volumes:
 | 
			
		||||
      - grafana_data:/var/lib/grafana
 | 
			
		||||
      - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
 | 
			
		||||
      - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
 | 
			
		||||
    ports:
 | 
			
		||||
      - "3000:3000"
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - prometheus
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
 | 
			
		||||
  loki:
 | 
			
		||||
    image: grafana/loki:latest
 | 
			
		||||
    container_name: freezone-loki
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: -config.file=/etc/loki/local-config.yaml
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./monitoring/loki.yml:/etc/loki/local-config.yaml:ro
 | 
			
		||||
      - loki_data:/loki
 | 
			
		||||
    ports:
 | 
			
		||||
      - "3100:3100"
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
 | 
			
		||||
  promtail:
 | 
			
		||||
    image: grafana/promtail:latest
 | 
			
		||||
    container_name: freezone-promtail
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: -config.file=/etc/promtail/config.yml
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./monitoring/promtail.yml:/etc/promtail/config.yml:ro
 | 
			
		||||
      - ./logs:/var/log/app:ro
 | 
			
		||||
      - /var/log:/var/log/host:ro
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - loki
 | 
			
		||||
    networks:
 | 
			
		||||
      - freezone-network
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  postgres_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
  redis_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
  prometheus_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
  grafana_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
  loki_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  freezone-network:
 | 
			
		||||
    driver: bridge
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
use std::env;
 | 
			
		||||
use config::{Config, ConfigError, File};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
/// Application configuration
 | 
			
		||||
#[derive(Debug, Deserialize, Clone)]
 | 
			
		||||
@@ -9,13 +9,10 @@ pub struct AppConfig {
 | 
			
		||||
    pub server: ServerConfig,
 | 
			
		||||
    /// Template configuration
 | 
			
		||||
    pub templates: TemplateConfig,
 | 
			
		||||
    /// Stripe configuration
 | 
			
		||||
    pub stripe: StripeConfig,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Server configuration
 | 
			
		||||
#[derive(Debug, Deserialize, Clone)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct ServerConfig {
 | 
			
		||||
    /// Host address to bind to
 | 
			
		||||
    pub host: String,
 | 
			
		||||
@@ -32,17 +29,6 @@ pub struct TemplateConfig {
 | 
			
		||||
    pub dir: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Stripe configuration
 | 
			
		||||
#[derive(Debug, Deserialize, Clone)]
 | 
			
		||||
pub struct StripeConfig {
 | 
			
		||||
    /// Stripe publishable key
 | 
			
		||||
    pub publishable_key: String,
 | 
			
		||||
    /// Stripe secret key
 | 
			
		||||
    pub secret_key: String,
 | 
			
		||||
    /// Webhook endpoint secret
 | 
			
		||||
    pub webhook_secret: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AppConfig {
 | 
			
		||||
    /// Loads configuration from files and environment variables
 | 
			
		||||
    pub fn new() -> Result<Self, ConfigError> {
 | 
			
		||||
@@ -51,10 +37,7 @@ impl AppConfig {
 | 
			
		||||
            .set_default("server.host", "127.0.0.1")?
 | 
			
		||||
            .set_default("server.port", 9999)?
 | 
			
		||||
            .set_default("server.workers", None::<u32>)?
 | 
			
		||||
            .set_default("templates.dir", "./src/views")?
 | 
			
		||||
            .set_default("stripe.publishable_key", "")?
 | 
			
		||||
            .set_default("stripe.secret_key", "")?
 | 
			
		||||
            .set_default("stripe.webhook_secret", None::<String>)?;
 | 
			
		||||
            .set_default("templates.dir", "./src/views")?;
 | 
			
		||||
 | 
			
		||||
        // Load from config file if it exists
 | 
			
		||||
        if let Ok(config_path) = env::var("APP_CONFIG") {
 | 
			
		||||
@@ -67,8 +50,7 @@ impl AppConfig {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
 | 
			
		||||
        config_builder =
 | 
			
		||||
            config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
 | 
			
		||||
        config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
 | 
			
		||||
 | 
			
		||||
        // Build and deserialize the config
 | 
			
		||||
        let config = config_builder.build()?;
 | 
			
		||||
@@ -79,4 +61,4 @@ impl AppConfig {
 | 
			
		||||
/// Returns the application configuration
 | 
			
		||||
pub fn get_config() -> AppConfig {
 | 
			
		||||
    AppConfig::new().expect("Failed to load configuration")
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -25,7 +25,6 @@ lazy_static! {
 | 
			
		||||
/// Controller for handling authentication-related routes
 | 
			
		||||
pub struct AuthController;
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl AuthController {
 | 
			
		||||
    /// Generate a JWT token for a user
 | 
			
		||||
    fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,12 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use actix_web::{HttpResponse, Responder, Result, web};
 | 
			
		||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
 | 
			
		||||
use crate::db::calendar::{
 | 
			
		||||
    add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar,
 | 
			
		||||
};
 | 
			
		||||
use crate::models::CalendarViewMode;
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
use heromodels::models::calendar::Event;
 | 
			
		||||
use heromodels_core::Model;
 | 
			
		||||
use crate::models::{CalendarEvent, CalendarViewMode};
 | 
			
		||||
use crate::utils::{RedisCalendarService, render_template};
 | 
			
		||||
 | 
			
		||||
/// Controller for handling calendar-related routes
 | 
			
		||||
pub struct CalendarController;
 | 
			
		||||
@@ -19,11 +14,9 @@ pub struct CalendarController;
 | 
			
		||||
impl CalendarController {
 | 
			
		||||
    /// Helper function to get user from session
 | 
			
		||||
    fn get_user_from_session(session: &Session) -> Option<Value> {
 | 
			
		||||
        session
 | 
			
		||||
            .get::<String>("user")
 | 
			
		||||
            .ok()
 | 
			
		||||
            .flatten()
 | 
			
		||||
            .and_then(|user_json| serde_json::from_str(&user_json).ok())
 | 
			
		||||
        session.get::<String>("user").ok().flatten().and_then(|user_json| {
 | 
			
		||||
            serde_json::from_str(&user_json).ok()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Handles the calendar page route
 | 
			
		||||
@@ -34,176 +27,113 @@ impl CalendarController {
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "calendar");
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Parse the view mode from the query parameters
 | 
			
		||||
        let view_mode =
 | 
			
		||||
            CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
 | 
			
		||||
        let view_mode = CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
 | 
			
		||||
        ctx.insert("view_mode", &view_mode.to_str());
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Parse the date from the query parameters or use the current date
 | 
			
		||||
        let date = if let Some(date_str) = &query.date {
 | 
			
		||||
            match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
 | 
			
		||||
                Ok(naive_date) => Utc
 | 
			
		||||
                    .from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap())
 | 
			
		||||
                    .into(),
 | 
			
		||||
                Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
 | 
			
		||||
                Err(_) => Utc::now(),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Utc::now()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        ctx.insert("current_date", &date.format("%Y-%m-%d").to_string());
 | 
			
		||||
        ctx.insert("current_year", &date.year());
 | 
			
		||||
        ctx.insert("current_month", &date.month());
 | 
			
		||||
        ctx.insert("current_day", &date.day());
 | 
			
		||||
 | 
			
		||||
        // Add user to context if available and ensure user has a calendar
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
 | 
			
		||||
            // Get or create user calendar
 | 
			
		||||
            if let (Some(user_id), Some(user_name)) = (
 | 
			
		||||
                user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
 | 
			
		||||
                user.get("full_name").and_then(|v| v.as_str()),
 | 
			
		||||
            ) {
 | 
			
		||||
                match get_or_create_user_calendar(user_id, user_name) {
 | 
			
		||||
                    Ok(calendar) => {
 | 
			
		||||
                        log::info!(
 | 
			
		||||
                            "User calendar ready: ID {}, Name: '{}'",
 | 
			
		||||
                            calendar.get_id(),
 | 
			
		||||
                            calendar.name
 | 
			
		||||
                        );
 | 
			
		||||
                        ctx.insert("user_calendar", &calendar);
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        log::error!("Failed to get or create user calendar: {}", e);
 | 
			
		||||
                        // Continue without calendar - the app should still work
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get events for the current view
 | 
			
		||||
        let (start_date, end_date) = match view_mode {
 | 
			
		||||
            CalendarViewMode::Year => {
 | 
			
		||||
                let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
 | 
			
		||||
                let end = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap();
 | 
			
		||||
                (start, end)
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Month => {
 | 
			
		||||
                let start = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
 | 
			
		||||
                let last_day = Self::last_day_of_month(date.year(), date.month());
 | 
			
		||||
                let end = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap();
 | 
			
		||||
                (start, end)
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Week => {
 | 
			
		||||
                // Calculate the start of the week (Sunday)
 | 
			
		||||
                let _weekday = date.weekday().num_days_from_sunday();
 | 
			
		||||
                let start_date = date
 | 
			
		||||
                    .date_naive()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .pred_opt()
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
 | 
			
		||||
                let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
 | 
			
		||||
                let end = start + chrono::Duration::days(7);
 | 
			
		||||
                (start, end)
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Day => {
 | 
			
		||||
                let start = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let end = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap();
 | 
			
		||||
                let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap();
 | 
			
		||||
                (start, end)
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get events from database
 | 
			
		||||
        let events = match get_events() {
 | 
			
		||||
            Ok(db_events) => {
 | 
			
		||||
                // Filter events for the date range
 | 
			
		||||
                db_events
 | 
			
		||||
                    .into_iter()
 | 
			
		||||
                    .filter(|event| {
 | 
			
		||||
                        // Event overlaps with the date range
 | 
			
		||||
                        event.start_time < end_date && event.end_time > start_date
 | 
			
		||||
                    })
 | 
			
		||||
                    .collect()
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        // Get events from Redis
 | 
			
		||||
        let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
 | 
			
		||||
            Ok(events) => events,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to get events from database: {}", e);
 | 
			
		||||
                log::error!("Failed to get events from Redis: {}", e);
 | 
			
		||||
                vec![]
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        ctx.insert("events", &events);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Generate calendar data based on the view mode
 | 
			
		||||
        match view_mode {
 | 
			
		||||
            CalendarViewMode::Year => {
 | 
			
		||||
                let months = (1..=12)
 | 
			
		||||
                    .map(|month| {
 | 
			
		||||
                        let month_name = match month {
 | 
			
		||||
                            1 => "January",
 | 
			
		||||
                            2 => "February",
 | 
			
		||||
                            3 => "March",
 | 
			
		||||
                            4 => "April",
 | 
			
		||||
                            5 => "May",
 | 
			
		||||
                            6 => "June",
 | 
			
		||||
                            7 => "July",
 | 
			
		||||
                            8 => "August",
 | 
			
		||||
                            9 => "September",
 | 
			
		||||
                            10 => "October",
 | 
			
		||||
                            11 => "November",
 | 
			
		||||
                            12 => "December",
 | 
			
		||||
                            _ => "",
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        let month_events = events
 | 
			
		||||
                            .iter()
 | 
			
		||||
                            .filter(|event| {
 | 
			
		||||
                                event.start_time.month() == month || event.end_time.month() == month
 | 
			
		||||
                            })
 | 
			
		||||
                            .cloned()
 | 
			
		||||
                            .collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
                        CalendarMonth {
 | 
			
		||||
                            month,
 | 
			
		||||
                            name: month_name.to_string(),
 | 
			
		||||
                            events: month_events,
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    .collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
                let months = (1..=12).map(|month| {
 | 
			
		||||
                    let month_name = match month {
 | 
			
		||||
                        1 => "January",
 | 
			
		||||
                        2 => "February",
 | 
			
		||||
                        3 => "March",
 | 
			
		||||
                        4 => "April",
 | 
			
		||||
                        5 => "May",
 | 
			
		||||
                        6 => "June",
 | 
			
		||||
                        7 => "July",
 | 
			
		||||
                        8 => "August",
 | 
			
		||||
                        9 => "September",
 | 
			
		||||
                        10 => "October",
 | 
			
		||||
                        11 => "November",
 | 
			
		||||
                        12 => "December",
 | 
			
		||||
                        _ => "",
 | 
			
		||||
                    };
 | 
			
		||||
                    
 | 
			
		||||
                    let month_events = events.iter()
 | 
			
		||||
                        .filter(|event| {
 | 
			
		||||
                            event.start_time.month() == month || event.end_time.month() == month
 | 
			
		||||
                        })
 | 
			
		||||
                        .cloned()
 | 
			
		||||
                        .collect::<Vec<_>>();
 | 
			
		||||
                    
 | 
			
		||||
                    CalendarMonth {
 | 
			
		||||
                        month,
 | 
			
		||||
                        name: month_name.to_string(),
 | 
			
		||||
                        events: month_events,
 | 
			
		||||
                    }
 | 
			
		||||
                }).collect::<Vec<_>>();
 | 
			
		||||
                
 | 
			
		||||
                ctx.insert("months", &months);
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Month => {
 | 
			
		||||
                let days_in_month = Self::last_day_of_month(date.year(), date.month());
 | 
			
		||||
                let first_day = Utc
 | 
			
		||||
                    .with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                let first_day = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
 | 
			
		||||
                let first_weekday = first_day.weekday().num_days_from_sunday();
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                let mut calendar_days = Vec::new();
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                // Add empty days for the start of the month
 | 
			
		||||
                for _ in 0..first_weekday {
 | 
			
		||||
                    calendar_days.push(CalendarDay {
 | 
			
		||||
@@ -212,34 +142,27 @@ impl CalendarController {
 | 
			
		||||
                        is_current_month: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                // Add days for the current month
 | 
			
		||||
                for day in 1..=days_in_month {
 | 
			
		||||
                    let day_events = events
 | 
			
		||||
                        .iter()
 | 
			
		||||
                    let day_events = events.iter()
 | 
			
		||||
                        .filter(|event| {
 | 
			
		||||
                            let day_start = Utc
 | 
			
		||||
                                .with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0)
 | 
			
		||||
                                .unwrap();
 | 
			
		||||
                            let day_end = Utc
 | 
			
		||||
                                .with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59)
 | 
			
		||||
                                .unwrap();
 | 
			
		||||
 | 
			
		||||
                            (event.start_time <= day_end && event.end_time >= day_start)
 | 
			
		||||
                                || (event.all_day
 | 
			
		||||
                                    && event.start_time.day() <= day
 | 
			
		||||
                                    && event.end_time.day() >= day)
 | 
			
		||||
                            let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap();
 | 
			
		||||
                            let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap();
 | 
			
		||||
                            
 | 
			
		||||
                            (event.start_time <= day_end && event.end_time >= day_start) ||
 | 
			
		||||
                            (event.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
 | 
			
		||||
                        })
 | 
			
		||||
                        .cloned()
 | 
			
		||||
                        .collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
                    
 | 
			
		||||
                    calendar_days.push(CalendarDay {
 | 
			
		||||
                        day,
 | 
			
		||||
                        events: day_events,
 | 
			
		||||
                        is_current_month: true,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                // Fill out the rest of the calendar grid (6 rows of 7 days)
 | 
			
		||||
                let remaining_days = 42 - calendar_days.len();
 | 
			
		||||
                for day in 1..=remaining_days {
 | 
			
		||||
@@ -249,250 +172,149 @@ impl CalendarController {
 | 
			
		||||
                        is_current_month: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                ctx.insert("calendar_days", &calendar_days);
 | 
			
		||||
                ctx.insert("month_name", &Self::month_name(date.month()));
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Week => {
 | 
			
		||||
                // Calculate the start of the week (Sunday)
 | 
			
		||||
                let weekday = date.weekday().num_days_from_sunday();
 | 
			
		||||
                let week_start = date - chrono::Duration::days(weekday as i64);
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                let mut week_days = Vec::new();
 | 
			
		||||
                for i in 0..7 {
 | 
			
		||||
                    let day_date = week_start + chrono::Duration::days(i);
 | 
			
		||||
                    let day_events = events
 | 
			
		||||
                        .iter()
 | 
			
		||||
                    let day_events = events.iter()
 | 
			
		||||
                        .filter(|event| {
 | 
			
		||||
                            let day_start = Utc
 | 
			
		||||
                                .with_ymd_and_hms(
 | 
			
		||||
                                    day_date.year(),
 | 
			
		||||
                                    day_date.month(),
 | 
			
		||||
                                    day_date.day(),
 | 
			
		||||
                                    0,
 | 
			
		||||
                                    0,
 | 
			
		||||
                                    0,
 | 
			
		||||
                                )
 | 
			
		||||
                                .unwrap();
 | 
			
		||||
                            let day_end = Utc
 | 
			
		||||
                                .with_ymd_and_hms(
 | 
			
		||||
                                    day_date.year(),
 | 
			
		||||
                                    day_date.month(),
 | 
			
		||||
                                    day_date.day(),
 | 
			
		||||
                                    23,
 | 
			
		||||
                                    59,
 | 
			
		||||
                                    59,
 | 
			
		||||
                                )
 | 
			
		||||
                                .unwrap();
 | 
			
		||||
 | 
			
		||||
                            (event.start_time <= day_end && event.end_time >= day_start)
 | 
			
		||||
                                || (event.all_day
 | 
			
		||||
                                    && event.start_time.day() <= day_date.day()
 | 
			
		||||
                                    && event.end_time.day() >= day_date.day())
 | 
			
		||||
                            let day_start = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 0, 0, 0).unwrap();
 | 
			
		||||
                            let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap();
 | 
			
		||||
                            
 | 
			
		||||
                            (event.start_time <= day_end && event.end_time >= day_start) ||
 | 
			
		||||
                            (event.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day())
 | 
			
		||||
                        })
 | 
			
		||||
                        .cloned()
 | 
			
		||||
                        .collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
                    
 | 
			
		||||
                    week_days.push(CalendarDay {
 | 
			
		||||
                        day: day_date.day(),
 | 
			
		||||
                        events: day_events,
 | 
			
		||||
                        is_current_month: day_date.month() == date.month(),
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                ctx.insert("week_days", &week_days);
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Day => {
 | 
			
		||||
                log::info!("Day view selected");
 | 
			
		||||
                ctx.insert(
 | 
			
		||||
                    "day_name",
 | 
			
		||||
                    &Self::day_name(date.weekday().num_days_from_sunday()),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                ctx.insert("day_name", &Self::day_name(date.weekday().num_days_from_sunday()));
 | 
			
		||||
                
 | 
			
		||||
                // Add debug info
 | 
			
		||||
                log::info!("Events count: {}", events.len());
 | 
			
		||||
                log::info!("Current date: {}", date.format("%Y-%m-%d"));
 | 
			
		||||
                log::info!(
 | 
			
		||||
                    "Day name: {}",
 | 
			
		||||
                    Self::day_name(date.weekday().num_days_from_sunday())
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
                log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday()));
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "calendar/index.html", &ctx)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the new event page route
 | 
			
		||||
    pub async fn new_event(tmpl: web::Data<Tera>, _session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "calendar");
 | 
			
		||||
 | 
			
		||||
        // Add user to context if available and ensure user has a calendar
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
 | 
			
		||||
            // Get or create user calendar
 | 
			
		||||
            if let (Some(user_id), Some(user_name)) = (
 | 
			
		||||
                user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
 | 
			
		||||
                user.get("full_name").and_then(|v| v.as_str()),
 | 
			
		||||
            ) {
 | 
			
		||||
                match get_or_create_user_calendar(user_id, user_name) {
 | 
			
		||||
                    Ok(calendar) => {
 | 
			
		||||
                        ctx.insert("user_calendar", &calendar);
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        log::error!("Failed to get or create user calendar: {}", e);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "calendar/new_event.html", &ctx)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the create event route
 | 
			
		||||
    pub async fn create_event(
 | 
			
		||||
        form: web::Form<EventForm>,
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        _session: Session,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Log the form data for debugging
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Creating event with form data: title='{}', start_time='{}', end_time='{}', all_day={}",
 | 
			
		||||
            form.title,
 | 
			
		||||
            form.start_time,
 | 
			
		||||
            form.end_time,
 | 
			
		||||
            form.all_day
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Parse the start and end times
 | 
			
		||||
        let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
 | 
			
		||||
            Ok(dt) => dt.with_timezone(&Utc),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to parse start time '{}': {}", form.start_time, e);
 | 
			
		||||
                return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
 | 
			
		||||
                log::error!("Failed to parse start time: {}", e);
 | 
			
		||||
                return Ok(HttpResponse::BadRequest().body("Invalid start time"));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
 | 
			
		||||
            Ok(dt) => dt.with_timezone(&Utc),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to parse end time '{}': {}", form.end_time, e);
 | 
			
		||||
                return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
 | 
			
		||||
                log::error!("Failed to parse end time: {}", e);
 | 
			
		||||
                return Ok(HttpResponse::BadRequest().body("Invalid end time"));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get user information from session
 | 
			
		||||
        let user_info = Self::get_user_from_session(&_session);
 | 
			
		||||
        let (user_id, user_name) = if let Some(user) = &user_info {
 | 
			
		||||
            let id = user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32);
 | 
			
		||||
            let name = user
 | 
			
		||||
                .get("full_name")
 | 
			
		||||
                .and_then(|v| v.as_str())
 | 
			
		||||
                .unwrap_or("Unknown User");
 | 
			
		||||
            log::info!("User from session: id={:?}, name='{}'", id, name);
 | 
			
		||||
            (id, name)
 | 
			
		||||
        } else {
 | 
			
		||||
            log::warn!("No user found in session");
 | 
			
		||||
            (None, "Unknown User")
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Create the event in the database
 | 
			
		||||
        match create_new_event(
 | 
			
		||||
            &form.title,
 | 
			
		||||
            Some(&form.description),
 | 
			
		||||
        
 | 
			
		||||
        // Create the event
 | 
			
		||||
        let event = CalendarEvent::new(
 | 
			
		||||
            form.title.clone(),
 | 
			
		||||
            form.description.clone(),
 | 
			
		||||
            start_time,
 | 
			
		||||
            end_time,
 | 
			
		||||
            None, // location
 | 
			
		||||
            Some(&form.color),
 | 
			
		||||
            Some(form.color.clone()),
 | 
			
		||||
            form.all_day,
 | 
			
		||||
            user_id,
 | 
			
		||||
            None, // category
 | 
			
		||||
            None, // reminder_minutes
 | 
			
		||||
        ) {
 | 
			
		||||
            Ok((event_id, _saved_event)) => {
 | 
			
		||||
                log::info!("Created event with ID: {}", event_id);
 | 
			
		||||
 | 
			
		||||
                // If user is logged in, add the event to their calendar
 | 
			
		||||
                if let Some(user_id) = user_id {
 | 
			
		||||
                    match get_or_create_user_calendar(user_id, user_name) {
 | 
			
		||||
                        Ok(calendar) => match add_event_to_calendar(calendar.get_id(), event_id) {
 | 
			
		||||
                            Ok(_) => {
 | 
			
		||||
                                log::info!(
 | 
			
		||||
                                    "Added event {} to calendar {}",
 | 
			
		||||
                                    event_id,
 | 
			
		||||
                                    calendar.get_id()
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(e) => {
 | 
			
		||||
                                log::error!("Failed to add event to calendar: {}", e);
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            log::error!("Failed to get user calendar: {}", e);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            None, // User ID would come from session in a real app
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        // Save the event to Redis
 | 
			
		||||
        match RedisCalendarService::save_event(&event) {
 | 
			
		||||
            Ok(_) => {
 | 
			
		||||
                // Redirect to the calendar page
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header(("Location", "/calendar"))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to save event to database: {}", e);
 | 
			
		||||
 | 
			
		||||
                log::error!("Failed to save event to Redis: {}", e);
 | 
			
		||||
                
 | 
			
		||||
                // Show an error message
 | 
			
		||||
                let mut ctx = tera::Context::new();
 | 
			
		||||
                ctx.insert("active_page", "calendar");
 | 
			
		||||
                ctx.insert("error", "Failed to save event");
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                // Add user to context if available
 | 
			
		||||
                if let Some(user) = user_info {
 | 
			
		||||
                if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
                    ctx.insert("user", &user);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
 | 
			
		||||
 | 
			
		||||
                Ok(HttpResponse::InternalServerError()
 | 
			
		||||
                    .content_type("text/html")
 | 
			
		||||
                    .body(result.into_body()))
 | 
			
		||||
                
 | 
			
		||||
                Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the delete event route
 | 
			
		||||
    pub async fn delete_event(
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        _session: Session,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        let id = path.into_inner();
 | 
			
		||||
 | 
			
		||||
        // Parse the event ID
 | 
			
		||||
        let event_id = match id.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                log::error!("Invalid event ID: {}", id);
 | 
			
		||||
                return Ok(HttpResponse::BadRequest().body("Invalid event ID"));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Delete the event from database
 | 
			
		||||
        match delete_event(event_id) {
 | 
			
		||||
        
 | 
			
		||||
        // Delete the event from Redis
 | 
			
		||||
        match RedisCalendarService::delete_event(&id) {
 | 
			
		||||
            Ok(_) => {
 | 
			
		||||
                log::info!("Deleted event with ID: {}", event_id);
 | 
			
		||||
                // Redirect to the calendar page
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header(("Location", "/calendar"))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to delete event from database: {}", e);
 | 
			
		||||
                log::error!("Failed to delete event from Redis: {}", e);
 | 
			
		||||
                Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Returns the last day of the month
 | 
			
		||||
    fn last_day_of_month(year: i32, month: u32) -> u32 {
 | 
			
		||||
        match month {
 | 
			
		||||
@@ -504,11 +326,11 @@ impl CalendarController {
 | 
			
		||||
                } else {
 | 
			
		||||
                    28
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
            _ => 30, // Default to 30 days
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Returns the name of the month
 | 
			
		||||
    fn month_name(month: u32) -> &'static str {
 | 
			
		||||
        match month {
 | 
			
		||||
@@ -527,7 +349,7 @@ impl CalendarController {
 | 
			
		||||
            _ => "",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Returns the name of the day
 | 
			
		||||
    fn day_name(day: u32) -> &'static str {
 | 
			
		||||
        match day {
 | 
			
		||||
@@ -565,7 +387,7 @@ pub struct EventForm {
 | 
			
		||||
#[derive(Debug, Serialize)]
 | 
			
		||||
struct CalendarDay {
 | 
			
		||||
    day: u32,
 | 
			
		||||
    events: Vec<Event>,
 | 
			
		||||
    events: Vec<CalendarEvent>,
 | 
			
		||||
    is_current_month: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -574,5 +396,5 @@ struct CalendarDay {
 | 
			
		||||
struct CalendarMonth {
 | 
			
		||||
    month: u32,
 | 
			
		||||
    name: String,
 | 
			
		||||
    events: Vec<Event>,
 | 
			
		||||
}
 | 
			
		||||
    events: Vec<CalendarEvent>,
 | 
			
		||||
}
 | 
			
		||||
@@ -1,20 +1,12 @@
 | 
			
		||||
use crate::config::get_config;
 | 
			
		||||
use crate::controllers::error::render_company_not_found;
 | 
			
		||||
use crate::db::company::*;
 | 
			
		||||
use crate::db::document::*;
 | 
			
		||||
use crate::models::document::DocumentType;
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_web::HttpRequest;
 | 
			
		||||
use actix_web::{HttpResponse, Result, web};
 | 
			
		||||
 | 
			
		||||
use heromodels::models::biz::{BusinessType, CompanyStatus};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use chrono::Utc;
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
 | 
			
		||||
// Form structs for company operations
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct CompanyRegistrationForm {
 | 
			
		||||
    pub company_name: String,
 | 
			
		||||
    pub company_type: String,
 | 
			
		||||
@@ -22,650 +14,232 @@ pub struct CompanyRegistrationForm {
 | 
			
		||||
    pub company_purpose: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
pub struct CompanyEditForm {
 | 
			
		||||
    pub company_name: String,
 | 
			
		||||
    pub company_type: String,
 | 
			
		||||
    pub email: Option<String>,
 | 
			
		||||
    pub phone: Option<String>,
 | 
			
		||||
    pub website: Option<String>,
 | 
			
		||||
    pub address: Option<String>,
 | 
			
		||||
    pub industry: Option<String>,
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub fiscal_year_end: Option<String>,
 | 
			
		||||
    pub status: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct CompanyController;
 | 
			
		||||
 | 
			
		||||
impl CompanyController {
 | 
			
		||||
    // Display the company management dashboard
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
        let config = get_config();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Starting Company dashboard rendering");
 | 
			
		||||
        
 | 
			
		||||
        // Add active_page for navigation highlighting
 | 
			
		||||
        context.insert("active_page", &"company");
 | 
			
		||||
 | 
			
		||||
        // Add Stripe configuration for payment processing
 | 
			
		||||
        context.insert("stripe_publishable_key", &config.stripe.publishable_key);
 | 
			
		||||
 | 
			
		||||
        // Load companies from database
 | 
			
		||||
        let companies = match get_companies() {
 | 
			
		||||
            Ok(companies) => companies,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to get companies from database: {}", e);
 | 
			
		||||
                vec![]
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        context.insert("companies", &companies);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Parse query parameters
 | 
			
		||||
        let query_string = req.query_string();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Check for success message
 | 
			
		||||
        if let Some(pos) = query_string.find("success=") {
 | 
			
		||||
            let start = pos + 8; // length of "success="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let success = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
 | 
			
		||||
            context.insert("success", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Check for entity context
 | 
			
		||||
        if let Some(pos) = query_string.find("entity=") {
 | 
			
		||||
            let start = pos + 7; // length of "entity="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let entity = &query_string[start..end];
 | 
			
		||||
            context.insert("entity", &entity);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Also get entity name if present
 | 
			
		||||
            if let Some(pos) = query_string.find("entity_name=") {
 | 
			
		||||
                let start = pos + 12; // length of "entity_name="
 | 
			
		||||
                let end = query_string[start..]
 | 
			
		||||
                    .find('&')
 | 
			
		||||
                    .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
                let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
 | 
			
		||||
                let entity_name = &query_string[start..end];
 | 
			
		||||
                let decoded_name =
 | 
			
		||||
                    urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
 | 
			
		||||
                let decoded_name = urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
 | 
			
		||||
                context.insert("entity_name", &decoded_name);
 | 
			
		||||
                println!("DEBUG: Entity context set to {} ({})", entity, decoded_name);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        render_template(&tmpl, "company/index.html", &context)
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Rendering Company dashboard template");
 | 
			
		||||
        let response = render_template(&tmpl, "company/index.html", &context);
 | 
			
		||||
        println!("DEBUG: Finished rendering Company dashboard template");
 | 
			
		||||
        response
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Display company edit form
 | 
			
		||||
    pub async fn edit_form(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        req: HttpRequest,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        // Add active_page for navigation highlighting
 | 
			
		||||
        context.insert("active_page", &"company");
 | 
			
		||||
 | 
			
		||||
        // Parse query parameters for success/error messages
 | 
			
		||||
        let query_string = req.query_string();
 | 
			
		||||
 | 
			
		||||
        // Check for success message
 | 
			
		||||
        if let Some(pos) = query_string.find("success=") {
 | 
			
		||||
            let start = pos + 8; // length of "success="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let success = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
 | 
			
		||||
            context.insert("success", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for error message
 | 
			
		||||
        if let Some(pos) = query_string.find("error=") {
 | 
			
		||||
            let start = pos + 6; // length of "error="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let error = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(error).unwrap_or_else(|_| error.into());
 | 
			
		||||
            context.insert("error", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse company ID
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => return render_company_not_found(&tmpl, Some(&company_id_str)).await,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Fetch company from database
 | 
			
		||||
        if let Ok(Some(company)) = get_company_by_id(company_id) {
 | 
			
		||||
            context.insert("company", &company);
 | 
			
		||||
 | 
			
		||||
            // Format timestamps for display
 | 
			
		||||
            let incorporation_date =
 | 
			
		||||
                chrono::DateTime::from_timestamp(company.incorporation_date, 0)
 | 
			
		||||
                    .map(|dt| dt.format("%Y-%m-%d").to_string())
 | 
			
		||||
                    .unwrap_or_else(|| "Unknown".to_string());
 | 
			
		||||
            context.insert("incorporation_date_formatted", &incorporation_date);
 | 
			
		||||
 | 
			
		||||
            render_template(&tmpl, "company/edit.html", &context)
 | 
			
		||||
        } else {
 | 
			
		||||
            render_company_not_found(&tmpl, Some(&company_id_str)).await
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // View company details
 | 
			
		||||
    pub async fn view_company(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        req: HttpRequest,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
    pub async fn view_company(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
 | 
			
		||||
        let company_id = path.into_inner();
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Viewing company details for {}", company_id);
 | 
			
		||||
        
 | 
			
		||||
        // Add active_page for navigation highlighting
 | 
			
		||||
        context.insert("active_page", &"company");
 | 
			
		||||
        context.insert("company_id", &company_id_str);
 | 
			
		||||
 | 
			
		||||
        // Parse query parameters for success/error messages
 | 
			
		||||
        let query_string = req.query_string();
 | 
			
		||||
 | 
			
		||||
        // Check for success message
 | 
			
		||||
        if let Some(pos) = query_string.find("success=") {
 | 
			
		||||
            let start = pos + 8; // length of "success="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let success = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
 | 
			
		||||
            context.insert("success", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for error message
 | 
			
		||||
        if let Some(pos) = query_string.find("error=") {
 | 
			
		||||
            let start = pos + 6; // length of "error="
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let error = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(error).unwrap_or_else(|_| error.into());
 | 
			
		||||
            context.insert("error", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse company ID
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => return render_company_not_found(&tmpl, Some(&company_id_str)).await,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Fetch company from database
 | 
			
		||||
        if let Ok(Some(company)) = get_company_by_id(company_id) {
 | 
			
		||||
            context.insert("company", &company);
 | 
			
		||||
 | 
			
		||||
            // Format timestamps for display
 | 
			
		||||
            let incorporation_date =
 | 
			
		||||
                chrono::DateTime::from_timestamp(company.incorporation_date, 0)
 | 
			
		||||
                    .map(|dt| dt.format("%Y-%m-%d").to_string())
 | 
			
		||||
                    .unwrap_or_else(|| "Unknown".to_string());
 | 
			
		||||
            context.insert("incorporation_date_formatted", &incorporation_date);
 | 
			
		||||
 | 
			
		||||
            // Get shareholders for this company
 | 
			
		||||
            let shareholders = match get_company_shareholders(company_id) {
 | 
			
		||||
                Ok(shareholders) => shareholders,
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::error!(
 | 
			
		||||
                        "Failed to get shareholders for company {}: {}",
 | 
			
		||||
                        company_id,
 | 
			
		||||
                        e
 | 
			
		||||
                    );
 | 
			
		||||
                    vec![]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            context.insert("shareholders", &shareholders);
 | 
			
		||||
 | 
			
		||||
            // Get payment information for this company
 | 
			
		||||
            if let Some(payment_info) =
 | 
			
		||||
                crate::controllers::payment::PaymentController::get_company_payment_info(company_id)
 | 
			
		||||
                    .await
 | 
			
		||||
            {
 | 
			
		||||
                context.insert("payment_info", &payment_info);
 | 
			
		||||
 | 
			
		||||
                // Format payment dates for display
 | 
			
		||||
                // Format timestamps from i64 to readable format
 | 
			
		||||
                let payment_created = chrono::DateTime::from_timestamp(payment_info.created_at, 0)
 | 
			
		||||
                    .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string())
 | 
			
		||||
                    .unwrap_or_else(|| "Unknown".to_string());
 | 
			
		||||
                context.insert("payment_created_formatted", &payment_created);
 | 
			
		||||
 | 
			
		||||
                if let Some(completed_at) = payment_info.completed_at {
 | 
			
		||||
                    let payment_completed = chrono::DateTime::from_timestamp(completed_at, 0)
 | 
			
		||||
                        .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string())
 | 
			
		||||
                        .unwrap_or_else(|| "Unknown".to_string());
 | 
			
		||||
                    context.insert("payment_completed_formatted", &payment_completed);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Format payment plan for display
 | 
			
		||||
                let payment_plan_display = match payment_info.payment_plan.as_str() {
 | 
			
		||||
                    "monthly" => "Monthly",
 | 
			
		||||
                    "yearly" => "Yearly (20% discount)",
 | 
			
		||||
                    "two_year" => "2-Year (40% discount)",
 | 
			
		||||
                    _ => &payment_info.payment_plan,
 | 
			
		||||
                };
 | 
			
		||||
                context.insert("payment_plan_display", &payment_plan_display);
 | 
			
		||||
 | 
			
		||||
                log::info!("Added payment info to company {} view", company_id);
 | 
			
		||||
            } else {
 | 
			
		||||
                log::info!("No payment info found for company {}", company_id);
 | 
			
		||||
        context.insert("company_id", &company_id);
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would fetch company data from a database
 | 
			
		||||
        // For now, we'll use mock data based on the company_id
 | 
			
		||||
        match company_id.as_str() {
 | 
			
		||||
            "company1" => {
 | 
			
		||||
                context.insert("company_name", &"Zanzibar Digital Solutions");
 | 
			
		||||
                context.insert("company_type", &"Startup FZC");
 | 
			
		||||
                context.insert("status", &"Active");
 | 
			
		||||
                context.insert("registration_date", &"2025-04-01");
 | 
			
		||||
                context.insert("purpose", &"Digital solutions and blockchain development");
 | 
			
		||||
                context.insert("plan", &"Startup FZC - $50/month");
 | 
			
		||||
                context.insert("next_billing", &"2025-06-01");
 | 
			
		||||
                context.insert("payment_method", &"Credit Card (****4582)");
 | 
			
		||||
                
 | 
			
		||||
                // Shareholders data
 | 
			
		||||
                let shareholders = vec![
 | 
			
		||||
                    ("John Smith", "60%"),
 | 
			
		||||
                    ("Sarah Johnson", "40%"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("shareholders", &shareholders);
 | 
			
		||||
                
 | 
			
		||||
                // Contracts data
 | 
			
		||||
                let contracts = vec![
 | 
			
		||||
                    ("Articles of Incorporation", "Signed"),
 | 
			
		||||
                    ("Terms & Conditions", "Signed"),
 | 
			
		||||
                    ("Digital Asset Issuance", "Signed"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("contracts", &contracts);
 | 
			
		||||
            },
 | 
			
		||||
            "company2" => {
 | 
			
		||||
                context.insert("company_name", &"Blockchain Innovations Ltd");
 | 
			
		||||
                context.insert("company_type", &"Growth FZC");
 | 
			
		||||
                context.insert("status", &"Active");
 | 
			
		||||
                context.insert("registration_date", &"2025-03-15");
 | 
			
		||||
                context.insert("purpose", &"Blockchain technology research and development");
 | 
			
		||||
                context.insert("plan", &"Growth FZC - $100/month");
 | 
			
		||||
                context.insert("next_billing", &"2025-06-15");
 | 
			
		||||
                context.insert("payment_method", &"Bank Transfer");
 | 
			
		||||
                
 | 
			
		||||
                // Shareholders data
 | 
			
		||||
                let shareholders = vec![
 | 
			
		||||
                    ("Michael Chen", "35%"),
 | 
			
		||||
                    ("Aisha Patel", "35%"),
 | 
			
		||||
                    ("David Okonkwo", "30%"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("shareholders", &shareholders);
 | 
			
		||||
                
 | 
			
		||||
                // Contracts data
 | 
			
		||||
                let contracts = vec![
 | 
			
		||||
                    ("Articles of Incorporation", "Signed"),
 | 
			
		||||
                    ("Terms & Conditions", "Signed"),
 | 
			
		||||
                    ("Digital Asset Issuance", "Signed"),
 | 
			
		||||
                    ("Physical Asset Holding", "Signed"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("contracts", &contracts);
 | 
			
		||||
            },
 | 
			
		||||
            "company3" => {
 | 
			
		||||
                context.insert("company_name", &"Sustainable Energy Cooperative");
 | 
			
		||||
                context.insert("company_type", &"Cooperative FZC");
 | 
			
		||||
                context.insert("status", &"Pending");
 | 
			
		||||
                context.insert("registration_date", &"2025-05-01");
 | 
			
		||||
                context.insert("purpose", &"Renewable energy production and distribution");
 | 
			
		||||
                context.insert("plan", &"Cooperative FZC - $200/month");
 | 
			
		||||
                context.insert("next_billing", &"Pending Activation");
 | 
			
		||||
                context.insert("payment_method", &"Pending");
 | 
			
		||||
                
 | 
			
		||||
                // Shareholders data
 | 
			
		||||
                let shareholders = vec![
 | 
			
		||||
                    ("Community Energy Group", "40%"),
 | 
			
		||||
                    ("Green Future Initiative", "30%"),
 | 
			
		||||
                    ("Sustainable Living Collective", "30%"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("shareholders", &shareholders);
 | 
			
		||||
                
 | 
			
		||||
                // Contracts data
 | 
			
		||||
                let contracts = vec![
 | 
			
		||||
                    ("Articles of Incorporation", "Signed"),
 | 
			
		||||
                    ("Terms & Conditions", "Signed"),
 | 
			
		||||
                    ("Cooperative Governance", "Pending"),
 | 
			
		||||
                ];
 | 
			
		||||
                context.insert("contracts", &contracts);
 | 
			
		||||
            },
 | 
			
		||||
            _ => {
 | 
			
		||||
                // If company_id is not recognized, redirect to company index
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/company"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            render_template(&tmpl, "company/view.html", &context)
 | 
			
		||||
        } else {
 | 
			
		||||
            render_company_not_found(&tmpl, Some(&company_id_str)).await
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Rendering company view template");
 | 
			
		||||
        let response = render_template(&tmpl, "company/view.html", &context);
 | 
			
		||||
        println!("DEBUG: Finished rendering company view template");
 | 
			
		||||
        response
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Switch to entity context
 | 
			
		||||
    pub async fn switch_entity(path: web::Path<String>) -> Result<HttpResponse> {
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
 | 
			
		||||
        // Parse company ID
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/company"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        let company_id = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Switching to entity context for {}", company_id);
 | 
			
		||||
        
 | 
			
		||||
        // Get company name based on ID (in a real app, this would come from a database)
 | 
			
		||||
        let company_name = match company_id.as_str() {
 | 
			
		||||
            "company1" => "Zanzibar Digital Solutions",
 | 
			
		||||
            "company2" => "Blockchain Innovations Ltd",
 | 
			
		||||
            "company3" => "Sustainable Energy Cooperative",
 | 
			
		||||
            _ => "Unknown Company"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get company from database
 | 
			
		||||
        let company_name = match get_company_by_id(company_id) {
 | 
			
		||||
            Ok(Some(company)) => company.name,
 | 
			
		||||
            Ok(None) => {
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/company"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to get company for switch: {}", e);
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/company"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would set a session/cookie for the current entity
 | 
			
		||||
        // Here we'll redirect back to the company page with a success message and entity parameter
 | 
			
		||||
        let success_message = format!("Switched to {} entity context", company_name);
 | 
			
		||||
        let encoded_message = urlencoding::encode(&success_message);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                "Location",
 | 
			
		||||
                format!(
 | 
			
		||||
                    "/company?success={}&entity={}&entity_name={}",
 | 
			
		||||
                    encoded_message,
 | 
			
		||||
                    company_id_str,
 | 
			
		||||
                    urlencoding::encode(&company_name)
 | 
			
		||||
                ),
 | 
			
		||||
            ))
 | 
			
		||||
            .append_header(("Location", format!("/company?success={}&entity={}&entity_name={}", 
 | 
			
		||||
                encoded_message, company_id, urlencoding::encode(company_name))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Deprecated registration method removed - now handled via payment flow
 | 
			
		||||
 | 
			
		||||
    // Legacy registration method (kept for reference but not used)
 | 
			
		||||
    #[allow(dead_code)]
 | 
			
		||||
    async fn legacy_register(mut form: actix_multipart::Multipart) -> Result<HttpResponse> {
 | 
			
		||||
        use actix_web::http::header;
 | 
			
		||||
        use chrono::Utc;
 | 
			
		||||
    
 | 
			
		||||
    // Process company registration
 | 
			
		||||
    pub async fn register(
 | 
			
		||||
        mut form: actix_multipart::Multipart,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        use actix_web::{http::header};
 | 
			
		||||
        use futures_util::stream::StreamExt as _;
 | 
			
		||||
        use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Processing company registration request");
 | 
			
		||||
        
 | 
			
		||||
        let mut fields: HashMap<String, String> = HashMap::new();
 | 
			
		||||
        let mut uploaded_files = Vec::new();
 | 
			
		||||
 | 
			
		||||
        let mut files = Vec::new();
 | 
			
		||||
        
 | 
			
		||||
        // Parse multipart form
 | 
			
		||||
        while let Some(Ok(mut field)) = form.next().await {
 | 
			
		||||
            let content_disposition = field.content_disposition();
 | 
			
		||||
            let field_name = content_disposition
 | 
			
		||||
                .get_name()
 | 
			
		||||
                .unwrap_or("unknown")
 | 
			
		||||
                .to_string();
 | 
			
		||||
            let filename = content_disposition.get_filename().map(|f| f.to_string());
 | 
			
		||||
 | 
			
		||||
            if field_name.starts_with("contract-") || field_name.ends_with("-doc") {
 | 
			
		||||
                // Handle file upload
 | 
			
		||||
                if let Some(filename) = filename {
 | 
			
		||||
                    let mut file_data = Vec::new();
 | 
			
		||||
                    while let Some(chunk) = field.next().await {
 | 
			
		||||
                        let data = chunk.unwrap();
 | 
			
		||||
                        file_data.extend_from_slice(&data);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if !file_data.is_empty() {
 | 
			
		||||
                        uploaded_files.push((field_name, filename, file_data));
 | 
			
		||||
                    }
 | 
			
		||||
            let mut value = Vec::new();
 | 
			
		||||
            while let Some(chunk) = field.next().await {
 | 
			
		||||
                let data = chunk.unwrap();
 | 
			
		||||
                value.extend_from_slice(&data);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Get field name from content disposition
 | 
			
		||||
            let cd = field.content_disposition();
 | 
			
		||||
            if let Some(name) = cd.get_name() {
 | 
			
		||||
                if name == "company_docs" {
 | 
			
		||||
                    files.push(value); // Just collect files in memory for now
 | 
			
		||||
                } else {
 | 
			
		||||
                    fields.insert(name.to_string(), String::from_utf8_lossy(&value).to_string());
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Handle form field
 | 
			
		||||
                let mut value = Vec::new();
 | 
			
		||||
                while let Some(chunk) = field.next().await {
 | 
			
		||||
                    let data = chunk.unwrap();
 | 
			
		||||
                    value.extend_from_slice(&data);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                fields.insert(field_name, String::from_utf8_lossy(&value).to_string());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Extract company details
 | 
			
		||||
        let company_name = fields.get("company_name").cloned().unwrap_or_default();
 | 
			
		||||
        let company_type_str = fields.get("company_type").cloned().unwrap_or_default();
 | 
			
		||||
        let company_purpose = fields.get("company_purpose").cloned().unwrap_or_default();
 | 
			
		||||
        let shareholders_str = fields.get("shareholders").cloned().unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        // Extract new contact fields
 | 
			
		||||
        let company_email = fields.get("company_email").cloned().unwrap_or_default();
 | 
			
		||||
        let company_phone = fields.get("company_phone").cloned().unwrap_or_default();
 | 
			
		||||
        let company_website = fields.get("company_website").cloned().unwrap_or_default();
 | 
			
		||||
        let company_address = fields.get("company_address").cloned().unwrap_or_default();
 | 
			
		||||
        let company_industry = fields.get("company_industry").cloned().unwrap_or_default();
 | 
			
		||||
        let fiscal_year_end = fields.get("fiscal_year_end").cloned().unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        // Validate required fields
 | 
			
		||||
        if company_name.is_empty() || company_type_str.is_empty() {
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    "/company?error=Company name and type are required",
 | 
			
		||||
                ))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if company_email.trim().is_empty() {
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((header::LOCATION, "/company?error=Company email is required"))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if company_phone.trim().is_empty() {
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((header::LOCATION, "/company?error=Company phone is required"))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if company_address.trim().is_empty() {
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    "/company?error=Company address is required",
 | 
			
		||||
                ))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse business type
 | 
			
		||||
        let business_type = match company_type_str.as_str() {
 | 
			
		||||
            "Startup FZC" => BusinessType::Starter,
 | 
			
		||||
            "Growth FZC" => BusinessType::Global,
 | 
			
		||||
            "Cooperative FZC" => BusinessType::Coop,
 | 
			
		||||
            "Single FZC" => BusinessType::Single,
 | 
			
		||||
            "Twin FZC" => BusinessType::Twin,
 | 
			
		||||
            _ => BusinessType::Single, // Default
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Generate registration number (in real app, this would be more sophisticated)
 | 
			
		||||
        let registration_number = format!(
 | 
			
		||||
            "FZC-{}-{}",
 | 
			
		||||
            Utc::now().format("%Y%m%d"),
 | 
			
		||||
            company_name
 | 
			
		||||
                .chars()
 | 
			
		||||
                .take(3)
 | 
			
		||||
                .collect::<String>()
 | 
			
		||||
                .to_uppercase()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Create company in database
 | 
			
		||||
        match create_new_company(
 | 
			
		||||
            company_name.clone(),
 | 
			
		||||
            registration_number,
 | 
			
		||||
            Utc::now().timestamp(),
 | 
			
		||||
            business_type,
 | 
			
		||||
            company_email,
 | 
			
		||||
            company_phone,
 | 
			
		||||
            company_website,
 | 
			
		||||
            company_address,
 | 
			
		||||
            company_industry,
 | 
			
		||||
            company_purpose,
 | 
			
		||||
            fiscal_year_end,
 | 
			
		||||
        ) {
 | 
			
		||||
            Ok((company_id, _company)) => {
 | 
			
		||||
                // TODO: Parse and create shareholders if provided
 | 
			
		||||
                if !shareholders_str.is_empty() {
 | 
			
		||||
                    // For now, just log the shareholders - in a real app, parse and create them
 | 
			
		||||
                    log::info!(
 | 
			
		||||
                        "Shareholders for company {}: {}",
 | 
			
		||||
                        company_id,
 | 
			
		||||
                        shareholders_str
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Save uploaded documents
 | 
			
		||||
                if !uploaded_files.is_empty() {
 | 
			
		||||
                    log::info!(
 | 
			
		||||
                        "Processing {} uploaded files for company {}",
 | 
			
		||||
                        uploaded_files.len(),
 | 
			
		||||
                        company_id
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    // Create uploads directory if it doesn't exist
 | 
			
		||||
                    let upload_dir = format!("/tmp/company_{}_documents", company_id);
 | 
			
		||||
                    if let Err(e) = fs::create_dir_all(&upload_dir) {
 | 
			
		||||
                        log::error!("Failed to create upload directory: {}", e);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Save each uploaded file
 | 
			
		||||
                        for (field_name, filename, file_data) in uploaded_files {
 | 
			
		||||
                            // Determine document type based on field name
 | 
			
		||||
                            let doc_type = match field_name.as_str() {
 | 
			
		||||
                                name if name.contains("shareholder") => DocumentType::Articles,
 | 
			
		||||
                                name if name.contains("bank") => DocumentType::Financial,
 | 
			
		||||
                                name if name.contains("cooperative") => DocumentType::Articles,
 | 
			
		||||
                                name if name.contains("digital") => DocumentType::Legal,
 | 
			
		||||
                                name if name.contains("contract") => DocumentType::Contract,
 | 
			
		||||
                                _ => DocumentType::Other,
 | 
			
		||||
                            };
 | 
			
		||||
 | 
			
		||||
                            // Generate unique filename
 | 
			
		||||
                            let timestamp = Utc::now().timestamp();
 | 
			
		||||
                            let file_extension = filename.split('.').last().unwrap_or("pdf");
 | 
			
		||||
                            let unique_filename = format!(
 | 
			
		||||
                                "{}_{}.{}",
 | 
			
		||||
                                timestamp,
 | 
			
		||||
                                filename.replace(" ", "_"),
 | 
			
		||||
                                file_extension
 | 
			
		||||
                            );
 | 
			
		||||
                            let file_path = format!("{}/{}", upload_dir, unique_filename);
 | 
			
		||||
 | 
			
		||||
                            // Save file to disk
 | 
			
		||||
                            if let Err(e) = fs::write(&file_path, &file_data) {
 | 
			
		||||
                                log::error!("Failed to save file {}: {}", filename, e);
 | 
			
		||||
                                continue;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // Save document metadata to database
 | 
			
		||||
                            let file_size = file_data.len() as u64;
 | 
			
		||||
                            let mime_type = match file_extension {
 | 
			
		||||
                                "pdf" => "application/pdf",
 | 
			
		||||
                                "doc" | "docx" => "application/msword",
 | 
			
		||||
                                "jpg" | "jpeg" => "image/jpeg",
 | 
			
		||||
                                "png" => "image/png",
 | 
			
		||||
                                _ => "application/octet-stream",
 | 
			
		||||
                            }
 | 
			
		||||
                            .to_string();
 | 
			
		||||
 | 
			
		||||
                            match create_new_document(
 | 
			
		||||
                                filename.clone(),
 | 
			
		||||
                                file_path,
 | 
			
		||||
                                file_size,
 | 
			
		||||
                                mime_type,
 | 
			
		||||
                                company_id,
 | 
			
		||||
                                "System".to_string(), // uploaded_by
 | 
			
		||||
                                doc_type,
 | 
			
		||||
                                Some("Uploaded during company registration".to_string()),
 | 
			
		||||
                                false, // not public by default
 | 
			
		||||
                                None,  // checksum
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Ok(_) => {
 | 
			
		||||
                                    log::info!("Successfully saved document: {}", filename);
 | 
			
		||||
                                }
 | 
			
		||||
                                Err(e) => {
 | 
			
		||||
                                    log::error!(
 | 
			
		||||
                                        "Failed to save document metadata for {}: {}",
 | 
			
		||||
                                        filename,
 | 
			
		||||
                                        e
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let success_message = format!(
 | 
			
		||||
                    "Successfully registered {} as a {}",
 | 
			
		||||
                    company_name, company_type_str
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!("/company?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to create company: {}", e);
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        "/company?error=Failed to register company",
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Process company edit form
 | 
			
		||||
    pub async fn edit(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        form: web::Form<CompanyEditForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        use actix_web::http::header;
 | 
			
		||||
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
 | 
			
		||||
        // Parse company ID
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => return render_company_not_found(&tmpl, Some(&company_id_str)).await,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Validate required fields
 | 
			
		||||
        if form.company_name.trim().is_empty() {
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    format!(
 | 
			
		||||
                        "/company/edit/{}?error=Company name is required",
 | 
			
		||||
                        company_id
 | 
			
		||||
                    ),
 | 
			
		||||
                ))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse business type
 | 
			
		||||
        let business_type = match form.company_type.as_str() {
 | 
			
		||||
            "Startup FZC" => BusinessType::Starter,
 | 
			
		||||
            "Growth FZC" => BusinessType::Global,
 | 
			
		||||
            "Cooperative FZC" => BusinessType::Coop,
 | 
			
		||||
            "Single FZC" => BusinessType::Single,
 | 
			
		||||
            "Twin FZC" => BusinessType::Twin,
 | 
			
		||||
            _ => BusinessType::Single, // Default
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Parse status
 | 
			
		||||
        let status = match form.status.as_str() {
 | 
			
		||||
            "Active" => CompanyStatus::Active,
 | 
			
		||||
            "Inactive" => CompanyStatus::Inactive,
 | 
			
		||||
            "Suspended" => CompanyStatus::Suspended,
 | 
			
		||||
            _ => CompanyStatus::Active, // Default
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Update company in database
 | 
			
		||||
        match update_company(
 | 
			
		||||
            company_id,
 | 
			
		||||
            Some(form.company_name.clone()),
 | 
			
		||||
            form.email.clone(),
 | 
			
		||||
            form.phone.clone(),
 | 
			
		||||
            form.website.clone(),
 | 
			
		||||
            form.address.clone(),
 | 
			
		||||
            form.industry.clone(),
 | 
			
		||||
            form.description.clone(),
 | 
			
		||||
            form.fiscal_year_end.clone(),
 | 
			
		||||
            Some(status),
 | 
			
		||||
            Some(business_type),
 | 
			
		||||
        ) {
 | 
			
		||||
            Ok(_) => {
 | 
			
		||||
                let success_message = format!("Successfully updated {}", form.company_name);
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/view/{}?success={}",
 | 
			
		||||
                            company_id,
 | 
			
		||||
                            urlencoding::encode(&success_message)
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to update company {}: {}", company_id, e);
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/edit/{}?error=Failed to update company",
 | 
			
		||||
                            company_id
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Debug endpoint to clean up corrupted database (emergency use only)
 | 
			
		||||
    pub async fn cleanup_database() -> Result<HttpResponse> {
 | 
			
		||||
        match crate::db::company::cleanup_corrupted_database() {
 | 
			
		||||
            Ok(message) => {
 | 
			
		||||
                log::info!("Database cleanup successful: {}", message);
 | 
			
		||||
                Ok(HttpResponse::Ok().json(serde_json::json!({
 | 
			
		||||
                    "success": true,
 | 
			
		||||
                    "message": message
 | 
			
		||||
                })))
 | 
			
		||||
            }
 | 
			
		||||
            Err(error) => {
 | 
			
		||||
                log::error!("Database cleanup failed: {}", error);
 | 
			
		||||
                Ok(HttpResponse::InternalServerError().json(serde_json::json!({
 | 
			
		||||
                    "success": false,
 | 
			
		||||
                    "error": error
 | 
			
		||||
                })))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        let company_type = fields.get("company_type").cloned().unwrap_or_default();
 | 
			
		||||
        let shareholders = fields.get("shareholders").cloned().unwrap_or_default();
 | 
			
		||||
        
 | 
			
		||||
        // Log received fields (mock DB insert)
 | 
			
		||||
        println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}", 
 | 
			
		||||
            company_name, company_type, shareholders, files.len());
 | 
			
		||||
        
 | 
			
		||||
        // Create success message
 | 
			
		||||
        let success_message = format!("Successfully registered {} as a {}", company_name, company_type);
 | 
			
		||||
        
 | 
			
		||||
        // Redirect back to /company with success message
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((header::LOCATION, format!("/company?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,15 +1,12 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Result};
 | 
			
		||||
use actix_web::HttpRequest;
 | 
			
		||||
use actix_web::{HttpResponse, Result, web};
 | 
			
		||||
use chrono::{Duration, Utc};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
use chrono::{Utc, Duration};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::models::asset::Asset;
 | 
			
		||||
use crate::models::defi::{
 | 
			
		||||
    DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition,
 | 
			
		||||
    ReceivingPosition,
 | 
			
		||||
};
 | 
			
		||||
use crate::models::asset::{Asset, AssetType, AssetStatus};
 | 
			
		||||
use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB};
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
 | 
			
		||||
// Form structs for DeFi operations
 | 
			
		||||
@@ -29,7 +26,6 @@ pub struct ReceivingForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct LiquidityForm {
 | 
			
		||||
    pub first_token: String,
 | 
			
		||||
    pub first_amount: f64,
 | 
			
		||||
@@ -39,7 +35,6 @@ pub struct LiquidityForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct StakingForm {
 | 
			
		||||
    pub asset_id: String,
 | 
			
		||||
    pub amount: f64,
 | 
			
		||||
@@ -54,7 +49,6 @@ pub struct SwapForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct CollateralForm {
 | 
			
		||||
    pub asset_id: String,
 | 
			
		||||
    pub amount: f64,
 | 
			
		||||
@@ -69,29 +63,29 @@ impl DefiController {
 | 
			
		||||
    // Display the DeFi dashboard
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Starting DeFi dashboard rendering");
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get mock assets for the dropdown selectors
 | 
			
		||||
        let assets = Self::get_mock_assets();
 | 
			
		||||
        println!("DEBUG: Generated {} mock assets", assets.len());
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add active_page for navigation highlighting
 | 
			
		||||
        context.insert("active_page", &"defi");
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add DeFi stats
 | 
			
		||||
        let defi_stats = Self::get_defi_stats();
 | 
			
		||||
        context.insert("defi_stats", &serde_json::to_value(defi_stats).unwrap());
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add recent assets for selection in forms
 | 
			
		||||
        let recent_assets: Vec<serde_json::Map<String, serde_json::Value>> = assets
 | 
			
		||||
            .iter()
 | 
			
		||||
            .take(5)
 | 
			
		||||
            .map(|a| Self::asset_to_json(a))
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        context.insert("recent_assets", &recent_assets);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get user's providing positions
 | 
			
		||||
        let db = DEFI_DB.lock().unwrap();
 | 
			
		||||
        let providing_positions = db.get_user_providing_positions("user123");
 | 
			
		||||
@@ -100,7 +94,7 @@ impl DefiController {
 | 
			
		||||
            .map(|p| serde_json::to_value(p).unwrap())
 | 
			
		||||
            .collect();
 | 
			
		||||
        context.insert("providing_positions", &providing_positions_json);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get user's receiving positions
 | 
			
		||||
        let receiving_positions = db.get_user_receiving_positions("user123");
 | 
			
		||||
        let receiving_positions_json: Vec<serde_json::Value> = receiving_positions
 | 
			
		||||
@@ -108,30 +102,27 @@ impl DefiController {
 | 
			
		||||
            .map(|p| serde_json::to_value(p).unwrap())
 | 
			
		||||
            .collect();
 | 
			
		||||
        context.insert("receiving_positions", &receiving_positions_json);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add success message if present in query params
 | 
			
		||||
        if let Some(success) = req.query_string().strip_prefix("success=") {
 | 
			
		||||
            let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
 | 
			
		||||
            context.insert("success_message", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        println!("DEBUG: Rendering DeFi dashboard template");
 | 
			
		||||
        let response = render_template(&tmpl, "defi/index.html", &context);
 | 
			
		||||
        println!("DEBUG: Finished rendering DeFi dashboard template");
 | 
			
		||||
        response
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process providing request
 | 
			
		||||
    pub async fn create_providing(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<ProvidingForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn create_providing(_tmpl: web::Data<Tera>, form: web::Form<ProvidingForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing providing request: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get the asset obligationails (in a real app, this would come from a database)
 | 
			
		||||
        let assets = Self::get_mock_assets();
 | 
			
		||||
        let asset = assets.iter().find(|a| a.id == form.asset_id);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if let Some(asset) = asset {
 | 
			
		||||
            // Calculate profit share and return amount
 | 
			
		||||
            let profit_share = match form.duration {
 | 
			
		||||
@@ -142,10 +133,9 @@ impl DefiController {
 | 
			
		||||
                365 => 12.0,
 | 
			
		||||
                _ => 4.2, // Default to 30 days rate
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let return_amount = form.amount
 | 
			
		||||
                + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let return_amount = form.amount + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
 | 
			
		||||
            
 | 
			
		||||
            // Create a new providing position
 | 
			
		||||
            let providing_position = ProvidingPosition {
 | 
			
		||||
                base: DefiPosition {
 | 
			
		||||
@@ -166,23 +156,17 @@ impl DefiController {
 | 
			
		||||
                profit_share_earned: profit_share,
 | 
			
		||||
                return_amount,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Add the position to the database
 | 
			
		||||
            {
 | 
			
		||||
                let mut db = DEFI_DB.lock().unwrap();
 | 
			
		||||
                db.add_providing_position(providing_position);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Redirect with success message
 | 
			
		||||
            let success_message = format!(
 | 
			
		||||
                "Successfully provided {} {} for {} days",
 | 
			
		||||
                form.amount, asset.name, form.duration
 | 
			
		||||
            );
 | 
			
		||||
            let success_message = format!("Successfully provided {} {} for {} days", form.amount, asset.name, form.duration);
 | 
			
		||||
            Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    "Location",
 | 
			
		||||
                    format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
                ))
 | 
			
		||||
                .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
                .finish())
 | 
			
		||||
        } else {
 | 
			
		||||
            // Asset not found, redirect with error
 | 
			
		||||
@@ -191,18 +175,15 @@ impl DefiController {
 | 
			
		||||
                .finish())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process receiving request
 | 
			
		||||
    pub async fn create_receiving(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<ReceivingForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn create_receiving(_tmpl: web::Data<Tera>, form: web::Form<ReceivingForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing receiving request: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get the asset obligationails (in a real app, this would come from a database)
 | 
			
		||||
        let assets = Self::get_mock_assets();
 | 
			
		||||
        let collateral_asset = assets.iter().find(|a| a.id == form.collateral_asset_id);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if let Some(collateral_asset) = collateral_asset {
 | 
			
		||||
            // Calculate profit share rate based on duration
 | 
			
		||||
            let profit_share_rate = match form.duration {
 | 
			
		||||
@@ -213,17 +194,15 @@ impl DefiController {
 | 
			
		||||
                365 => 10.0,
 | 
			
		||||
                _ => 5.0, // Default to 30 days rate
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Calculate profit share and total to repay
 | 
			
		||||
            let profit_share =
 | 
			
		||||
                form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
 | 
			
		||||
            let profit_share = form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
 | 
			
		||||
            let total_to_repay = form.amount + profit_share;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Calculate collateral value and ratio
 | 
			
		||||
            let collateral_value = form.collateral_amount
 | 
			
		||||
                * collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
 | 
			
		||||
            let collateral_value = form.collateral_amount * collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
 | 
			
		||||
            let collateral_ratio = (collateral_value / form.amount) * 100.0;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Create a new receiving position
 | 
			
		||||
            let receiving_position = ReceivingPosition {
 | 
			
		||||
                base: DefiPosition {
 | 
			
		||||
@@ -251,23 +230,18 @@ impl DefiController {
 | 
			
		||||
                total_to_repay,
 | 
			
		||||
                collateral_ratio,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Add the position to the database
 | 
			
		||||
            {
 | 
			
		||||
                let mut db = DEFI_DB.lock().unwrap();
 | 
			
		||||
                db.add_receiving_position(receiving_position);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Redirect with success message
 | 
			
		||||
            let success_message = format!(
 | 
			
		||||
                "Successfully borrowed {} ZDFZ using {} {} as collateral",
 | 
			
		||||
                form.amount, form.collateral_amount, collateral_asset.name
 | 
			
		||||
            );
 | 
			
		||||
            let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral", 
 | 
			
		||||
                form.amount, form.collateral_amount, collateral_asset.name);
 | 
			
		||||
            Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    "Location",
 | 
			
		||||
                    format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
                ))
 | 
			
		||||
                .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
                .finish())
 | 
			
		||||
        } else {
 | 
			
		||||
            // Asset not found, redirect with error
 | 
			
		||||
@@ -276,202 +250,116 @@ impl DefiController {
 | 
			
		||||
                .finish())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process liquidity provision
 | 
			
		||||
    pub async fn add_liquidity(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<LiquidityForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn add_liquidity(_tmpl: web::Data<Tera>, form: web::Form<LiquidityForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing liquidity provision: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, this would add liquidity to a pool in the database
 | 
			
		||||
        // For now, we'll just redirect back to the DeFi dashboard with a success message
 | 
			
		||||
 | 
			
		||||
        let success_message = format!(
 | 
			
		||||
            "Successfully added liquidity: {} {} and {} {}",
 | 
			
		||||
            form.first_amount, form.first_token, form.second_amount, form.second_token
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let success_message = format!("Successfully added liquidity: {} {} and {} {}", 
 | 
			
		||||
            form.first_amount, form.first_token, form.second_amount, form.second_token);
 | 
			
		||||
        
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                "Location",
 | 
			
		||||
                format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
            ))
 | 
			
		||||
            .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process staking request
 | 
			
		||||
    pub async fn create_staking(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<StakingForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn create_staking(_tmpl: web::Data<Tera>, form: web::Form<StakingForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing staking request: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, this would create a staking position in the database
 | 
			
		||||
        // For now, we'll just redirect back to the DeFi dashboard with a success message
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                "Location",
 | 
			
		||||
                format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
            ))
 | 
			
		||||
            .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process token swap
 | 
			
		||||
    pub async fn swap_tokens(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<SwapForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn swap_tokens(_tmpl: web::Data<Tera>, form: web::Form<SwapForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing token swap: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, this would perform a token swap in the database
 | 
			
		||||
        // For now, we'll just redirect back to the DeFi dashboard with a success message
 | 
			
		||||
 | 
			
		||||
        let success_message = format!(
 | 
			
		||||
            "Successfully swapped {} {} to {}",
 | 
			
		||||
            form.from_amount, form.from_token, form.to_token
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let success_message = format!("Successfully swapped {} {} to {}", 
 | 
			
		||||
            form.from_amount, form.from_token, form.to_token);
 | 
			
		||||
        
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                "Location",
 | 
			
		||||
                format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
            ))
 | 
			
		||||
            .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Process collateral position creation
 | 
			
		||||
    pub async fn create_collateral(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<CollateralForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn create_collateral(_tmpl: web::Data<Tera>, form: web::Form<CollateralForm>) -> Result<HttpResponse> {
 | 
			
		||||
        println!("DEBUG: Processing collateral creation: {:?}", form);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, this would create a collateral position in the database
 | 
			
		||||
        // For now, we'll just redirect back to the DeFi dashboard with a success message
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let purpose_str = match form.purpose.as_str() {
 | 
			
		||||
            "funds" => "secure a funds",
 | 
			
		||||
            "synthetic" => "generate synthetic assets",
 | 
			
		||||
            "leverage" => "leverage trading",
 | 
			
		||||
            _ => "collateralization",
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let success_message = format!(
 | 
			
		||||
            "Successfully collateralized {} {} for {}",
 | 
			
		||||
            form.amount, form.asset_id, purpose_str
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let success_message = format!("Successfully collateralized {} {} for {}", 
 | 
			
		||||
            form.amount, form.asset_id, purpose_str);
 | 
			
		||||
        
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                "Location",
 | 
			
		||||
                format!("/defi?success={}", urlencoding::encode(&success_message)),
 | 
			
		||||
            ))
 | 
			
		||||
            .append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Helper method to get DeFi statistics
 | 
			
		||||
    fn get_defi_stats() -> serde_json::Map<String, serde_json::Value> {
 | 
			
		||||
        let mut stats = serde_json::Map::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Handle Option<Number> by unwrapping with expect
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "total_value_locked".to_string(),
 | 
			
		||||
            serde_json::Value::Number(
 | 
			
		||||
                serde_json::Number::from_f64(1250000.0).expect("Valid float"),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "providing_volume".to_string(),
 | 
			
		||||
            serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")),
 | 
			
		||||
        );
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "receiving_volume".to_string(),
 | 
			
		||||
            serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")),
 | 
			
		||||
        );
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "liquidity_pools_count".to_string(),
 | 
			
		||||
            serde_json::Value::Number(serde_json::Number::from(12)),
 | 
			
		||||
        );
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "active_stakers".to_string(),
 | 
			
		||||
            serde_json::Value::Number(serde_json::Number::from(156)),
 | 
			
		||||
        );
 | 
			
		||||
        stats.insert(
 | 
			
		||||
            "total_swap_volume".to_string(),
 | 
			
		||||
            serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        stats.insert("total_value_locked".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(1250000.0).expect("Valid float")));
 | 
			
		||||
        stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")));
 | 
			
		||||
        stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")));
 | 
			
		||||
        stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12)));
 | 
			
		||||
        stats.insert("active_stakers".to_string(), serde_json::Value::Number(serde_json::Number::from(156)));
 | 
			
		||||
        stats.insert("total_swap_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")));
 | 
			
		||||
        
 | 
			
		||||
        stats
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Helper method to convert Asset to a JSON object for templates
 | 
			
		||||
    fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
 | 
			
		||||
        let mut map = serde_json::Map::new();
 | 
			
		||||
 | 
			
		||||
        map.insert(
 | 
			
		||||
            "id".to_string(),
 | 
			
		||||
            serde_json::Value::String(asset.id.clone()),
 | 
			
		||||
        );
 | 
			
		||||
        map.insert(
 | 
			
		||||
            "name".to_string(),
 | 
			
		||||
            serde_json::Value::String(asset.name.clone()),
 | 
			
		||||
        );
 | 
			
		||||
        map.insert(
 | 
			
		||||
            "description".to_string(),
 | 
			
		||||
            serde_json::Value::String(asset.description.clone()),
 | 
			
		||||
        );
 | 
			
		||||
        map.insert(
 | 
			
		||||
            "asset_type".to_string(),
 | 
			
		||||
            serde_json::Value::String(asset.asset_type.as_str().to_string()),
 | 
			
		||||
        );
 | 
			
		||||
        map.insert(
 | 
			
		||||
            "status".to_string(),
 | 
			
		||||
            serde_json::Value::String(asset.status.as_str().to_string()),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        map.insert("id".to_string(), serde_json::Value::String(asset.id.clone()));
 | 
			
		||||
        map.insert("name".to_string(), serde_json::Value::String(asset.name.clone()));
 | 
			
		||||
        map.insert("description".to_string(), serde_json::Value::String(asset.description.clone()));
 | 
			
		||||
        map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string()));
 | 
			
		||||
        map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string()));
 | 
			
		||||
        
 | 
			
		||||
        // Add current valuation
 | 
			
		||||
        if let Some(latest) = asset.latest_valuation() {
 | 
			
		||||
            if let Some(num) = serde_json::Number::from_f64(latest.value) {
 | 
			
		||||
                map.insert(
 | 
			
		||||
                    "current_valuation".to_string(),
 | 
			
		||||
                    serde_json::Value::Number(num),
 | 
			
		||||
                );
 | 
			
		||||
                map.insert("current_valuation".to_string(), serde_json::Value::Number(num));
 | 
			
		||||
            } else {
 | 
			
		||||
                map.insert(
 | 
			
		||||
                    "current_valuation".to_string(),
 | 
			
		||||
                    serde_json::Value::Number(serde_json::Number::from(0)),
 | 
			
		||||
                );
 | 
			
		||||
                map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
 | 
			
		||||
            }
 | 
			
		||||
            map.insert(
 | 
			
		||||
                "valuation_currency".to_string(),
 | 
			
		||||
                serde_json::Value::String(latest.currency.clone()),
 | 
			
		||||
            );
 | 
			
		||||
            map.insert(
 | 
			
		||||
                "valuation_date".to_string(),
 | 
			
		||||
                serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()),
 | 
			
		||||
            );
 | 
			
		||||
            map.insert("valuation_currency".to_string(), serde_json::Value::String(latest.currency.clone()));
 | 
			
		||||
            map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()));
 | 
			
		||||
        } else {
 | 
			
		||||
            map.insert(
 | 
			
		||||
                "current_valuation".to_string(),
 | 
			
		||||
                serde_json::Value::Number(serde_json::Number::from(0)),
 | 
			
		||||
            );
 | 
			
		||||
            map.insert(
 | 
			
		||||
                "valuation_currency".to_string(),
 | 
			
		||||
                serde_json::Value::String("USD".to_string()),
 | 
			
		||||
            );
 | 
			
		||||
            map.insert(
 | 
			
		||||
                "valuation_date".to_string(),
 | 
			
		||||
                serde_json::Value::String("N/A".to_string()),
 | 
			
		||||
            );
 | 
			
		||||
            map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
 | 
			
		||||
            map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string()));
 | 
			
		||||
            map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        map
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Generate mock assets for testing
 | 
			
		||||
    fn get_mock_assets() -> Vec<Asset> {
 | 
			
		||||
        // Reuse the asset controller's mock data function
 | 
			
		||||
 
 | 
			
		||||
@@ -1,382 +0,0 @@
 | 
			
		||||
use crate::controllers::error::render_company_not_found;
 | 
			
		||||
use crate::db::{company::get_company_by_id, document::*};
 | 
			
		||||
use crate::models::document::{DocumentStatistics, DocumentType};
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
use actix_multipart::Multipart;
 | 
			
		||||
use actix_web::{HttpRequest, HttpResponse, Result, web};
 | 
			
		||||
use futures_util::stream::StreamExt as _;
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::io::Write;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
 | 
			
		||||
// Form structs removed - not currently used in document operations
 | 
			
		||||
 | 
			
		||||
pub struct DocumentController;
 | 
			
		||||
 | 
			
		||||
impl DocumentController {
 | 
			
		||||
    /// Display company documents management page
 | 
			
		||||
    pub async fn index(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        req: HttpRequest,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        // Add active_page for navigation highlighting
 | 
			
		||||
        context.insert("active_page", &"company");
 | 
			
		||||
 | 
			
		||||
        // Parse query parameters for success/error messages
 | 
			
		||||
        let query_string = req.query_string();
 | 
			
		||||
 | 
			
		||||
        // Check for success message
 | 
			
		||||
        if let Some(pos) = query_string.find("success=") {
 | 
			
		||||
            let start = pos + 8;
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let success = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
 | 
			
		||||
            context.insert("success", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for error message
 | 
			
		||||
        if let Some(pos) = query_string.find("error=") {
 | 
			
		||||
            let start = pos + 6;
 | 
			
		||||
            let end = query_string[start..]
 | 
			
		||||
                .find('&')
 | 
			
		||||
                .map_or(query_string.len(), |e| e + start);
 | 
			
		||||
            let error = &query_string[start..end];
 | 
			
		||||
            let decoded = urlencoding::decode(error).unwrap_or_else(|_| error.into());
 | 
			
		||||
            context.insert("error", &decoded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Parse company ID
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => return render_company_not_found(&tmpl, Some(&company_id_str)).await,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Fetch company from database
 | 
			
		||||
        if let Ok(Some(company)) = get_company_by_id(company_id) {
 | 
			
		||||
            context.insert("company", &company);
 | 
			
		||||
            context.insert("company_id", &company_id);
 | 
			
		||||
 | 
			
		||||
            // Get documents for this company
 | 
			
		||||
            let documents = match get_company_documents(company_id) {
 | 
			
		||||
                Ok(documents) => documents,
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::error!("Failed to get documents for company {}: {}", company_id, e);
 | 
			
		||||
                    vec![]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Calculate statistics
 | 
			
		||||
            let stats = DocumentStatistics::new(&documents);
 | 
			
		||||
            context.insert("documents", &documents);
 | 
			
		||||
            context.insert("stats", &stats);
 | 
			
		||||
 | 
			
		||||
            // Add document types for dropdown (as template-friendly tuples)
 | 
			
		||||
            let document_types: Vec<(String, String)> = DocumentType::all()
 | 
			
		||||
                .into_iter()
 | 
			
		||||
                .map(|dt| (format!("{:?}", dt), dt.as_str().to_string()))
 | 
			
		||||
                .collect();
 | 
			
		||||
            context.insert("document_types", &document_types);
 | 
			
		||||
 | 
			
		||||
            render_template(&tmpl, "company/documents.html", &context)
 | 
			
		||||
        } else {
 | 
			
		||||
            render_company_not_found(&tmpl, Some(&company_id_str)).await
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Handle document upload
 | 
			
		||||
    pub async fn upload(path: web::Path<String>, mut payload: Multipart) -> Result<HttpResponse> {
 | 
			
		||||
        use actix_web::http::header;
 | 
			
		||||
 | 
			
		||||
        let company_id_str = path.into_inner();
 | 
			
		||||
        log::info!("Document upload request for company: {}", company_id_str);
 | 
			
		||||
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/documents/{}?error=Invalid company ID",
 | 
			
		||||
                            company_id_str
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut form_fields: HashMap<String, String> = HashMap::new();
 | 
			
		||||
        let mut uploaded_files = Vec::new();
 | 
			
		||||
 | 
			
		||||
        // Parse multipart form
 | 
			
		||||
        log::info!("Starting multipart form parsing");
 | 
			
		||||
        while let Some(Ok(mut field)) = payload.next().await {
 | 
			
		||||
            let content_disposition = field.content_disposition();
 | 
			
		||||
            let field_name = content_disposition
 | 
			
		||||
                .get_name()
 | 
			
		||||
                .unwrap_or("unknown")
 | 
			
		||||
                .to_string();
 | 
			
		||||
            let filename = content_disposition.get_filename().map(|f| f.to_string());
 | 
			
		||||
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Processing field: {} (filename: {:?})",
 | 
			
		||||
                field_name,
 | 
			
		||||
                filename
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if field_name == "documents" {
 | 
			
		||||
                // Handle file upload
 | 
			
		||||
                if let Some(filename) = filename {
 | 
			
		||||
                    let mut file_data = Vec::new();
 | 
			
		||||
                    while let Some(chunk) = field.next().await {
 | 
			
		||||
                        let data = chunk.unwrap();
 | 
			
		||||
                        file_data.extend_from_slice(&data);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if !file_data.is_empty() {
 | 
			
		||||
                        uploaded_files.push((filename, file_data));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Handle form fields
 | 
			
		||||
                let mut field_data = Vec::new();
 | 
			
		||||
                while let Some(chunk) = field.next().await {
 | 
			
		||||
                    let data = chunk.unwrap();
 | 
			
		||||
                    field_data.extend_from_slice(&data);
 | 
			
		||||
                }
 | 
			
		||||
                let field_value = String::from_utf8_lossy(&field_data).to_string();
 | 
			
		||||
                form_fields.insert(field_name, field_value);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Multipart parsing complete. Files: {}, Form fields: {:?}",
 | 
			
		||||
            uploaded_files.len(),
 | 
			
		||||
            form_fields
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if uploaded_files.is_empty() {
 | 
			
		||||
            log::warn!("No files uploaded");
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    format!("/company/documents/{}?error=No files selected", company_id),
 | 
			
		||||
                ))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create uploads directory if it doesn't exist
 | 
			
		||||
        let upload_dir = format!("/tmp/company_{}_documents", company_id);
 | 
			
		||||
        if let Err(e) = fs::create_dir_all(&upload_dir) {
 | 
			
		||||
            log::error!("Failed to create upload directory: {}", e);
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    format!(
 | 
			
		||||
                        "/company/documents/{}?error=Failed to create upload directory",
 | 
			
		||||
                        company_id
 | 
			
		||||
                    ),
 | 
			
		||||
                ))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let document_type = DocumentType::from_str(
 | 
			
		||||
            &form_fields
 | 
			
		||||
                .get("document_type")
 | 
			
		||||
                .cloned()
 | 
			
		||||
                .unwrap_or_default(),
 | 
			
		||||
        );
 | 
			
		||||
        let description = form_fields.get("description").cloned();
 | 
			
		||||
        let is_public = form_fields.get("is_public").map_or(false, |v| v == "on");
 | 
			
		||||
 | 
			
		||||
        let mut success_count = 0;
 | 
			
		||||
        let mut error_count = 0;
 | 
			
		||||
 | 
			
		||||
        // Process each uploaded file
 | 
			
		||||
        for (filename, file_data) in uploaded_files {
 | 
			
		||||
            let file_path = format!("{}/{}", upload_dir, filename);
 | 
			
		||||
 | 
			
		||||
            // Save file to disk
 | 
			
		||||
            match fs::File::create(&file_path) {
 | 
			
		||||
                Ok(mut file) => {
 | 
			
		||||
                    if let Err(e) = file.write_all(&file_data) {
 | 
			
		||||
                        log::error!("Failed to write file {}: {}", filename, e);
 | 
			
		||||
                        error_count += 1;
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::error!("Failed to create file {}: {}", filename, e);
 | 
			
		||||
                    error_count += 1;
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Determine MIME type based on file extension
 | 
			
		||||
            let mime_type = match Path::new(&filename)
 | 
			
		||||
                .extension()
 | 
			
		||||
                .and_then(|ext| ext.to_str())
 | 
			
		||||
                .map(|ext| ext.to_lowercase())
 | 
			
		||||
                .as_deref()
 | 
			
		||||
            {
 | 
			
		||||
                Some("pdf") => "application/pdf",
 | 
			
		||||
                Some("doc") | Some("docx") => "application/msword",
 | 
			
		||||
                Some("jpg") | Some("jpeg") => "image/jpeg",
 | 
			
		||||
                Some("png") => "image/png",
 | 
			
		||||
                Some("txt") => "text/plain",
 | 
			
		||||
                _ => "application/octet-stream",
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Save document to database
 | 
			
		||||
            match create_new_document(
 | 
			
		||||
                filename.clone(),
 | 
			
		||||
                file_path,
 | 
			
		||||
                file_data.len() as u64,
 | 
			
		||||
                mime_type.to_string(),
 | 
			
		||||
                company_id,
 | 
			
		||||
                "System".to_string(), // TODO: Use actual logged-in user
 | 
			
		||||
                document_type.clone(),
 | 
			
		||||
                description.clone(),
 | 
			
		||||
                is_public,
 | 
			
		||||
                None, // TODO: Calculate checksum
 | 
			
		||||
            ) {
 | 
			
		||||
                Ok(_) => success_count += 1,
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::error!("Failed to save document {} to database: {}", filename, e);
 | 
			
		||||
                    error_count += 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let message = if error_count == 0 {
 | 
			
		||||
            format!("Successfully uploaded {} document(s)", success_count)
 | 
			
		||||
        } else {
 | 
			
		||||
            format!(
 | 
			
		||||
                "Uploaded {} document(s), {} failed",
 | 
			
		||||
                success_count, error_count
 | 
			
		||||
            )
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .append_header((
 | 
			
		||||
                header::LOCATION,
 | 
			
		||||
                format!(
 | 
			
		||||
                    "/company/documents/{}?success={}",
 | 
			
		||||
                    company_id,
 | 
			
		||||
                    urlencoding::encode(&message)
 | 
			
		||||
                ),
 | 
			
		||||
            ))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Delete a document
 | 
			
		||||
    pub async fn delete(path: web::Path<(String, String)>) -> Result<HttpResponse> {
 | 
			
		||||
        use actix_web::http::header;
 | 
			
		||||
 | 
			
		||||
        let (company_id_str, document_id_str) = path.into_inner();
 | 
			
		||||
        let company_id = match company_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/documents/{}?error=Invalid company ID",
 | 
			
		||||
                            company_id_str
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let document_id = match document_id_str.parse::<u32>() {
 | 
			
		||||
            Ok(id) => id,
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/documents/{}?error=Invalid document ID",
 | 
			
		||||
                            company_id
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get document to check if it exists and belongs to the company
 | 
			
		||||
        match get_document_by_id(document_id) {
 | 
			
		||||
            Ok(Some(document)) => {
 | 
			
		||||
                if document.company_id != company_id {
 | 
			
		||||
                    return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                        .append_header((
 | 
			
		||||
                            header::LOCATION,
 | 
			
		||||
                            format!("/company/documents/{}?error=Document not found", company_id),
 | 
			
		||||
                        ))
 | 
			
		||||
                        .finish());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Delete file from disk
 | 
			
		||||
                if let Err(e) = fs::remove_file(&document.file_path) {
 | 
			
		||||
                    log::warn!("Failed to delete file {}: {}", document.file_path, e);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Delete from database
 | 
			
		||||
                match delete_document(document_id) {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        let message = format!("Successfully deleted document '{}'", document.name);
 | 
			
		||||
                        Ok(HttpResponse::SeeOther()
 | 
			
		||||
                            .append_header((
 | 
			
		||||
                                header::LOCATION,
 | 
			
		||||
                                format!(
 | 
			
		||||
                                    "/company/documents/{}?success={}",
 | 
			
		||||
                                    company_id,
 | 
			
		||||
                                    urlencoding::encode(&message)
 | 
			
		||||
                                ),
 | 
			
		||||
                            ))
 | 
			
		||||
                            .finish())
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        log::error!("Failed to delete document from database: {}", e);
 | 
			
		||||
                        Ok(HttpResponse::SeeOther()
 | 
			
		||||
                            .append_header((
 | 
			
		||||
                                header::LOCATION,
 | 
			
		||||
                                format!(
 | 
			
		||||
                                    "/company/documents/{}?error=Failed to delete document",
 | 
			
		||||
                                    company_id
 | 
			
		||||
                                ),
 | 
			
		||||
                            ))
 | 
			
		||||
                            .finish())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Ok(None) => Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((
 | 
			
		||||
                    header::LOCATION,
 | 
			
		||||
                    format!("/company/documents/{}?error=Document not found", company_id),
 | 
			
		||||
                ))
 | 
			
		||||
                .finish()),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to get document: {}", e);
 | 
			
		||||
                Ok(HttpResponse::SeeOther()
 | 
			
		||||
                    .append_header((
 | 
			
		||||
                        header::LOCATION,
 | 
			
		||||
                        format!(
 | 
			
		||||
                            "/company/documents/{}?error=Failed to access document",
 | 
			
		||||
                            company_id
 | 
			
		||||
                        ),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .finish())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,125 +0,0 @@
 | 
			
		||||
use actix_web::{Error, HttpResponse, web};
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
 | 
			
		||||
pub struct ErrorController;
 | 
			
		||||
 | 
			
		||||
impl ErrorController {
 | 
			
		||||
    /// Renders a 404 Not Found page with customizable content
 | 
			
		||||
    pub async fn not_found(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        error_title: Option<&str>,
 | 
			
		||||
        error_message: Option<&str>,
 | 
			
		||||
        return_url: Option<&str>,
 | 
			
		||||
        return_text: Option<&str>,
 | 
			
		||||
    ) -> Result<HttpResponse, Error> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        // Set default or custom error content
 | 
			
		||||
        context.insert("error_title", &error_title.unwrap_or("Page Not Found"));
 | 
			
		||||
        context.insert(
 | 
			
		||||
            "error_message",
 | 
			
		||||
            &error_message
 | 
			
		||||
                .unwrap_or("The page you're looking for doesn't exist or has been moved."),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Optional return URL and text
 | 
			
		||||
        if let Some(url) = return_url {
 | 
			
		||||
            context.insert("return_url", &url);
 | 
			
		||||
            context.insert("return_text", &return_text.unwrap_or("Return"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Render the 404 template with 404 status
 | 
			
		||||
        match tmpl.render("errors/404.html", &context) {
 | 
			
		||||
            Ok(rendered) => Ok(HttpResponse::NotFound()
 | 
			
		||||
                .content_type("text/html; charset=utf-8")
 | 
			
		||||
                .body(rendered)),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                log::error!("Failed to render 404 template: {}", e);
 | 
			
		||||
                // Fallback to simple text response
 | 
			
		||||
                Ok(HttpResponse::NotFound()
 | 
			
		||||
                    .content_type("text/plain")
 | 
			
		||||
                    .body("404 - Page Not Found"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Renders a 404 page for contract not found
 | 
			
		||||
    pub async fn contract_not_found(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        contract_id: Option<&str>,
 | 
			
		||||
    ) -> Result<HttpResponse, Error> {
 | 
			
		||||
        let error_title = "Contract Not Found";
 | 
			
		||||
        let error_message = if let Some(id) = contract_id {
 | 
			
		||||
            format!(
 | 
			
		||||
                "The contract with ID '{}' doesn't exist or has been removed.",
 | 
			
		||||
                id
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            "The contract you're looking for doesn't exist or has been removed.".to_string()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Self::not_found(
 | 
			
		||||
            tmpl,
 | 
			
		||||
            Some(error_title),
 | 
			
		||||
            Some(&error_message),
 | 
			
		||||
            Some("/contracts"),
 | 
			
		||||
            Some("Back to Contracts"),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // calendar_event_not_found removed - not used
 | 
			
		||||
 | 
			
		||||
    /// Renders a 404 page for company not found
 | 
			
		||||
    pub async fn company_not_found(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        company_id: Option<&str>,
 | 
			
		||||
    ) -> Result<HttpResponse, Error> {
 | 
			
		||||
        let error_title = "Company Not Found";
 | 
			
		||||
        let error_message = if let Some(id) = company_id {
 | 
			
		||||
            format!(
 | 
			
		||||
                "The company with ID '{}' doesn't exist or has been removed.",
 | 
			
		||||
                id
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            "The company you're looking for doesn't exist or has been removed.".to_string()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Self::not_found(
 | 
			
		||||
            tmpl,
 | 
			
		||||
            Some(error_title),
 | 
			
		||||
            Some(&error_message),
 | 
			
		||||
            Some("/company"),
 | 
			
		||||
            Some("Back to Companies"),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Renders a generic 404 page
 | 
			
		||||
    pub async fn generic_not_found(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
 | 
			
		||||
        Self::not_found(tmpl, None, None, None, None).await
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper function to quickly render a contract not found response
 | 
			
		||||
pub async fn render_contract_not_found(
 | 
			
		||||
    tmpl: &web::Data<Tera>,
 | 
			
		||||
    contract_id: Option<&str>,
 | 
			
		||||
) -> Result<HttpResponse, Error> {
 | 
			
		||||
    ErrorController::contract_not_found(tmpl.clone(), contract_id).await
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// render_calendar_event_not_found removed - not used
 | 
			
		||||
 | 
			
		||||
/// Helper function to quickly render a company not found response
 | 
			
		||||
pub async fn render_company_not_found(
 | 
			
		||||
    tmpl: &web::Data<Tera>,
 | 
			
		||||
    company_id: Option<&str>,
 | 
			
		||||
) -> Result<HttpResponse, Error> {
 | 
			
		||||
    ErrorController::company_not_found(tmpl.clone(), company_id).await
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper function to quickly render a generic not found response
 | 
			
		||||
pub async fn render_generic_not_found(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
 | 
			
		||||
    ErrorController::generic_not_found(tmpl).await
 | 
			
		||||
}
 | 
			
		||||
@@ -609,7 +609,6 @@ impl FlowController {
 | 
			
		||||
 | 
			
		||||
/// Form for creating a new flow
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct FlowForm {
 | 
			
		||||
    /// Flow name
 | 
			
		||||
    pub name: String,
 | 
			
		||||
@@ -621,7 +620,6 @@ pub struct FlowForm {
 | 
			
		||||
 | 
			
		||||
/// Form for marking a step as stuck
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct StuckForm {
 | 
			
		||||
    /// Reason for being stuck
 | 
			
		||||
    pub reason: String,
 | 
			
		||||
@@ -629,7 +627,6 @@ pub struct StuckForm {
 | 
			
		||||
 | 
			
		||||
/// Form for adding a log to a step
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct LogForm {
 | 
			
		||||
    /// Log message
 | 
			
		||||
    pub message: String,
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,418 +0,0 @@
 | 
			
		||||
use actix_web::{HttpResponse, Result, web};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct HealthStatus {
 | 
			
		||||
    pub status: String,
 | 
			
		||||
    pub timestamp: String,
 | 
			
		||||
    pub version: String,
 | 
			
		||||
    pub uptime_seconds: u64,
 | 
			
		||||
    pub checks: Vec<HealthCheck>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
pub struct HealthCheck {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub status: String,
 | 
			
		||||
    pub response_time_ms: u64,
 | 
			
		||||
    pub message: Option<String>,
 | 
			
		||||
    pub details: Option<serde_json::Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HealthStatus {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            status: "unknown".to_string(),
 | 
			
		||||
            timestamp: chrono::Utc::now().to_rfc3339(),
 | 
			
		||||
            version: env!("CARGO_PKG_VERSION").to_string(),
 | 
			
		||||
            uptime_seconds: 0,
 | 
			
		||||
            checks: Vec::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_uptime(&mut self, uptime: Duration) {
 | 
			
		||||
        self.uptime_seconds = uptime.as_secs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_check(&mut self, check: HealthCheck) {
 | 
			
		||||
        self.checks.push(check);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn calculate_overall_status(&mut self) {
 | 
			
		||||
        let all_healthy = self.checks.iter().all(|check| check.status == "healthy");
 | 
			
		||||
        let any_degraded = self.checks.iter().any(|check| check.status == "degraded");
 | 
			
		||||
 | 
			
		||||
        self.status = if all_healthy {
 | 
			
		||||
            "healthy".to_string()
 | 
			
		||||
        } else if any_degraded {
 | 
			
		||||
            "degraded".to_string()
 | 
			
		||||
        } else {
 | 
			
		||||
            "unhealthy".to_string()
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HealthCheck {
 | 
			
		||||
    pub fn new(name: &str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            status: "unknown".to_string(),
 | 
			
		||||
            response_time_ms: 0,
 | 
			
		||||
            message: None,
 | 
			
		||||
            details: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn healthy(name: &str, response_time_ms: u64) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            status: "healthy".to_string(),
 | 
			
		||||
            response_time_ms,
 | 
			
		||||
            message: Some("OK".to_string()),
 | 
			
		||||
            details: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn degraded(name: &str, response_time_ms: u64, message: &str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            status: "degraded".to_string(),
 | 
			
		||||
            response_time_ms,
 | 
			
		||||
            message: Some(message.to_string()),
 | 
			
		||||
            details: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn unhealthy(name: &str, response_time_ms: u64, error: &str) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            status: "unhealthy".to_string(),
 | 
			
		||||
            response_time_ms,
 | 
			
		||||
            message: Some(error.to_string()),
 | 
			
		||||
            details: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_details(mut self, details: serde_json::Value) -> Self {
 | 
			
		||||
        self.details = Some(details);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Health check endpoint
 | 
			
		||||
pub async fn health_check() -> Result<HttpResponse> {
 | 
			
		||||
    let start_time = Instant::now();
 | 
			
		||||
    let mut status = HealthStatus::new();
 | 
			
		||||
 | 
			
		||||
    // Set uptime (in a real app, you'd track this from startup)
 | 
			
		||||
    status.set_uptime(Duration::from_secs(3600)); // Placeholder
 | 
			
		||||
 | 
			
		||||
    // Check database connectivity
 | 
			
		||||
    let db_check = check_database_health().await;
 | 
			
		||||
    status.add_check(db_check);
 | 
			
		||||
 | 
			
		||||
    // Check Redis connectivity
 | 
			
		||||
    let redis_check = check_redis_health().await;
 | 
			
		||||
    status.add_check(redis_check);
 | 
			
		||||
 | 
			
		||||
    // Check Stripe connectivity
 | 
			
		||||
    let stripe_check = check_stripe_health().await;
 | 
			
		||||
    status.add_check(stripe_check);
 | 
			
		||||
 | 
			
		||||
    // Check file system
 | 
			
		||||
    let fs_check = check_filesystem_health().await;
 | 
			
		||||
    status.add_check(fs_check);
 | 
			
		||||
 | 
			
		||||
    // Check memory usage
 | 
			
		||||
    let memory_check = check_memory_health().await;
 | 
			
		||||
    status.add_check(memory_check);
 | 
			
		||||
 | 
			
		||||
    // Calculate overall status
 | 
			
		||||
    status.calculate_overall_status();
 | 
			
		||||
 | 
			
		||||
    let response_code = match status.status.as_str() {
 | 
			
		||||
        "healthy" => 200,
 | 
			
		||||
        "degraded" => 200, // Still operational
 | 
			
		||||
        _ => 503,          // Service unavailable
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Health check completed in {}ms - Status: {}",
 | 
			
		||||
        start_time.elapsed().as_millis(),
 | 
			
		||||
        status.status
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(
 | 
			
		||||
        HttpResponse::build(actix_web::http::StatusCode::from_u16(response_code).unwrap())
 | 
			
		||||
            .json(status),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Detailed health check endpoint for monitoring systems
 | 
			
		||||
pub async fn health_check_detailed() -> Result<HttpResponse> {
 | 
			
		||||
    let start_time = Instant::now();
 | 
			
		||||
    let mut status = HealthStatus::new();
 | 
			
		||||
 | 
			
		||||
    // Set uptime
 | 
			
		||||
    status.set_uptime(Duration::from_secs(3600)); // Placeholder
 | 
			
		||||
 | 
			
		||||
    // Detailed database check
 | 
			
		||||
    let db_check = check_database_health_detailed().await;
 | 
			
		||||
    status.add_check(db_check);
 | 
			
		||||
 | 
			
		||||
    // Detailed Redis check
 | 
			
		||||
    let redis_check = check_redis_health_detailed().await;
 | 
			
		||||
    status.add_check(redis_check);
 | 
			
		||||
 | 
			
		||||
    // Detailed Stripe check
 | 
			
		||||
    let stripe_check = check_stripe_health_detailed().await;
 | 
			
		||||
    status.add_check(stripe_check);
 | 
			
		||||
 | 
			
		||||
    // Check external dependencies
 | 
			
		||||
    let external_check = check_external_dependencies().await;
 | 
			
		||||
    status.add_check(external_check);
 | 
			
		||||
 | 
			
		||||
    // Performance metrics
 | 
			
		||||
    let perf_check = check_performance_metrics().await;
 | 
			
		||||
    status.add_check(perf_check);
 | 
			
		||||
 | 
			
		||||
    status.calculate_overall_status();
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Detailed health check completed in {}ms - Status: {}",
 | 
			
		||||
        start_time.elapsed().as_millis(),
 | 
			
		||||
        status.status
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(status))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Simple readiness check for load balancers
 | 
			
		||||
pub async fn readiness_check() -> Result<HttpResponse> {
 | 
			
		||||
    // Quick checks for essential services
 | 
			
		||||
    let db_ok = check_database_connectivity().await;
 | 
			
		||||
    let redis_ok = check_redis_connectivity().await;
 | 
			
		||||
 | 
			
		||||
    if db_ok && redis_ok {
 | 
			
		||||
        Ok(HttpResponse::Ok().json(serde_json::json!({
 | 
			
		||||
            "status": "ready",
 | 
			
		||||
            "timestamp": chrono::Utc::now().to_rfc3339()
 | 
			
		||||
        })))
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok(HttpResponse::ServiceUnavailable().json(serde_json::json!({
 | 
			
		||||
            "status": "not_ready",
 | 
			
		||||
            "timestamp": chrono::Utc::now().to_rfc3339()
 | 
			
		||||
        })))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Simple liveness check
 | 
			
		||||
pub async fn liveness_check() -> Result<HttpResponse> {
 | 
			
		||||
    Ok(HttpResponse::Ok().json(serde_json::json!({
 | 
			
		||||
        "status": "alive",
 | 
			
		||||
        "timestamp": chrono::Utc::now().to_rfc3339(),
 | 
			
		||||
        "version": env!("CARGO_PKG_VERSION")
 | 
			
		||||
    })))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Health check implementations
 | 
			
		||||
 | 
			
		||||
async fn check_database_health() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    match crate::db::db::get_db() {
 | 
			
		||||
        Ok(_) => HealthCheck::healthy("database", start.elapsed().as_millis() as u64),
 | 
			
		||||
        Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
            "database",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            &format!("Database connection failed: {}", e),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_database_health_detailed() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    match crate::db::db::get_db() {
 | 
			
		||||
        Ok(db) => {
 | 
			
		||||
            // Try to perform a simple operation
 | 
			
		||||
            let details = serde_json::json!({
 | 
			
		||||
                "connection_pool_size": "N/A", // Would need to expose from heromodels
 | 
			
		||||
                "active_connections": "N/A",
 | 
			
		||||
                "database_size": "N/A"
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            HealthCheck::healthy("database", start.elapsed().as_millis() as u64)
 | 
			
		||||
                .with_details(details)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
            "database",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            &format!("Database connection failed: {}", e),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_redis_health() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    // Try to connect to Redis
 | 
			
		||||
    match crate::utils::redis_service::get_connection() {
 | 
			
		||||
        Ok(_) => HealthCheck::healthy("redis", start.elapsed().as_millis() as u64),
 | 
			
		||||
        Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
            "redis",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            &format!("Redis connection failed: {}", e),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_redis_health_detailed() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    match crate::utils::redis_service::get_connection() {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            let details = serde_json::json!({
 | 
			
		||||
                "connection_status": "connected",
 | 
			
		||||
                "memory_usage": "N/A",
 | 
			
		||||
                "connected_clients": "N/A"
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            HealthCheck::healthy("redis", start.elapsed().as_millis() as u64).with_details(details)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
            "redis",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            &format!("Redis connection failed: {}", e),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_stripe_health() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    // Check if Stripe configuration is available
 | 
			
		||||
    let config = crate::config::get_config();
 | 
			
		||||
    if !config.stripe.secret_key.is_empty() {
 | 
			
		||||
        HealthCheck::healthy("stripe", start.elapsed().as_millis() as u64)
 | 
			
		||||
    } else {
 | 
			
		||||
        HealthCheck::degraded(
 | 
			
		||||
            "stripe",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            "Stripe secret key not configured",
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_stripe_health_detailed() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    let config = crate::config::get_config();
 | 
			
		||||
    let has_secret = !config.stripe.secret_key.is_empty();
 | 
			
		||||
    let has_webhook_secret = config.stripe.webhook_secret.is_some();
 | 
			
		||||
 | 
			
		||||
    let details = serde_json::json!({
 | 
			
		||||
        "secret_key_configured": has_secret,
 | 
			
		||||
        "webhook_secret_configured": has_webhook_secret,
 | 
			
		||||
        "api_version": "2023-10-16" // Current Stripe API version
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if has_secret && has_webhook_secret {
 | 
			
		||||
        HealthCheck::healthy("stripe", start.elapsed().as_millis() as u64).with_details(details)
 | 
			
		||||
    } else {
 | 
			
		||||
        HealthCheck::degraded(
 | 
			
		||||
            "stripe",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            "Stripe configuration incomplete",
 | 
			
		||||
        )
 | 
			
		||||
        .with_details(details)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_filesystem_health() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    // Check if we can write to the data directory
 | 
			
		||||
    match std::fs::create_dir_all("data") {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            // Try to write a test file
 | 
			
		||||
            match std::fs::write("data/.health_check", "test") {
 | 
			
		||||
                Ok(_) => {
 | 
			
		||||
                    // Clean up
 | 
			
		||||
                    let _ = std::fs::remove_file("data/.health_check");
 | 
			
		||||
                    HealthCheck::healthy("filesystem", start.elapsed().as_millis() as u64)
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
                    "filesystem",
 | 
			
		||||
                    start.elapsed().as_millis() as u64,
 | 
			
		||||
                    &format!("Cannot write to data directory: {}", e),
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => HealthCheck::unhealthy(
 | 
			
		||||
            "filesystem",
 | 
			
		||||
            start.elapsed().as_millis() as u64,
 | 
			
		||||
            &format!("Cannot create data directory: {}", e),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_memory_health() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    // Basic memory check (in a real app, you'd use system metrics)
 | 
			
		||||
    let details = serde_json::json!({
 | 
			
		||||
        "status": "basic_check_only",
 | 
			
		||||
        "note": "Detailed memory metrics require system integration"
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    HealthCheck::healthy("memory", start.elapsed().as_millis() as u64).with_details(details)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_external_dependencies() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    // Check external services (placeholder)
 | 
			
		||||
    let details = serde_json::json!({
 | 
			
		||||
        "external_apis": "not_implemented",
 | 
			
		||||
        "third_party_services": "not_implemented"
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    HealthCheck::healthy("external_dependencies", start.elapsed().as_millis() as u64)
 | 
			
		||||
        .with_details(details)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_performance_metrics() -> HealthCheck {
 | 
			
		||||
    let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
    let details = serde_json::json!({
 | 
			
		||||
        "avg_response_time_ms": "N/A",
 | 
			
		||||
        "requests_per_second": "N/A",
 | 
			
		||||
        "error_rate": "N/A",
 | 
			
		||||
        "cpu_usage": "N/A"
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    HealthCheck::healthy("performance", start.elapsed().as_millis() as u64).with_details(details)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Quick connectivity checks for readiness
 | 
			
		||||
 | 
			
		||||
async fn check_database_connectivity() -> bool {
 | 
			
		||||
    crate::db::db::get_db().is_ok()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn check_redis_connectivity() -> bool {
 | 
			
		||||
    crate::utils::redis_service::get_connection().is_ok()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Configure health check routes
 | 
			
		||||
pub fn configure_health_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
    cfg.service(
 | 
			
		||||
        web::scope("/health")
 | 
			
		||||
            .route("", web::get().to(health_check))
 | 
			
		||||
            .route("/detailed", web::get().to(health_check_detailed))
 | 
			
		||||
            .route("/ready", web::get().to(readiness_check))
 | 
			
		||||
            .route("/live", web::get().to(liveness_check)),
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -96,7 +96,6 @@ impl HomeController {
 | 
			
		||||
 | 
			
		||||
/// Represents the data submitted in the contact form
 | 
			
		||||
#[derive(Debug, serde::Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct ContactForm {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
use actix_web::{HttpResponse, Result, http, web};
 | 
			
		||||
use chrono::{Duration, Utc};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use actix_web::{web, HttpResponse, Result, http};
 | 
			
		||||
use tera::{Context, Tera};
 | 
			
		||||
use chrono::{Utc, Duration};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::models::asset::{Asset, AssetType, AssetStatus};
 | 
			
		||||
use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
 | 
			
		||||
use crate::controllers::asset::AssetController;
 | 
			
		||||
use crate::models::asset::{Asset, AssetStatus, AssetType};
 | 
			
		||||
use crate::models::marketplace::{Listing, ListingStatus, ListingType, MarketplaceStatistics};
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
@@ -21,7 +22,6 @@ pub struct ListingForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct BidForm {
 | 
			
		||||
    pub amount: f64,
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
@@ -38,33 +38,30 @@ impl MarketplaceController {
 | 
			
		||||
    // Display the marketplace dashboard
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
        let stats = MarketplaceStatistics::new(&listings);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get featured listings (up to 4)
 | 
			
		||||
        let featured_listings: Vec<&Listing> = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let featured_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.featured && l.status == ListingStatus::Active)
 | 
			
		||||
            .take(4)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get recent listings (up to 8)
 | 
			
		||||
        let mut recent_listings: Vec<&Listing> = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let mut recent_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Sort by created_at (newest first)
 | 
			
		||||
        recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
        let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get recent sales (up to 5)
 | 
			
		||||
        let mut recent_sales: Vec<&Listing> = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let mut recent_sales: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Sold)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Sort by sold_at (newest first)
 | 
			
		||||
        recent_sales.sort_by(|a, b| {
 | 
			
		||||
            let a_sold = a.sold_at.unwrap_or(a.created_at);
 | 
			
		||||
@@ -72,101 +69,88 @@ impl MarketplaceController {
 | 
			
		||||
            b_sold.cmp(&a_sold)
 | 
			
		||||
        });
 | 
			
		||||
        let recent_sales = recent_sales.into_iter().take(5).collect::<Vec<_>>();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add data to context
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("stats", &stats);
 | 
			
		||||
        context.insert("featured_listings", &featured_listings);
 | 
			
		||||
        context.insert("recent_listings", &recent_listings);
 | 
			
		||||
        context.insert("recent_sales", &recent_sales);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/index.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Display all marketplace listings
 | 
			
		||||
    pub async fn list_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Filter active listings
 | 
			
		||||
        let active_listings: Vec<&Listing> = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let active_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("listings", &active_listings);
 | 
			
		||||
        context.insert(
 | 
			
		||||
            "listing_types",
 | 
			
		||||
            &[
 | 
			
		||||
                ListingType::FixedPrice.as_str(),
 | 
			
		||||
                ListingType::Auction.as_str(),
 | 
			
		||||
                ListingType::Exchange.as_str(),
 | 
			
		||||
            ],
 | 
			
		||||
        );
 | 
			
		||||
        context.insert(
 | 
			
		||||
            "asset_types",
 | 
			
		||||
            &[
 | 
			
		||||
                AssetType::Token.as_str(),
 | 
			
		||||
                AssetType::Artwork.as_str(),
 | 
			
		||||
                AssetType::RealEstate.as_str(),
 | 
			
		||||
                AssetType::IntellectualProperty.as_str(),
 | 
			
		||||
                AssetType::Commodity.as_str(),
 | 
			
		||||
                AssetType::Share.as_str(),
 | 
			
		||||
                AssetType::Bond.as_str(),
 | 
			
		||||
                AssetType::Other.as_str(),
 | 
			
		||||
            ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        context.insert("listing_types", &[
 | 
			
		||||
            ListingType::FixedPrice.as_str(),
 | 
			
		||||
            ListingType::Auction.as_str(),
 | 
			
		||||
            ListingType::Exchange.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        context.insert("asset_types", &[
 | 
			
		||||
            AssetType::Token.as_str(),
 | 
			
		||||
            AssetType::Artwork.as_str(),
 | 
			
		||||
            AssetType::RealEstate.as_str(),
 | 
			
		||||
            AssetType::IntellectualProperty.as_str(),
 | 
			
		||||
            AssetType::Commodity.as_str(),
 | 
			
		||||
            AssetType::Share.as_str(),
 | 
			
		||||
            AssetType::Bond.as_str(),
 | 
			
		||||
            AssetType::Other.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/listings.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Display my listings
 | 
			
		||||
    pub async fn my_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Filter by current user (mock user ID)
 | 
			
		||||
        let user_id = "user-123";
 | 
			
		||||
        let my_listings: Vec<&Listing> =
 | 
			
		||||
            listings.iter().filter(|l| l.seller_id == user_id).collect();
 | 
			
		||||
 | 
			
		||||
        let my_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
            .filter(|l| l.seller_id == user_id)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("listings", &my_listings);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/my_listings.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Display listing details
 | 
			
		||||
    pub async fn listing_detail(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
    pub async fn listing_detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        let listings = Self::get_mock_listings();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Find the listing
 | 
			
		||||
        let listing = listings.iter().find(|l| l.id == listing_id);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if let Some(listing) = listing {
 | 
			
		||||
            // Get similar listings (same asset type, active)
 | 
			
		||||
            let similar_listings: Vec<&Listing> = listings
 | 
			
		||||
                .iter()
 | 
			
		||||
                .filter(|l| {
 | 
			
		||||
                    l.asset_type == listing.asset_type
 | 
			
		||||
                        && l.status == ListingStatus::Active
 | 
			
		||||
                        && l.id != listing.id
 | 
			
		||||
                })
 | 
			
		||||
            let similar_listings: Vec<&Listing> = listings.iter()
 | 
			
		||||
                .filter(|l| l.asset_type == listing.asset_type &&
 | 
			
		||||
                       l.status == ListingStatus::Active &&
 | 
			
		||||
                       l.id != listing.id)
 | 
			
		||||
                .take(4)
 | 
			
		||||
                .collect();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Get highest bid amount and minimum bid for auction listings
 | 
			
		||||
            let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction
 | 
			
		||||
            {
 | 
			
		||||
            let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction {
 | 
			
		||||
                if let Some(bid) = listing.highest_bid() {
 | 
			
		||||
                    (Some(bid.amount), bid.amount + 1.0)
 | 
			
		||||
                } else {
 | 
			
		||||
@@ -175,79 +159,74 @@ impl MarketplaceController {
 | 
			
		||||
            } else {
 | 
			
		||||
                (None, 0.0)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            context.insert("active_page", &"marketplace");
 | 
			
		||||
            context.insert("listing", listing);
 | 
			
		||||
            context.insert("similar_listings", &similar_listings);
 | 
			
		||||
            context.insert("highest_bid_amount", &highest_bid_amount);
 | 
			
		||||
            context.insert("minimum_bid", &minimum_bid);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Add current user info for bid/purchase forms
 | 
			
		||||
            let user_id = "user-123";
 | 
			
		||||
            let user_name = "Alice Hostly";
 | 
			
		||||
            context.insert("user_id", &user_id);
 | 
			
		||||
            context.insert("user_name", &user_name);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            render_template(&tmpl, "marketplace/listing_detail.html", &context)
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(HttpResponse::NotFound().finish())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Display create listing form
 | 
			
		||||
    pub async fn create_listing_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
 | 
			
		||||
        let mut context = Context::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get user's assets for selection
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let user_id = "user-123"; // Mock user ID
 | 
			
		||||
 | 
			
		||||
        let user_assets: Vec<&Asset> = assets
 | 
			
		||||
            .iter()
 | 
			
		||||
        
 | 
			
		||||
        let user_assets: Vec<&Asset> = assets.iter()
 | 
			
		||||
            .filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        context.insert("active_page", &"marketplace");
 | 
			
		||||
        context.insert("assets", &user_assets);
 | 
			
		||||
        context.insert(
 | 
			
		||||
            "listing_types",
 | 
			
		||||
            &[
 | 
			
		||||
                ListingType::FixedPrice.as_str(),
 | 
			
		||||
                ListingType::Auction.as_str(),
 | 
			
		||||
                ListingType::Exchange.as_str(),
 | 
			
		||||
            ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        context.insert("listing_types", &[
 | 
			
		||||
            ListingType::FixedPrice.as_str(),
 | 
			
		||||
            ListingType::Auction.as_str(),
 | 
			
		||||
            ListingType::Exchange.as_str(),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        render_template(&tmpl, "marketplace/create_listing.html", &context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Create a new listing
 | 
			
		||||
    pub async fn create_listing(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        form: web::Form<ListingForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get the asset details
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let asset = assets.iter().find(|a| a.id == form.asset_id);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if let Some(asset) = asset {
 | 
			
		||||
            // Process tags
 | 
			
		||||
            let tags = match form.tags {
 | 
			
		||||
                Some(tags_str) => tags_str
 | 
			
		||||
                    .split(',')
 | 
			
		||||
                Some(tags_str) => tags_str.split(',')
 | 
			
		||||
                    .map(|s| s.trim().to_string())
 | 
			
		||||
                    .filter(|s| !s.is_empty())
 | 
			
		||||
                    .collect(),
 | 
			
		||||
                None => Vec::new(),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Calculate expiration date if provided
 | 
			
		||||
            let expires_at = form
 | 
			
		||||
                .duration_days
 | 
			
		||||
                .map(|days| Utc::now() + Duration::days(days as i64));
 | 
			
		||||
 | 
			
		||||
            let expires_at = form.duration_days.map(|days| {
 | 
			
		||||
                Utc::now() + Duration::days(days as i64)
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Parse listing type
 | 
			
		||||
            let listing_type = match form.listing_type.as_str() {
 | 
			
		||||
                "Fixed Price" => ListingType::FixedPrice,
 | 
			
		||||
@@ -255,11 +234,11 @@ impl MarketplaceController {
 | 
			
		||||
                "Exchange" => ListingType::Exchange,
 | 
			
		||||
                _ => ListingType::FixedPrice,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Mock user data
 | 
			
		||||
            let user_id = "user-123";
 | 
			
		||||
            let user_name = "Alice Hostly";
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Create the listing
 | 
			
		||||
            let _listing = Listing::new(
 | 
			
		||||
                form.title,
 | 
			
		||||
@@ -276,9 +255,9 @@ impl MarketplaceController {
 | 
			
		||||
                tags,
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // In a real application, we would save the listing to a database here
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Redirect to the marketplace
 | 
			
		||||
            Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .insert_header((http::header::LOCATION, "/marketplace"))
 | 
			
		||||
@@ -288,101 +267,94 @@ impl MarketplaceController {
 | 
			
		||||
            let mut context = Context::new();
 | 
			
		||||
            context.insert("active_page", &"marketplace");
 | 
			
		||||
            context.insert("error", &"Asset not found");
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            render_template(&tmpl, "marketplace/create_listing.html", &context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Submit a bid on an auction listing
 | 
			
		||||
    #[allow(dead_code)]
 | 
			
		||||
    pub async fn submit_bid(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        _form: web::Form<BidForm>,
 | 
			
		||||
        form: web::Form<BidForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let _form = _form.into_inner();
 | 
			
		||||
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate the bid
 | 
			
		||||
        // 3. Create the bid
 | 
			
		||||
        // 4. Save it to the database
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect back to the listing
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((
 | 
			
		||||
                http::header::LOCATION,
 | 
			
		||||
                format!("/marketplace/{}", listing_id),
 | 
			
		||||
            ))
 | 
			
		||||
            .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Purchase a fixed-price listing
 | 
			
		||||
    pub async fn purchase_listing(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        form: web::Form<PurchaseForm>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let listing_id = path.into_inner();
 | 
			
		||||
        let form = form.into_inner();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        if !form.agree_to_terms {
 | 
			
		||||
            // User must agree to terms
 | 
			
		||||
            return Ok(HttpResponse::SeeOther()
 | 
			
		||||
                .insert_header((
 | 
			
		||||
                    http::header::LOCATION,
 | 
			
		||||
                    format!("/marketplace/{}", listing_id),
 | 
			
		||||
                ))
 | 
			
		||||
                .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate the purchase
 | 
			
		||||
        // 3. Process the transaction
 | 
			
		||||
        // 4. Update the listing status
 | 
			
		||||
        // 5. Transfer the asset
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect to the marketplace
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((http::header::LOCATION, "/marketplace"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Cancel a listing
 | 
			
		||||
    pub async fn cancel_listing(
 | 
			
		||||
        _tmpl: web::Data<Tera>,
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
    ) -> Result<HttpResponse> {
 | 
			
		||||
        let _listing_id = path.into_inner();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // In a real application, we would:
 | 
			
		||||
        // 1. Find the listing in the database
 | 
			
		||||
        // 2. Validate that the current user is the seller
 | 
			
		||||
        // 3. Update the listing status
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // For now, we'll just redirect to my listings
 | 
			
		||||
        Ok(HttpResponse::SeeOther()
 | 
			
		||||
            .insert_header((http::header::LOCATION, "/marketplace/my"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Generate mock listings for development
 | 
			
		||||
    pub fn get_mock_listings() -> Vec<Listing> {
 | 
			
		||||
        let assets = AssetController::get_mock_assets();
 | 
			
		||||
        let mut listings = Vec::new();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Mock user data
 | 
			
		||||
        let user_ids = vec!["user-123", "user-456", "user-789"];
 | 
			
		||||
        let user_names = vec!["Alice Hostly", "Ethan Cloudman", "Priya Servera"];
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Create some fixed price listings
 | 
			
		||||
        for i in 0..6 {
 | 
			
		||||
            let asset_index = i % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 50.0 + (i as f64 * 10.0),
 | 
			
		||||
                AssetType::Artwork => 500.0 + (i as f64 * 100.0),
 | 
			
		||||
@@ -393,13 +365,10 @@ impl MarketplaceController {
 | 
			
		||||
                AssetType::Bond => 1500.0 + (i as f64 * 300.0),
 | 
			
		||||
                AssetType::Other => 800.0 + (i as f64 * 150.0),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} for Sale", asset.name),
 | 
			
		||||
                format!(
 | 
			
		||||
                    "This is a great opportunity to own {}. {}",
 | 
			
		||||
                    asset.name, asset.description
 | 
			
		||||
                ),
 | 
			
		||||
                format!("This is a great opportunity to own {}. {}", asset.name, asset.description),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
@@ -412,21 +381,21 @@ impl MarketplaceController {
 | 
			
		||||
                vec!["digital".to_string(), "asset".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Make some listings featured
 | 
			
		||||
            if i % 5 == 0 {
 | 
			
		||||
                listing.set_featured(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Create some auction listings
 | 
			
		||||
        for i in 0..4 {
 | 
			
		||||
            let asset_index = (i + 6) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let starting_price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 40.0 + (i as f64 * 5.0),
 | 
			
		||||
                AssetType::Artwork => 400.0 + (i as f64 * 50.0),
 | 
			
		||||
@@ -437,7 +406,7 @@ impl MarketplaceController {
 | 
			
		||||
                AssetType::Bond => 1200.0 + (i as f64 * 250.0),
 | 
			
		||||
                AssetType::Other => 600.0 + (i as f64 * 120.0),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("Auction: {}", asset.name),
 | 
			
		||||
                format!("Bid on this amazing {}. {}", asset.name, asset.description),
 | 
			
		||||
@@ -453,13 +422,12 @@ impl MarketplaceController {
 | 
			
		||||
                vec!["auction".to_string(), "bidding".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Add some bids to the auctions
 | 
			
		||||
            let num_bids = 2 + (i % 3);
 | 
			
		||||
            for j in 0..num_bids {
 | 
			
		||||
                let bidder_index = (j + 1) % user_ids.len();
 | 
			
		||||
                if bidder_index != user_index {
 | 
			
		||||
                    // Ensure seller isn't bidding
 | 
			
		||||
                if bidder_index != user_index {  // Ensure seller isn't bidding
 | 
			
		||||
                    let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
 | 
			
		||||
                    let _ = listing.add_bid(
 | 
			
		||||
                        user_ids[bidder_index].to_string(),
 | 
			
		||||
@@ -469,21 +437,21 @@ impl MarketplaceController {
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Make some listings featured
 | 
			
		||||
            if i % 3 == 0 {
 | 
			
		||||
                listing.set_featured(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Create some exchange listings
 | 
			
		||||
        for i in 0..3 {
 | 
			
		||||
            let asset_index = (i + 10) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let value = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 60.0 + (i as f64 * 15.0),
 | 
			
		||||
                AssetType::Artwork => 600.0 + (i as f64 * 150.0),
 | 
			
		||||
@@ -494,36 +462,33 @@ impl MarketplaceController {
 | 
			
		||||
                AssetType::Bond => 1800.0 + (i as f64 * 350.0),
 | 
			
		||||
                AssetType::Other => 1000.0 + (i as f64 * 200.0),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let listing = Listing::new(
 | 
			
		||||
                format!("Trade: {}", asset.name),
 | 
			
		||||
                format!(
 | 
			
		||||
                    "Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.",
 | 
			
		||||
                    asset.name
 | 
			
		||||
                ),
 | 
			
		||||
                format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name),
 | 
			
		||||
                asset.id.clone(),
 | 
			
		||||
                asset.name.clone(),
 | 
			
		||||
                asset.asset_type.clone(),
 | 
			
		||||
                user_ids[user_index].to_string(),
 | 
			
		||||
                user_names[user_index].to_string(),
 | 
			
		||||
                value, // Estimated value for exchange
 | 
			
		||||
                value,  // Estimated value for exchange
 | 
			
		||||
                "USD".to_string(),
 | 
			
		||||
                ListingType::Exchange,
 | 
			
		||||
                Some(Utc::now() + Duration::days(60)),
 | 
			
		||||
                vec!["exchange".to_string(), "trade".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Create some sold listings
 | 
			
		||||
        for i in 0..5 {
 | 
			
		||||
            let asset_index = (i + 13) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let seller_index = i % user_ids.len();
 | 
			
		||||
            let buyer_index = (i + 1) % user_ids.len();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 55.0 + (i as f64 * 12.0),
 | 
			
		||||
                AssetType::Artwork => 550.0 + (i as f64 * 120.0),
 | 
			
		||||
@@ -534,9 +499,9 @@ impl MarketplaceController {
 | 
			
		||||
                AssetType::Bond => 1650.0 + (i as f64 * 330.0),
 | 
			
		||||
                AssetType::Other => 900.0 + (i as f64 * 180.0),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let sale_price = price * 0.95; // Slight discount on sale
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let sale_price = price * 0.95;  // Slight discount on sale
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} - SOLD", asset.name),
 | 
			
		||||
                format!("This {} was sold recently.", asset.name),
 | 
			
		||||
@@ -552,27 +517,27 @@ impl MarketplaceController {
 | 
			
		||||
                vec!["sold".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Mark as sold
 | 
			
		||||
            let _ = listing.mark_as_sold(
 | 
			
		||||
                user_ids[buyer_index].to_string(),
 | 
			
		||||
                user_names[buyer_index].to_string(),
 | 
			
		||||
                sale_price,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Set sold date to be sometime in the past
 | 
			
		||||
            let days_ago = i as i64 + 1;
 | 
			
		||||
            listing.sold_at = Some(Utc::now() - Duration::days(days_ago));
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Create a few cancelled listings
 | 
			
		||||
        for i in 0..2 {
 | 
			
		||||
            let asset_index = (i + 18) % assets.len();
 | 
			
		||||
            let asset = &assets[asset_index];
 | 
			
		||||
            let user_index = i % user_ids.len();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let price = match asset.asset_type {
 | 
			
		||||
                AssetType::Token => 45.0 + (i as f64 * 8.0),
 | 
			
		||||
                AssetType::Artwork => 450.0 + (i as f64 * 80.0),
 | 
			
		||||
@@ -583,7 +548,7 @@ impl MarketplaceController {
 | 
			
		||||
                AssetType::Bond => 1350.0 + (i as f64 * 270.0),
 | 
			
		||||
                AssetType::Other => 750.0 + (i as f64 * 150.0),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            let mut listing = Listing::new(
 | 
			
		||||
                format!("{} - Cancelled", asset.name),
 | 
			
		||||
                format!("This listing for {} was cancelled.", asset.name),
 | 
			
		||||
@@ -599,13 +564,13 @@ impl MarketplaceController {
 | 
			
		||||
                vec!["cancelled".to_string()],
 | 
			
		||||
                asset.image_url.clone(),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Cancel the listing
 | 
			
		||||
            let _ = listing.cancel();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            listings.push(listing);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        listings
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,14 @@
 | 
			
		||||
// Export controllers
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod auth;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
pub mod company;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod defi;
 | 
			
		||||
pub mod document;
 | 
			
		||||
pub mod error;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod governance;
 | 
			
		||||
pub mod health;
 | 
			
		||||
pub mod home;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
pub mod payment;
 | 
			
		||||
pub mod auth;
 | 
			
		||||
pub mod ticket;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
pub mod governance;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod defi;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
pub mod company;
 | 
			
		||||
 | 
			
		||||
// Re-export controllers for easier imports
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,362 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::calendar::{AttendanceStatus, Attendee, Calendar, Event},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
 | 
			
		||||
/// Creates a new calendar and saves it to the database. Returns the saved calendar and its ID.
 | 
			
		||||
pub fn create_new_calendar(
 | 
			
		||||
    name: &str,
 | 
			
		||||
    description: Option<&str>,
 | 
			
		||||
    owner_id: Option<u32>,
 | 
			
		||||
    is_public: bool,
 | 
			
		||||
    color: Option<&str>,
 | 
			
		||||
) -> Result<(u32, Calendar), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create a new calendar (with auto-generated ID)
 | 
			
		||||
    let mut calendar = Calendar::new(None, name);
 | 
			
		||||
 | 
			
		||||
    if let Some(desc) = description {
 | 
			
		||||
        calendar = calendar.description(desc);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(owner) = owner_id {
 | 
			
		||||
        calendar = calendar.owner_id(owner);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(col) = color {
 | 
			
		||||
        calendar = calendar.color(col);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    calendar = calendar.is_public(is_public);
 | 
			
		||||
 | 
			
		||||
    // Save the calendar to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .expect("can open calendar collection");
 | 
			
		||||
    let (calendar_id, saved_calendar) = collection.set(&calendar).expect("can save calendar");
 | 
			
		||||
 | 
			
		||||
    Ok((calendar_id, saved_calendar))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new event and saves it to the database. Returns the saved event and its ID.
 | 
			
		||||
pub fn create_new_event(
 | 
			
		||||
    title: &str,
 | 
			
		||||
    description: Option<&str>,
 | 
			
		||||
    start_time: DateTime<Utc>,
 | 
			
		||||
    end_time: DateTime<Utc>,
 | 
			
		||||
    location: Option<&str>,
 | 
			
		||||
    color: Option<&str>,
 | 
			
		||||
    all_day: bool,
 | 
			
		||||
    created_by: Option<u32>,
 | 
			
		||||
    category: Option<&str>,
 | 
			
		||||
    reminder_minutes: Option<i32>,
 | 
			
		||||
) -> Result<(u32, Event), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create a new event (with auto-generated ID)
 | 
			
		||||
    let mut event = Event::new(title, start_time, end_time);
 | 
			
		||||
 | 
			
		||||
    if let Some(desc) = description {
 | 
			
		||||
        event = event.description(desc);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(loc) = location {
 | 
			
		||||
        event = event.location(loc);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(col) = color {
 | 
			
		||||
        event = event.color(col);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(user_id) = created_by {
 | 
			
		||||
        event = event.created_by(user_id);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(cat) = category {
 | 
			
		||||
        event = event.category(cat);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(reminder) = reminder_minutes {
 | 
			
		||||
        event = event.reminder_minutes(reminder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event = event.all_day(all_day);
 | 
			
		||||
 | 
			
		||||
    // Save the event to the database
 | 
			
		||||
    let collection = db.collection::<Event>().expect("can open event collection");
 | 
			
		||||
    let (event_id, saved_event) = collection.set(&event).expect("can save event");
 | 
			
		||||
 | 
			
		||||
    Ok((event_id, saved_event))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all calendars from the database and returns them as a Vec<Calendar>.
 | 
			
		||||
pub fn get_calendars() -> Result<Vec<Calendar>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .expect("can open calendar collection");
 | 
			
		||||
 | 
			
		||||
    // Try to load all calendars, but handle deserialization errors gracefully
 | 
			
		||||
    let calendars = match collection.get_all() {
 | 
			
		||||
        Ok(calendars) => calendars,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error loading calendars: {:?}", e);
 | 
			
		||||
            vec![] // Return an empty vector if there's an error
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(calendars)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all events from the database and returns them as a Vec<Event>.
 | 
			
		||||
pub fn get_events() -> Result<Vec<Event>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db.collection::<Event>().expect("can open event collection");
 | 
			
		||||
 | 
			
		||||
    // Try to load all events, but handle deserialization errors gracefully
 | 
			
		||||
    let events = match collection.get_all() {
 | 
			
		||||
        Ok(events) => events,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error loading events: {:?}", e);
 | 
			
		||||
            vec![] // Return an empty vector if there's an error
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(events)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single calendar by its ID from the database.
 | 
			
		||||
pub fn get_calendar_by_id(calendar_id: u32) -> Result<Option<Calendar>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(calendar_id) {
 | 
			
		||||
        Ok(calendar) => Ok(calendar),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching calendar by id {}: {:?}", calendar_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch calendar: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single event by its ID from the database.
 | 
			
		||||
pub fn get_event_by_id(event_id: u32) -> Result<Option<Event>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Event>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(event_id) {
 | 
			
		||||
        Ok(event) => Ok(event),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching event by id {}: {:?}", event_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch event: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new attendee and saves it to the database. Returns the saved attendee and its ID.
 | 
			
		||||
pub fn create_new_attendee(
 | 
			
		||||
    contact_id: u32,
 | 
			
		||||
    status: AttendanceStatus,
 | 
			
		||||
) -> Result<(u32, Attendee), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create a new attendee (with auto-generated ID)
 | 
			
		||||
    let attendee = Attendee::new(contact_id).status(status);
 | 
			
		||||
 | 
			
		||||
    // Save the attendee to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Attendee>()
 | 
			
		||||
        .expect("can open attendee collection");
 | 
			
		||||
    let (attendee_id, saved_attendee) = collection.set(&attendee).expect("can save attendee");
 | 
			
		||||
 | 
			
		||||
    Ok((attendee_id, saved_attendee))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single attendee by its ID from the database.
 | 
			
		||||
pub fn get_attendee_by_id(attendee_id: u32) -> Result<Option<Attendee>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Attendee>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(attendee_id) {
 | 
			
		||||
        Ok(attendee) => Ok(attendee),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching attendee by id {}: {:?}", attendee_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch attendee: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates attendee status in the database and returns the updated attendee.
 | 
			
		||||
pub fn update_attendee_status(
 | 
			
		||||
    attendee_id: u32,
 | 
			
		||||
    status: AttendanceStatus,
 | 
			
		||||
) -> Result<Attendee, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Attendee>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut attendee) = collection
 | 
			
		||||
        .get_by_id(attendee_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch attendee: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        attendee = attendee.status(status);
 | 
			
		||||
        let (_, updated_attendee) = collection
 | 
			
		||||
            .set(&attendee)
 | 
			
		||||
            .map_err(|e| format!("Failed to update attendee: {:?}", e))?;
 | 
			
		||||
        Ok(updated_attendee)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Attendee not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Add attendee to event
 | 
			
		||||
pub fn add_attendee_to_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Event>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut event) = collection
 | 
			
		||||
        .get_by_id(event_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch event: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        event = event.add_attendee(attendee_id);
 | 
			
		||||
        let (_, updated_event) = collection
 | 
			
		||||
            .set(&event)
 | 
			
		||||
            .map_err(|e| format!("Failed to update event: {:?}", e))?;
 | 
			
		||||
        Ok(updated_event)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Event not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Remove attendee from event
 | 
			
		||||
pub fn remove_attendee_from_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Event>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut event) = collection
 | 
			
		||||
        .get_by_id(event_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch event: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        event = event.remove_attendee(attendee_id);
 | 
			
		||||
        let (_, updated_event) = collection
 | 
			
		||||
            .set(&event)
 | 
			
		||||
            .map_err(|e| format!("Failed to update event: {:?}", e))?;
 | 
			
		||||
        Ok(updated_event)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Event not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Add event to calendar
 | 
			
		||||
pub fn add_event_to_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut calendar) = collection
 | 
			
		||||
        .get_by_id(calendar_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        calendar = calendar.add_event(event_id as i64);
 | 
			
		||||
        let (_, updated_calendar) = collection
 | 
			
		||||
            .set(&calendar)
 | 
			
		||||
            .map_err(|e| format!("Failed to update calendar: {:?}", e))?;
 | 
			
		||||
        Ok(updated_calendar)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Calendar not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Remove event from calendar
 | 
			
		||||
pub fn remove_event_from_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut calendar) = collection
 | 
			
		||||
        .get_by_id(calendar_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        calendar = calendar.remove_event(event_id as i64);
 | 
			
		||||
        let (_, updated_calendar) = collection
 | 
			
		||||
            .set(&calendar)
 | 
			
		||||
            .map_err(|e| format!("Failed to update calendar: {:?}", e))?;
 | 
			
		||||
        Ok(updated_calendar)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Calendar not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes a calendar from the database.
 | 
			
		||||
pub fn delete_calendar(calendar_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    collection
 | 
			
		||||
        .delete_by_id(calendar_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to delete calendar: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes an event from the database.
 | 
			
		||||
pub fn delete_event(event_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Event>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    collection
 | 
			
		||||
        .delete_by_id(event_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to delete event: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets or creates a calendar for a user. If the user already has a calendar, returns it.
 | 
			
		||||
/// If not, creates a new calendar for the user and returns it.
 | 
			
		||||
pub fn get_or_create_user_calendar(user_id: u32, user_name: &str) -> Result<Calendar, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Calendar>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Try to find existing calendar for this user
 | 
			
		||||
    let calendars = match collection.get_all() {
 | 
			
		||||
        Ok(calendars) => calendars,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error loading calendars: {:?}", e);
 | 
			
		||||
            vec![] // Return an empty vector if there's an error
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Look for a calendar owned by this user
 | 
			
		||||
    for calendar in calendars {
 | 
			
		||||
        if let Some(owner_id) = calendar.owner_id {
 | 
			
		||||
            if owner_id == user_id {
 | 
			
		||||
                return Ok(calendar);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // No calendar found for this user, create a new one
 | 
			
		||||
    let calendar_name = format!("{}'s Calendar", user_name);
 | 
			
		||||
    let (_, new_calendar) = create_new_calendar(
 | 
			
		||||
        &calendar_name,
 | 
			
		||||
        Some("Personal calendar"),
 | 
			
		||||
        Some(user_id),
 | 
			
		||||
        false,           // Private calendar
 | 
			
		||||
        Some("#4285F4"), // Default blue color
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    Ok(new_calendar)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,500 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::biz::{BusinessType, Company, CompanyStatus, Shareholder, ShareholderType},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Creates a new company and saves it to the database
 | 
			
		||||
pub fn create_new_company(
 | 
			
		||||
    name: String,
 | 
			
		||||
    registration_number: String,
 | 
			
		||||
    incorporation_date: i64,
 | 
			
		||||
    business_type: BusinessType,
 | 
			
		||||
    email: String,
 | 
			
		||||
    phone: String,
 | 
			
		||||
    website: String,
 | 
			
		||||
    address: String,
 | 
			
		||||
    industry: String,
 | 
			
		||||
    description: String,
 | 
			
		||||
    fiscal_year_end: String,
 | 
			
		||||
) -> Result<(u32, Company), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create using heromodels constructor
 | 
			
		||||
    let company = Company::new(name, registration_number, incorporation_date)
 | 
			
		||||
        .business_type(business_type)
 | 
			
		||||
        .email(email)
 | 
			
		||||
        .phone(phone)
 | 
			
		||||
        .website(website)
 | 
			
		||||
        .address(address)
 | 
			
		||||
        .industry(industry)
 | 
			
		||||
        .description(description)
 | 
			
		||||
        .fiscal_year_end(fiscal_year_end)
 | 
			
		||||
        .status(CompanyStatus::PendingPayment);
 | 
			
		||||
 | 
			
		||||
    // Save to database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .expect("can open company collection");
 | 
			
		||||
    let (id, saved_company) = collection.set(&company).expect("can save company");
 | 
			
		||||
 | 
			
		||||
    Ok((id, saved_company))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new company with a specific status and saves it to the database
 | 
			
		||||
pub fn create_new_company_with_status(
 | 
			
		||||
    name: String,
 | 
			
		||||
    registration_number: String,
 | 
			
		||||
    incorporation_date: i64,
 | 
			
		||||
    business_type: BusinessType,
 | 
			
		||||
    email: String,
 | 
			
		||||
    phone: String,
 | 
			
		||||
    website: String,
 | 
			
		||||
    address: String,
 | 
			
		||||
    industry: String,
 | 
			
		||||
    description: String,
 | 
			
		||||
    fiscal_year_end: String,
 | 
			
		||||
    status: CompanyStatus,
 | 
			
		||||
) -> Result<(u32, Company), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create using heromodels constructor with specified status
 | 
			
		||||
    let company = Company::new(name, registration_number, incorporation_date)
 | 
			
		||||
        .business_type(business_type)
 | 
			
		||||
        .email(email)
 | 
			
		||||
        .phone(phone)
 | 
			
		||||
        .website(website)
 | 
			
		||||
        .address(address)
 | 
			
		||||
        .industry(industry)
 | 
			
		||||
        .description(description)
 | 
			
		||||
        .fiscal_year_end(fiscal_year_end)
 | 
			
		||||
        .status(status);
 | 
			
		||||
 | 
			
		||||
    // Save to database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .expect("can open company collection");
 | 
			
		||||
    let (id, saved_company) = collection.set(&company).expect("can save company");
 | 
			
		||||
 | 
			
		||||
    Ok((id, saved_company))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all companies from the database
 | 
			
		||||
pub fn get_companies() -> Result<Vec<Company>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .expect("can open company collection");
 | 
			
		||||
 | 
			
		||||
    let companies = match collection.get_all() {
 | 
			
		||||
        Ok(companies) => {
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Successfully loaded {} companies from database",
 | 
			
		||||
                companies.len()
 | 
			
		||||
            );
 | 
			
		||||
            companies
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to load companies from database: {:?}", e);
 | 
			
		||||
            // Return the error instead of empty vec to properly handle corruption
 | 
			
		||||
            return Err(format!("Failed to get companies: {:?}", e));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(companies)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Update company status (e.g., from PendingPayment to Active)
 | 
			
		||||
pub fn update_company_status(
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    new_status: CompanyStatus,
 | 
			
		||||
) -> Result<Option<Company>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .expect("can open company collection");
 | 
			
		||||
 | 
			
		||||
    // Try to get all companies, with corruption recovery
 | 
			
		||||
    let all_companies = match collection.get_all() {
 | 
			
		||||
        Ok(companies) => companies,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to get companies for status update: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            // If we have a decode error, try to recover by clearing corrupted data
 | 
			
		||||
            if format!("{:?}", e).contains("Decode") {
 | 
			
		||||
                log::warn!("Database corruption detected, attempting recovery...");
 | 
			
		||||
 | 
			
		||||
                // Try to recover by clearing the collection and recreating
 | 
			
		||||
                match recover_from_database_corruption() {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        log::info!(
 | 
			
		||||
                            "Database recovery successful, but company {} may be lost",
 | 
			
		||||
                            company_id
 | 
			
		||||
                        );
 | 
			
		||||
                        return Err(format!(
 | 
			
		||||
                            "Database was corrupted and recovered, but company {} was not found. Please re-register.",
 | 
			
		||||
                            company_id
 | 
			
		||||
                        ));
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(recovery_err) => {
 | 
			
		||||
                        log::error!("Database recovery failed: {}", recovery_err);
 | 
			
		||||
                        return Err(format!(
 | 
			
		||||
                            "Database corruption detected and recovery failed: {}",
 | 
			
		||||
                            recovery_err
 | 
			
		||||
                        ));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Err(format!("Failed to get companies: {:?}", e));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Find the company by ID
 | 
			
		||||
    for (_index, company) in all_companies.iter().enumerate() {
 | 
			
		||||
        if company.base_data.id == company_id {
 | 
			
		||||
            // Create updated company with new status
 | 
			
		||||
            let mut updated_company = company.clone();
 | 
			
		||||
            updated_company.status = new_status.clone();
 | 
			
		||||
 | 
			
		||||
            // Update in database
 | 
			
		||||
            let (_, saved_company) = collection.set(&updated_company).map_err(|e| {
 | 
			
		||||
                log::error!("Failed to update company status: {:?}", e);
 | 
			
		||||
                format!("Failed to update company: {:?}", e)
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
            log::info!("Updated company {} status to {:?}", company_id, new_status);
 | 
			
		||||
 | 
			
		||||
            return Ok(Some(saved_company));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    log::warn!(
 | 
			
		||||
        "Company not found with ID: {} (cannot update status)",
 | 
			
		||||
        company_id
 | 
			
		||||
    );
 | 
			
		||||
    Ok(None)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single company by its ID
 | 
			
		||||
pub fn get_company_by_id(company_id: u32) -> Result<Option<Company>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(company_id) {
 | 
			
		||||
        Ok(company) => Ok(company),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching company by id {}: {:?}", company_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch company: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates company in the database
 | 
			
		||||
pub fn update_company(
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    email: Option<String>,
 | 
			
		||||
    phone: Option<String>,
 | 
			
		||||
    website: Option<String>,
 | 
			
		||||
    address: Option<String>,
 | 
			
		||||
    industry: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    fiscal_year_end: Option<String>,
 | 
			
		||||
    status: Option<CompanyStatus>,
 | 
			
		||||
    business_type: Option<BusinessType>,
 | 
			
		||||
) -> Result<Company, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut company) = collection
 | 
			
		||||
        .get_by_id(company_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch company: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        // Update using builder pattern
 | 
			
		||||
        if let Some(name) = name {
 | 
			
		||||
            company.name = name;
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(email) = email {
 | 
			
		||||
            company = company.email(email);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(phone) = phone {
 | 
			
		||||
            company = company.phone(phone);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(website) = website {
 | 
			
		||||
            company = company.website(website);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(address) = address {
 | 
			
		||||
            company = company.address(address);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(industry) = industry {
 | 
			
		||||
            company = company.industry(industry);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(description) = description {
 | 
			
		||||
            company = company.description(description);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(fiscal_year_end) = fiscal_year_end {
 | 
			
		||||
            company = company.fiscal_year_end(fiscal_year_end);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(status) = status {
 | 
			
		||||
            company = company.status(status);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(business_type) = business_type {
 | 
			
		||||
            company = company.business_type(business_type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (_, updated_company) = collection
 | 
			
		||||
            .set(&company)
 | 
			
		||||
            .map_err(|e| format!("Failed to update company: {:?}", e))?;
 | 
			
		||||
        Ok(updated_company)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Company not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes company from the database
 | 
			
		||||
pub fn delete_company(company_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    collection
 | 
			
		||||
        .delete_by_id(company_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to delete company: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes a company by name (useful for cleaning up test data)
 | 
			
		||||
pub fn delete_company_by_name(company_name: &str) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Get all companies and find the one with matching name
 | 
			
		||||
    let companies = collection
 | 
			
		||||
        .get_all()
 | 
			
		||||
        .map_err(|e| format!("Failed to get companies: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    let company_to_delete = companies
 | 
			
		||||
        .iter()
 | 
			
		||||
        .find(|c| c.name.trim().to_lowercase() == company_name.trim().to_lowercase());
 | 
			
		||||
 | 
			
		||||
    if let Some(company) = company_to_delete {
 | 
			
		||||
        collection
 | 
			
		||||
            .delete_by_id(company.base_data.id)
 | 
			
		||||
            .map_err(|e| format!("Failed to delete company: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Successfully deleted company '{}' with ID {}",
 | 
			
		||||
            company.name,
 | 
			
		||||
            company.base_data.id
 | 
			
		||||
        );
 | 
			
		||||
        Ok(())
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(format!("Company '{}' not found", company_name))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Lists all company names in the database (useful for debugging duplicates)
 | 
			
		||||
pub fn list_company_names() -> Result<Vec<String>, String> {
 | 
			
		||||
    let companies = get_companies()?;
 | 
			
		||||
    let names: Vec<String> = companies.iter().map(|c| c.name.clone()).collect();
 | 
			
		||||
    Ok(names)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Recover from database corruption by clearing corrupted data
 | 
			
		||||
fn recover_from_database_corruption() -> Result<(), String> {
 | 
			
		||||
    log::warn!("Attempting to recover from database corruption...");
 | 
			
		||||
 | 
			
		||||
    // Since there's no clear method available, we'll provide instructions for manual recovery
 | 
			
		||||
    log::warn!("Database corruption detected - manual intervention required");
 | 
			
		||||
    log::warn!("To fix: Stop the application, delete the database files, and restart");
 | 
			
		||||
 | 
			
		||||
    Err(
 | 
			
		||||
        "Database corruption detected. Please restart the application to reset the database."
 | 
			
		||||
            .to_string(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Manual function to clean up corrupted database (for emergency use)
 | 
			
		||||
pub fn cleanup_corrupted_database() -> Result<String, String> {
 | 
			
		||||
    log::warn!("Manual database cleanup initiated...");
 | 
			
		||||
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Company>()
 | 
			
		||||
        .expect("can open company collection");
 | 
			
		||||
 | 
			
		||||
    // Try to get companies to check for corruption
 | 
			
		||||
    match collection.get_all() {
 | 
			
		||||
        Ok(companies) => {
 | 
			
		||||
            log::info!("Database is healthy with {} companies", companies.len());
 | 
			
		||||
            Ok(format!(
 | 
			
		||||
                "Database is healthy with {} companies",
 | 
			
		||||
                companies.len()
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Database corruption detected: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            // Since we can't clear the collection programmatically, provide instructions
 | 
			
		||||
            log::error!("Database corruption detected but cannot be fixed automatically");
 | 
			
		||||
            Err("Database corruption detected. Please stop the application, delete the database files in the 'data' directory, and restart the application.".to_string())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// === Shareholder Management Functions ===
 | 
			
		||||
 | 
			
		||||
/// Creates a new shareholder and saves it to the database
 | 
			
		||||
pub fn create_new_shareholder(
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    user_id: u32,
 | 
			
		||||
    name: String,
 | 
			
		||||
    shares: f64,
 | 
			
		||||
    percentage: f64,
 | 
			
		||||
    shareholder_type: ShareholderType,
 | 
			
		||||
    since: i64,
 | 
			
		||||
) -> Result<(u32, Shareholder), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create a new shareholder
 | 
			
		||||
    let shareholder = Shareholder::new()
 | 
			
		||||
        .company_id(company_id)
 | 
			
		||||
        .user_id(user_id)
 | 
			
		||||
        .name(name)
 | 
			
		||||
        .shares(shares)
 | 
			
		||||
        .percentage(percentage)
 | 
			
		||||
        .type_(shareholder_type)
 | 
			
		||||
        .since(since);
 | 
			
		||||
 | 
			
		||||
    // Save the shareholder to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .expect("can open shareholder collection");
 | 
			
		||||
    let (shareholder_id, saved_shareholder) =
 | 
			
		||||
        collection.set(&shareholder).expect("can save shareholder");
 | 
			
		||||
 | 
			
		||||
    Ok((shareholder_id, saved_shareholder))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets all shareholders for a specific company
 | 
			
		||||
pub fn get_company_shareholders(company_id: u32) -> Result<Vec<Shareholder>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .expect("can open shareholder collection");
 | 
			
		||||
 | 
			
		||||
    let all_shareholders = match collection.get_all() {
 | 
			
		||||
        Ok(shareholders) => shareholders,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to load shareholders from database: {:?}", e);
 | 
			
		||||
            vec![]
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Filter shareholders by company_id
 | 
			
		||||
    let company_shareholders = all_shareholders
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|shareholder| shareholder.company_id == company_id)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(company_shareholders)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets all shareholders from the database
 | 
			
		||||
pub fn get_all_shareholders() -> Result<Vec<Shareholder>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .expect("can open shareholder collection");
 | 
			
		||||
 | 
			
		||||
    let shareholders = match collection.get_all() {
 | 
			
		||||
        Ok(shareholders) => shareholders,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to load shareholders from database: {:?}", e);
 | 
			
		||||
            vec![]
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(shareholders)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single shareholder by its ID
 | 
			
		||||
pub fn get_shareholder_by_id(shareholder_id: u32) -> Result<Option<Shareholder>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(shareholder_id) {
 | 
			
		||||
        Ok(shareholder) => Ok(shareholder),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!(
 | 
			
		||||
                "Error fetching shareholder by id {}: {:?}",
 | 
			
		||||
                shareholder_id,
 | 
			
		||||
                e
 | 
			
		||||
            );
 | 
			
		||||
            Err(format!("Failed to fetch shareholder: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates shareholder in the database
 | 
			
		||||
pub fn update_shareholder(
 | 
			
		||||
    shareholder_id: u32,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    shares: Option<f64>,
 | 
			
		||||
    percentage: Option<f64>,
 | 
			
		||||
    shareholder_type: Option<ShareholderType>,
 | 
			
		||||
) -> Result<Shareholder, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut shareholder) = collection
 | 
			
		||||
        .get_by_id(shareholder_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch shareholder: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        // Update using builder pattern
 | 
			
		||||
        if let Some(name) = name {
 | 
			
		||||
            shareholder = shareholder.name(name);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(shares) = shares {
 | 
			
		||||
            shareholder = shareholder.shares(shares);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(percentage) = percentage {
 | 
			
		||||
            shareholder = shareholder.percentage(percentage);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(shareholder_type) = shareholder_type {
 | 
			
		||||
            shareholder = shareholder.type_(shareholder_type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (_, updated_shareholder) = collection
 | 
			
		||||
            .set(&shareholder)
 | 
			
		||||
            .map_err(|e| format!("Failed to update shareholder: {:?}", e))?;
 | 
			
		||||
        Ok(updated_shareholder)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Shareholder not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes shareholder from the database
 | 
			
		||||
pub fn delete_shareholder(shareholder_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Shareholder>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    collection
 | 
			
		||||
        .delete_by_id(shareholder_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to delete shareholder: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
@@ -1,460 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
 | 
			
		||||
/// Creates a new contract and saves it to the database. Returns the saved contract and its ID.
 | 
			
		||||
pub fn create_new_contract(
 | 
			
		||||
    base_id: u32,
 | 
			
		||||
    contract_id: &str,
 | 
			
		||||
    title: &str,
 | 
			
		||||
    description: &str,
 | 
			
		||||
    contract_type: &str,
 | 
			
		||||
    status: ContractStatus,
 | 
			
		||||
    created_by: &str,
 | 
			
		||||
    terms_and_conditions: Option<&str>,
 | 
			
		||||
    start_date: Option<u64>,
 | 
			
		||||
    end_date: Option<u64>,
 | 
			
		||||
    renewal_period_days: Option<u32>,
 | 
			
		||||
) -> Result<(u32, Contract), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create a new contract using the heromodels Contract::new constructor
 | 
			
		||||
    let mut contract = Contract::new(base_id, contract_id.to_string())
 | 
			
		||||
        .title(title)
 | 
			
		||||
        .description(description)
 | 
			
		||||
        .contract_type(contract_type.to_string())
 | 
			
		||||
        .status(status)
 | 
			
		||||
        .created_by(created_by.to_string());
 | 
			
		||||
 | 
			
		||||
    if let Some(terms) = terms_and_conditions {
 | 
			
		||||
        contract = contract.terms_and_conditions(terms);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(start) = start_date {
 | 
			
		||||
        contract = contract.start_date(start);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(end) = end_date {
 | 
			
		||||
        contract = contract.end_date(end);
 | 
			
		||||
    }
 | 
			
		||||
    if let Some(renewal) = renewal_period_days {
 | 
			
		||||
        contract = contract.renewal_period_days(renewal as i32);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save the contract to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .expect("can open contract collection");
 | 
			
		||||
    let (contract_id, saved_contract) = collection.set(&contract).expect("can save contract");
 | 
			
		||||
 | 
			
		||||
    Ok((contract_id, saved_contract))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all contracts from the database and returns them as a Vec<Contract>.
 | 
			
		||||
pub fn get_contracts() -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .expect("can open contract collection");
 | 
			
		||||
 | 
			
		||||
    // Try to load all contracts, but handle deserialization errors gracefully
 | 
			
		||||
    let contracts = match collection.get_all() {
 | 
			
		||||
        Ok(contracts) => contracts,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to load contracts from database: {:?}", e);
 | 
			
		||||
            vec![] // Return empty vector if there's an error
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single contract by its ID from the database.
 | 
			
		||||
pub fn get_contract_by_id(contract_id: u32) -> Result<Option<Contract>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(contract_id) {
 | 
			
		||||
        Ok(contract) => Ok(contract),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching contract by id {}: {:?}", contract_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch contract: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a contract's basic information in the database and returns the updated contract.
 | 
			
		||||
pub fn update_contract(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    title: &str,
 | 
			
		||||
    description: &str,
 | 
			
		||||
    contract_type: &str,
 | 
			
		||||
    terms_and_conditions: Option<&str>,
 | 
			
		||||
    start_date: Option<u64>,
 | 
			
		||||
    end_date: Option<u64>,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        // Update the contract fields
 | 
			
		||||
        contract = contract
 | 
			
		||||
            .title(title)
 | 
			
		||||
            .description(description)
 | 
			
		||||
            .contract_type(contract_type.to_string());
 | 
			
		||||
 | 
			
		||||
        if let Some(terms) = terms_and_conditions {
 | 
			
		||||
            contract = contract.terms_and_conditions(terms);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(start) = start_date {
 | 
			
		||||
            contract = contract.start_date(start);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(end) = end_date {
 | 
			
		||||
            contract = contract.end_date(end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a contract's status in the database and returns the updated contract.
 | 
			
		||||
pub fn update_contract_status(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    status: ContractStatus,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        contract = contract.status(status);
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds a signer to a contract and returns the updated contract.
 | 
			
		||||
pub fn add_signer_to_contract(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    signer_id: &str,
 | 
			
		||||
    name: &str,
 | 
			
		||||
    email: &str,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        let signer =
 | 
			
		||||
            ContractSigner::new(signer_id.to_string(), name.to_string(), email.to_string());
 | 
			
		||||
        contract = contract.add_signer(signer);
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Adds a revision to a contract and returns the updated contract.
 | 
			
		||||
pub fn add_revision_to_contract(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    version: u32,
 | 
			
		||||
    content: &str,
 | 
			
		||||
    created_by: &str,
 | 
			
		||||
    comments: Option<&str>,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        let revision = ContractRevision::new(
 | 
			
		||||
            version,
 | 
			
		||||
            content.to_string(),
 | 
			
		||||
            current_timestamp_secs(),
 | 
			
		||||
            created_by.to_string(),
 | 
			
		||||
        )
 | 
			
		||||
        .comments(comments.unwrap_or("").to_string());
 | 
			
		||||
 | 
			
		||||
        contract = contract.add_revision(revision);
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a signer's status for a contract and returns the updated contract.
 | 
			
		||||
pub fn update_signer_status(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    signer_id: &str,
 | 
			
		||||
    status: SignerStatus,
 | 
			
		||||
    comments: Option<&str>,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    update_signer_status_with_signature(contract_id, signer_id, status, comments, None)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a signer's status with signature data for a contract and returns the updated contract.
 | 
			
		||||
pub fn update_signer_status_with_signature(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    signer_id: &str,
 | 
			
		||||
    status: SignerStatus,
 | 
			
		||||
    comments: Option<&str>,
 | 
			
		||||
    signature_data: Option<&str>,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        // Find and update the signer
 | 
			
		||||
        let mut signer_found = false;
 | 
			
		||||
        for signer in &mut contract.signers {
 | 
			
		||||
            if signer.id == signer_id {
 | 
			
		||||
                signer.status = status.clone();
 | 
			
		||||
                if status == SignerStatus::Signed {
 | 
			
		||||
                    signer.signed_at = Some(current_timestamp_secs());
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(comment) = comments {
 | 
			
		||||
                    signer.comments = Some(comment.to_string());
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(sig_data) = signature_data {
 | 
			
		||||
                    signer.signature_data = Some(sig_data.to_string());
 | 
			
		||||
                }
 | 
			
		||||
                signer_found = true;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !signer_found {
 | 
			
		||||
            return Err(format!("Signer with ID {} not found", signer_id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes a contract from the database.
 | 
			
		||||
pub fn delete_contract(contract_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    collection
 | 
			
		||||
        .delete_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to delete contract: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets contracts by status
 | 
			
		||||
pub fn get_contracts_by_status(status: ContractStatus) -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let contracts = get_contracts()?;
 | 
			
		||||
    let filtered_contracts = contracts
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|contract| contract.status == status)
 | 
			
		||||
        .collect();
 | 
			
		||||
    Ok(filtered_contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets contracts by creator
 | 
			
		||||
pub fn get_contracts_by_creator(created_by: &str) -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let contracts = get_contracts()?;
 | 
			
		||||
    let filtered_contracts = contracts
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|contract| contract.created_by == created_by)
 | 
			
		||||
        .collect();
 | 
			
		||||
    Ok(filtered_contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets contracts that need renewal (approaching end date)
 | 
			
		||||
pub fn get_contracts_needing_renewal(days_ahead: u64) -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let contracts = get_contracts()?;
 | 
			
		||||
    let threshold_timestamp = current_timestamp_secs() + (days_ahead * 24 * 60 * 60);
 | 
			
		||||
 | 
			
		||||
    let filtered_contracts = contracts
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|contract| {
 | 
			
		||||
            if let Some(end_date) = contract.end_date {
 | 
			
		||||
                end_date <= threshold_timestamp && contract.status == ContractStatus::Active
 | 
			
		||||
            } else {
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
    Ok(filtered_contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets expired contracts
 | 
			
		||||
pub fn get_expired_contracts() -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let contracts = get_contracts()?;
 | 
			
		||||
    let current_time = current_timestamp_secs();
 | 
			
		||||
 | 
			
		||||
    let filtered_contracts = contracts
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|contract| {
 | 
			
		||||
            if let Some(end_date) = contract.end_date {
 | 
			
		||||
                end_date < current_time && contract.status != ContractStatus::Expired
 | 
			
		||||
            } else {
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
    Ok(filtered_contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates multiple contracts to expired status
 | 
			
		||||
pub fn mark_contracts_as_expired(contract_ids: Vec<u32>) -> Result<Vec<Contract>, String> {
 | 
			
		||||
    let mut updated_contracts = Vec::new();
 | 
			
		||||
 | 
			
		||||
    for contract_id in contract_ids {
 | 
			
		||||
        match update_contract_status(contract_id, ContractStatus::Expired) {
 | 
			
		||||
            Ok(contract) => updated_contracts.push(contract),
 | 
			
		||||
            Err(e) => log::error!("Failed to update contract {}: {}", contract_id, e),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(updated_contracts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a signer's reminder timestamp for a contract and returns the updated contract.
 | 
			
		||||
pub fn update_signer_reminder_timestamp(
 | 
			
		||||
    contract_id: u32,
 | 
			
		||||
    signer_id: &str,
 | 
			
		||||
    _timestamp: u64,
 | 
			
		||||
) -> Result<Contract, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Contract>()
 | 
			
		||||
        .expect("can open contract collection");
 | 
			
		||||
 | 
			
		||||
    if let Some(mut contract) = collection
 | 
			
		||||
        .get_by_id(contract_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch contract: {:?}", e))?
 | 
			
		||||
    {
 | 
			
		||||
        let mut signer_found = false;
 | 
			
		||||
        for signer in &mut contract.signers {
 | 
			
		||||
            if signer.id == signer_id {
 | 
			
		||||
                // TODO: Update reminder timestamp when field is available in heromodels
 | 
			
		||||
                // signer.last_reminder_mail_sent_at = Some(timestamp);
 | 
			
		||||
                signer_found = true;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !signer_found {
 | 
			
		||||
            return Err(format!("Signer with ID {} not found", signer_id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let (_, updated_contract) = collection
 | 
			
		||||
            .set(&contract)
 | 
			
		||||
            .map_err(|e| format!("Failed to update contract: {:?}", e))?;
 | 
			
		||||
        Ok(updated_contract)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Contract not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets contract statistics
 | 
			
		||||
pub fn get_contract_statistics() -> Result<ContractStatistics, String> {
 | 
			
		||||
    let contracts = get_contracts()?;
 | 
			
		||||
 | 
			
		||||
    let total = contracts.len();
 | 
			
		||||
    let draft = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::Draft)
 | 
			
		||||
        .count();
 | 
			
		||||
    let pending = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::PendingSignatures)
 | 
			
		||||
        .count();
 | 
			
		||||
    let signed = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::Signed)
 | 
			
		||||
        .count();
 | 
			
		||||
    let active = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::Active)
 | 
			
		||||
        .count();
 | 
			
		||||
    let expired = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::Expired)
 | 
			
		||||
        .count();
 | 
			
		||||
    let cancelled = contracts
 | 
			
		||||
        .iter()
 | 
			
		||||
        .filter(|c| c.status == ContractStatus::Cancelled)
 | 
			
		||||
        .count();
 | 
			
		||||
 | 
			
		||||
    Ok(ContractStatistics {
 | 
			
		||||
        total_contracts: total,
 | 
			
		||||
        draft_contracts: draft,
 | 
			
		||||
        pending_signature_contracts: pending,
 | 
			
		||||
        signed_contracts: signed,
 | 
			
		||||
        active_contracts: active,
 | 
			
		||||
        expired_contracts: expired,
 | 
			
		||||
        cancelled_contracts: cancelled,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A helper for current timestamp (seconds since epoch)
 | 
			
		||||
fn current_timestamp_secs() -> u64 {
 | 
			
		||||
    std::time::SystemTime::now()
 | 
			
		||||
        .duration_since(std::time::UNIX_EPOCH)
 | 
			
		||||
        .unwrap_or_default()
 | 
			
		||||
        .as_secs()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Contract statistics structure
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct ContractStatistics {
 | 
			
		||||
    pub total_contracts: usize,
 | 
			
		||||
    pub draft_contracts: usize,
 | 
			
		||||
    pub pending_signature_contracts: usize,
 | 
			
		||||
    pub signed_contracts: usize,
 | 
			
		||||
    pub active_contracts: usize,
 | 
			
		||||
    pub expired_contracts: usize,
 | 
			
		||||
    pub cancelled_contracts: usize,
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use heromodels::db::hero::OurDB;
 | 
			
		||||
 | 
			
		||||
/// The path to the database file. Change this as needed for your environment.
 | 
			
		||||
pub const DB_PATH: &str = "/tmp/freezone_db";
 | 
			
		||||
 | 
			
		||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
 | 
			
		||||
pub fn get_db() -> Result<OurDB, String> {
 | 
			
		||||
    let db_path = PathBuf::from(DB_PATH);
 | 
			
		||||
    if let Some(parent) = db_path.parent() {
 | 
			
		||||
        let _ = std::fs::create_dir_all(parent);
 | 
			
		||||
    }
 | 
			
		||||
    // Temporarily reset the database to fix the serialization issue
 | 
			
		||||
    let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
 | 
			
		||||
    Ok(db)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,199 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use crate::models::document::{Document, DocumentType};
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
const DOCUMENTS_FILE: &str = "/tmp/freezone_documents.json";
 | 
			
		||||
 | 
			
		||||
/// Helper function to load documents from JSON file
 | 
			
		||||
fn load_documents() -> Result<Vec<Document>, String> {
 | 
			
		||||
    if !Path::new(DOCUMENTS_FILE).exists() {
 | 
			
		||||
        return Ok(vec![]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let content = fs::read_to_string(DOCUMENTS_FILE)
 | 
			
		||||
        .map_err(|e| format!("Failed to read documents file: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    if content.trim().is_empty() {
 | 
			
		||||
        return Ok(vec![]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    serde_json::from_str(&content).map_err(|e| format!("Failed to parse documents JSON: {}", e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper function to save documents to JSON file
 | 
			
		||||
fn save_documents(documents: &[Document]) -> Result<(), String> {
 | 
			
		||||
    let content = serde_json::to_string_pretty(documents)
 | 
			
		||||
        .map_err(|e| format!("Failed to serialize documents: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    fs::write(DOCUMENTS_FILE, content).map_err(|e| format!("Failed to write documents file: {}", e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new document and saves it to the database
 | 
			
		||||
pub fn create_new_document(
 | 
			
		||||
    name: String,
 | 
			
		||||
    file_path: String,
 | 
			
		||||
    file_size: u64,
 | 
			
		||||
    mime_type: String,
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    uploaded_by: String,
 | 
			
		||||
    document_type: DocumentType,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    is_public: bool,
 | 
			
		||||
    checksum: Option<String>,
 | 
			
		||||
) -> Result<u32, String> {
 | 
			
		||||
    let mut documents = load_documents()?;
 | 
			
		||||
 | 
			
		||||
    // Create new document
 | 
			
		||||
    let mut document = Document::new(
 | 
			
		||||
        name,
 | 
			
		||||
        file_path,
 | 
			
		||||
        file_size,
 | 
			
		||||
        mime_type,
 | 
			
		||||
        company_id,
 | 
			
		||||
        uploaded_by,
 | 
			
		||||
    )
 | 
			
		||||
    .document_type(document_type)
 | 
			
		||||
    .is_public(is_public);
 | 
			
		||||
 | 
			
		||||
    if let Some(desc) = description {
 | 
			
		||||
        document = document.description(desc);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if let Some(checksum) = checksum {
 | 
			
		||||
        document = document.checksum(checksum);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate next ID (simple incremental)
 | 
			
		||||
    let next_id = documents.iter().map(|d| d.id).max().unwrap_or(0) + 1;
 | 
			
		||||
    document.id = next_id;
 | 
			
		||||
 | 
			
		||||
    documents.push(document);
 | 
			
		||||
    save_documents(&documents)?;
 | 
			
		||||
 | 
			
		||||
    Ok(next_id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all documents from the database
 | 
			
		||||
pub fn get_documents() -> Result<Vec<Document>, String> {
 | 
			
		||||
    load_documents()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets all documents for a specific company
 | 
			
		||||
pub fn get_company_documents(company_id: u32) -> Result<Vec<Document>, String> {
 | 
			
		||||
    let all_documents = load_documents()?;
 | 
			
		||||
 | 
			
		||||
    // Filter documents by company_id
 | 
			
		||||
    let company_documents = all_documents
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|document| document.company_id == company_id)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(company_documents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single document by its ID
 | 
			
		||||
pub fn get_document_by_id(document_id: u32) -> Result<Option<Document>, String> {
 | 
			
		||||
    let documents = load_documents()?;
 | 
			
		||||
 | 
			
		||||
    let document = documents.into_iter().find(|doc| doc.id == document_id);
 | 
			
		||||
 | 
			
		||||
    Ok(document)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates document in the database
 | 
			
		||||
pub fn update_document(
 | 
			
		||||
    document_id: u32,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    document_type: Option<DocumentType>,
 | 
			
		||||
    is_public: Option<bool>,
 | 
			
		||||
) -> Result<Document, String> {
 | 
			
		||||
    let mut documents = load_documents()?;
 | 
			
		||||
 | 
			
		||||
    if let Some(document) = documents.iter_mut().find(|doc| doc.id == document_id) {
 | 
			
		||||
        // Update fields
 | 
			
		||||
        if let Some(name) = name {
 | 
			
		||||
            document.name = name;
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(description) = description {
 | 
			
		||||
            document.description = Some(description);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(document_type) = document_type {
 | 
			
		||||
            document.document_type = document_type;
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(is_public) = is_public {
 | 
			
		||||
            document.is_public = is_public;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let updated_document = document.clone();
 | 
			
		||||
        save_documents(&documents)?;
 | 
			
		||||
        Ok(updated_document)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err("Document not found".to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes document from the database
 | 
			
		||||
pub fn delete_document(document_id: u32) -> Result<(), String> {
 | 
			
		||||
    let mut documents = load_documents()?;
 | 
			
		||||
 | 
			
		||||
    let initial_len = documents.len();
 | 
			
		||||
    documents.retain(|doc| doc.id != document_id);
 | 
			
		||||
 | 
			
		||||
    if documents.len() == initial_len {
 | 
			
		||||
        return Err("Document not found".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    save_documents(&documents)?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets documents by type for a company
 | 
			
		||||
pub fn get_company_documents_by_type(
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    document_type: DocumentType,
 | 
			
		||||
) -> Result<Vec<Document>, String> {
 | 
			
		||||
    let company_documents = get_company_documents(company_id)?;
 | 
			
		||||
 | 
			
		||||
    let filtered_documents = company_documents
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|doc| doc.document_type == document_type)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(filtered_documents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets public documents for a company
 | 
			
		||||
pub fn get_public_company_documents(company_id: u32) -> Result<Vec<Document>, String> {
 | 
			
		||||
    let company_documents = get_company_documents(company_id)?;
 | 
			
		||||
 | 
			
		||||
    let public_documents = company_documents
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|doc| doc.is_public)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(public_documents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Searches documents by name for a company
 | 
			
		||||
pub fn search_company_documents(
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    search_term: &str,
 | 
			
		||||
) -> Result<Vec<Document>, String> {
 | 
			
		||||
    let company_documents = get_company_documents(company_id)?;
 | 
			
		||||
 | 
			
		||||
    let search_term_lower = search_term.to_lowercase();
 | 
			
		||||
    let matching_documents = company_documents
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|doc| {
 | 
			
		||||
            doc.name.to_lowercase().contains(&search_term_lower)
 | 
			
		||||
                || doc.description.as_ref().map_or(false, |desc| {
 | 
			
		||||
                    desc.to_lowercase().contains(&search_term_lower)
 | 
			
		||||
                })
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(matching_documents)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,257 +0,0 @@
 | 
			
		||||
use chrono::{Duration, Utc};
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::governance::{Activity, ActivityType, Proposal, ProposalStatus},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
 | 
			
		||||
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
 | 
			
		||||
pub fn create_new_proposal(
 | 
			
		||||
    creator_id: &str,
 | 
			
		||||
    creator_name: &str,
 | 
			
		||||
    title: &str,
 | 
			
		||||
    description: &str,
 | 
			
		||||
    status: ProposalStatus,
 | 
			
		||||
    voting_start_date: Option<chrono::DateTime<Utc>>,
 | 
			
		||||
    voting_end_date: Option<chrono::DateTime<Utc>>,
 | 
			
		||||
) -> Result<(u32, Proposal), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    let created_at = Utc::now();
 | 
			
		||||
    let updated_at = created_at;
 | 
			
		||||
 | 
			
		||||
    // Create a new proposal (with auto-generated ID)
 | 
			
		||||
    let proposal = Proposal::new(
 | 
			
		||||
        None,
 | 
			
		||||
        creator_id,
 | 
			
		||||
        creator_name,
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        status,
 | 
			
		||||
        created_at,
 | 
			
		||||
        updated_at,
 | 
			
		||||
        voting_start_date.unwrap_or_else(Utc::now),
 | 
			
		||||
        voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
 | 
			
		||||
    );
 | 
			
		||||
    // Save the proposal to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .expect("can open proposal collection");
 | 
			
		||||
    let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
 | 
			
		||||
 | 
			
		||||
    Ok((proposal_id, saved_proposal))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all proposals from the database and returns them as a Vec<Proposal>.
 | 
			
		||||
pub fn get_proposals() -> Result<Vec<Proposal>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .expect("can open proposal collection");
 | 
			
		||||
 | 
			
		||||
    // Try to load all proposals, but handle deserialization errors gracefully
 | 
			
		||||
    let proposals = match collection.get_all() {
 | 
			
		||||
        Ok(props) => props,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error loading proposals: {:?}", e);
 | 
			
		||||
            vec![] // Return an empty vector if there's an error
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(proposals)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fetches a single proposal by its ID from the database.
 | 
			
		||||
pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
    match collection.get_by_id(proposal_id) {
 | 
			
		||||
        Ok(proposal) => Ok(Some(proposal.expect("proposal not found"))),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Error fetching proposal by id {}: {:?}", proposal_id, e);
 | 
			
		||||
            Err(format!("Failed to fetch proposal: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Submits a vote on a proposal and returns the updated proposal
 | 
			
		||||
pub fn submit_vote_on_proposal(
 | 
			
		||||
    proposal_id: u32,
 | 
			
		||||
    user_id: i32,
 | 
			
		||||
    vote_type: &str,
 | 
			
		||||
    shares_count: u32, // Default to 1 if not specified
 | 
			
		||||
    comment: Option<String>,
 | 
			
		||||
) -> Result<Proposal, String> {
 | 
			
		||||
    // Get the proposal from the database
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Get the proposal
 | 
			
		||||
    let mut proposal = collection
 | 
			
		||||
        .get_by_id(proposal_id)
 | 
			
		||||
        .map_err(|e| format!("Failed to fetch proposal: {:?}", e))?
 | 
			
		||||
        .ok_or_else(|| format!("Proposal not found with ID: {}", proposal_id))?;
 | 
			
		||||
 | 
			
		||||
    // Ensure the proposal has vote options
 | 
			
		||||
    // Check if the proposal already has options
 | 
			
		||||
    if proposal.options.is_empty() {
 | 
			
		||||
        // Add standard vote options if they don't exist
 | 
			
		||||
        proposal = proposal.add_option(1, "Approve", Some("Approve the proposal"));
 | 
			
		||||
        proposal = proposal.add_option(2, "Reject", Some("Reject the proposal"));
 | 
			
		||||
        proposal = proposal.add_option(3, "Abstain", Some("Abstain from voting"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Map vote_type to option_id
 | 
			
		||||
    let option_id = match vote_type {
 | 
			
		||||
        "Yes" => 1,     // Approve
 | 
			
		||||
        "No" => 2,      // Reject
 | 
			
		||||
        "Abstain" => 3, // Abstain
 | 
			
		||||
        _ => return Err(format!("Invalid vote type: {}", vote_type)),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Since we're having issues with the cast_vote method, let's implement a workaround
 | 
			
		||||
    // that directly updates the vote count for the selected option
 | 
			
		||||
 | 
			
		||||
    // Check if the proposal is active
 | 
			
		||||
    if proposal.status != ProposalStatus::Active {
 | 
			
		||||
        return Err(format!(
 | 
			
		||||
            "Cannot vote on a proposal with status: {:?}",
 | 
			
		||||
            proposal.status
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if voting period is valid
 | 
			
		||||
    let now = Utc::now();
 | 
			
		||||
    if now > proposal.vote_end_date {
 | 
			
		||||
        return Err("Voting period has ended".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if now < proposal.vote_start_date {
 | 
			
		||||
        return Err("Voting period has not started yet".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find the option and increment its count
 | 
			
		||||
    let mut option_found = false;
 | 
			
		||||
    for option in &mut proposal.options {
 | 
			
		||||
        if option.id == option_id {
 | 
			
		||||
            option.count += shares_count as i64;
 | 
			
		||||
            option_found = true;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !option_found {
 | 
			
		||||
        return Err(format!("Option with ID {} not found", option_id));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Record the vote in the proposal's ballots
 | 
			
		||||
    // We'll create a simple ballot with an auto-generated ID
 | 
			
		||||
    let ballot_id = proposal.ballots.len() as u32 + 1;
 | 
			
		||||
 | 
			
		||||
    // Create a new ballot and add it to the proposal's ballots
 | 
			
		||||
    use heromodels::models::governance::Ballot;
 | 
			
		||||
 | 
			
		||||
    // Use the Ballot::new constructor which handles the BaseModelData creation
 | 
			
		||||
    let mut ballot = Ballot::new(
 | 
			
		||||
        Some(ballot_id),
 | 
			
		||||
        user_id as u32,
 | 
			
		||||
        option_id,
 | 
			
		||||
        shares_count as i64,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Set the comment if provided
 | 
			
		||||
    ballot.comment = comment;
 | 
			
		||||
 | 
			
		||||
    // Store the local time (EEST = UTC+3) as the vote timestamp
 | 
			
		||||
    // This ensures the displayed time matches the user's local time
 | 
			
		||||
    let utc_now = Utc::now();
 | 
			
		||||
    let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
 | 
			
		||||
    let local_time = utc_now.with_timezone(&local_offset);
 | 
			
		||||
 | 
			
		||||
    // Store the local time as a timestamp (this is what will be displayed)
 | 
			
		||||
    ballot.base_data.created_at = local_time.timestamp();
 | 
			
		||||
 | 
			
		||||
    // Add the ballot to the proposal's ballots
 | 
			
		||||
    proposal.ballots.push(ballot);
 | 
			
		||||
 | 
			
		||||
    // Update the proposal's updated_at timestamp
 | 
			
		||||
    proposal.updated_at = Utc::now();
 | 
			
		||||
 | 
			
		||||
    // Save the updated proposal
 | 
			
		||||
    let (_, updated_proposal) = collection
 | 
			
		||||
        .set(&proposal)
 | 
			
		||||
        .map_err(|e| format!("Failed to save vote: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(updated_proposal)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(unused_assignments)]
 | 
			
		||||
/// Creates a new governance activity and saves it to the database using OurDB
 | 
			
		||||
pub fn create_activity(
 | 
			
		||||
    proposal_id: u32,
 | 
			
		||||
    proposal_title: &str,
 | 
			
		||||
    creator_name: &str,
 | 
			
		||||
    activity_type: &ActivityType,
 | 
			
		||||
) -> Result<(u32, Activity), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let mut activity = Activity::default();
 | 
			
		||||
 | 
			
		||||
    match activity_type {
 | 
			
		||||
        ActivityType::ProposalCreated => {
 | 
			
		||||
            activity = Activity::proposal_created(proposal_id, proposal_title, creator_name);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VoteCast => {
 | 
			
		||||
            activity = Activity::vote_cast(proposal_id, proposal_title, creator_name);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VotingStarted => {
 | 
			
		||||
            activity = Activity::voting_started(proposal_id, proposal_title);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VotingEnded => {
 | 
			
		||||
            activity = Activity::voting_ended(proposal_id, proposal_title);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save the proposal to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .expect("can open activity collection");
 | 
			
		||||
 | 
			
		||||
    let (proposal_id, saved_proposal) = collection.set(&activity).expect("can save proposal");
 | 
			
		||||
    Ok((proposal_id, saved_proposal))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_recent_activities() -> Result<Vec<Activity>, String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    let mut db_activities = collection
 | 
			
		||||
        .get_all()
 | 
			
		||||
        .map_err(|e| format!("DB fetch error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Sort by created_at descending
 | 
			
		||||
    db_activities.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
 | 
			
		||||
    // Take the top 10 most recent
 | 
			
		||||
    let recent_activities = db_activities.into_iter().take(10).collect();
 | 
			
		||||
 | 
			
		||||
    Ok(recent_activities)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_all_activities() -> Result<Vec<Activity>, String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    let db_activities = collection
 | 
			
		||||
        .get_all()
 | 
			
		||||
        .map_err(|e| format!("DB fetch error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(db_activities)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
pub mod company;
 | 
			
		||||
pub mod contracts;
 | 
			
		||||
pub mod db;
 | 
			
		||||
pub mod document;
 | 
			
		||||
pub mod governance;
 | 
			
		||||
pub mod payment;
 | 
			
		||||
pub mod registration;
 | 
			
		||||
@@ -1,355 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::{Payment, PaymentStatus},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Creates a new payment and saves it to the database
 | 
			
		||||
pub fn create_new_payment(
 | 
			
		||||
    payment_intent_id: String,
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
    payment_plan: String,
 | 
			
		||||
    setup_fee: f64,
 | 
			
		||||
    monthly_fee: f64,
 | 
			
		||||
    total_amount: f64,
 | 
			
		||||
) -> Result<(u32, Payment), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    // Create using heromodels constructor
 | 
			
		||||
    let payment = Payment::new(
 | 
			
		||||
        payment_intent_id.clone(),
 | 
			
		||||
        company_id,
 | 
			
		||||
        payment_plan,
 | 
			
		||||
        setup_fee,
 | 
			
		||||
        monthly_fee,
 | 
			
		||||
        total_amount,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Save to database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
    let (id, saved_payment) = collection.set(&payment).expect("can save payment");
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Created payment with ID {} for company {} (Intent: {})",
 | 
			
		||||
        id,
 | 
			
		||||
        company_id,
 | 
			
		||||
        payment_intent_id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok((id, saved_payment))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Loads all payments from the database
 | 
			
		||||
pub fn get_payments() -> Result<Vec<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    let payments = match collection.get_all() {
 | 
			
		||||
        Ok(payments) => payments,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to load payments from database: {:?}", e);
 | 
			
		||||
            vec![]
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    Ok(payments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets a payment by its database ID
 | 
			
		||||
pub fn get_payment_by_id(payment_id: u32) -> Result<Option<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    match collection.get_by_id(payment_id) {
 | 
			
		||||
        Ok(payment) => Ok(payment),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to get payment by ID {}: {:?}", payment_id, e);
 | 
			
		||||
            Err(format!("Failed to get payment: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets a payment by Stripe payment intent ID
 | 
			
		||||
pub fn get_payment_by_intent_id(payment_intent_id: &str) -> Result<Option<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    // Get all payments and find by payment_intent_id
 | 
			
		||||
    // TODO: Use indexed query when available in heromodels
 | 
			
		||||
    let payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    let payment = payments
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .find(|p| p.payment_intent_id == payment_intent_id);
 | 
			
		||||
 | 
			
		||||
    Ok(payment)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets all payments for a specific company
 | 
			
		||||
pub fn get_company_payments(company_id: u32) -> Result<Vec<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    // Get all payments and filter by company_id
 | 
			
		||||
    // TODO: Use indexed query when available in heromodels
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    let company_payments = all_payments
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|payment| payment.company_id == company_id)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(company_payments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates a payment in the database
 | 
			
		||||
pub fn update_payment(payment: Payment) -> Result<(u32, Payment), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    let (id, updated_payment) = collection.set(&payment).expect("can update payment");
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Updated payment with ID {} (Intent: {}, Status: {:?})",
 | 
			
		||||
        id,
 | 
			
		||||
        payment.payment_intent_id,
 | 
			
		||||
        payment.status
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok((id, updated_payment))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Update payment with company ID after company creation
 | 
			
		||||
pub fn update_payment_company_id(
 | 
			
		||||
    payment_intent_id: &str,
 | 
			
		||||
    company_id: u32,
 | 
			
		||||
) -> Result<Option<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    // Get all payments and find the one to update
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments for company ID update: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    // Find the payment by payment_intent_id
 | 
			
		||||
    for (_index, payment) in all_payments.iter().enumerate() {
 | 
			
		||||
        if payment.payment_intent_id == payment_intent_id {
 | 
			
		||||
            // Create updated payment with company_id
 | 
			
		||||
            let mut updated_payment = payment.clone();
 | 
			
		||||
            updated_payment.company_id = company_id;
 | 
			
		||||
 | 
			
		||||
            // Update in database (this is a limitation of current DB interface)
 | 
			
		||||
            let (_, saved_payment) = collection.set(&updated_payment).map_err(|e| {
 | 
			
		||||
                log::error!("Failed to update payment company ID: {:?}", e);
 | 
			
		||||
                format!("Failed to update payment: {:?}", e)
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Updated payment {} with company ID {}",
 | 
			
		||||
                payment_intent_id,
 | 
			
		||||
                company_id
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return Ok(Some(saved_payment));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    log::warn!(
 | 
			
		||||
        "Payment not found for intent ID: {} (cannot update company ID)",
 | 
			
		||||
        payment_intent_id
 | 
			
		||||
    );
 | 
			
		||||
    Ok(None)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Update payment status
 | 
			
		||||
pub fn update_payment_status(
 | 
			
		||||
    payment_intent_id: &str,
 | 
			
		||||
    status: heromodels::models::biz::PaymentStatus,
 | 
			
		||||
) -> Result<Option<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    // Get all payments and find the one to update
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments for status update: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    // Find the payment by payment_intent_id
 | 
			
		||||
    for (_index, payment) in all_payments.iter().enumerate() {
 | 
			
		||||
        if payment.payment_intent_id == payment_intent_id {
 | 
			
		||||
            // Create updated payment with new status
 | 
			
		||||
            let mut updated_payment = payment.clone();
 | 
			
		||||
            updated_payment.status = status.clone();
 | 
			
		||||
 | 
			
		||||
            // Update in database
 | 
			
		||||
            let (_, saved_payment) = collection.set(&updated_payment).map_err(|e| {
 | 
			
		||||
                log::error!("Failed to update payment status: {:?}", e);
 | 
			
		||||
                format!("Failed to update payment: {:?}", e)
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Updated payment {} status to {:?}",
 | 
			
		||||
                payment_intent_id,
 | 
			
		||||
                status
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return Ok(Some(saved_payment));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    log::warn!(
 | 
			
		||||
        "Payment not found for intent ID: {} (cannot update status)",
 | 
			
		||||
        payment_intent_id
 | 
			
		||||
    );
 | 
			
		||||
    Ok(None)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get all pending payments (for monitoring/retry)
 | 
			
		||||
pub fn get_pending_payments() -> Result<Vec<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    // Filter for pending payments
 | 
			
		||||
    let pending_payments = all_payments
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|payment| payment.status == heromodels::models::biz::PaymentStatus::Pending)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(pending_payments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get failed payments (for retry/investigation)
 | 
			
		||||
pub fn get_failed_payments() -> Result<Vec<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    // Filter for failed payments
 | 
			
		||||
    let failed_payments = all_payments
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|payment| payment.status == heromodels::models::biz::PaymentStatus::Failed)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(failed_payments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Completes a payment (marks as completed with Stripe customer ID)
 | 
			
		||||
pub fn complete_payment(
 | 
			
		||||
    payment_intent_id: &str,
 | 
			
		||||
    stripe_customer_id: Option<String>,
 | 
			
		||||
) -> Result<Option<Payment>, String> {
 | 
			
		||||
    if let Some(payment) = get_payment_by_intent_id(payment_intent_id)? {
 | 
			
		||||
        let completed_payment = payment.complete_payment(stripe_customer_id);
 | 
			
		||||
        let (_, updated_payment) = update_payment(completed_payment)?;
 | 
			
		||||
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Completed payment {} for company {}",
 | 
			
		||||
            payment_intent_id,
 | 
			
		||||
            updated_payment.company_id
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Ok(Some(updated_payment))
 | 
			
		||||
    } else {
 | 
			
		||||
        log::warn!("Payment not found for intent ID: {}", payment_intent_id);
 | 
			
		||||
        Ok(None)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Marks a payment as failed
 | 
			
		||||
pub fn fail_payment(payment_intent_id: &str) -> Result<Option<Payment>, String> {
 | 
			
		||||
    if let Some(payment) = get_payment_by_intent_id(payment_intent_id)? {
 | 
			
		||||
        let failed_payment = payment.fail_payment();
 | 
			
		||||
        let (_, updated_payment) = update_payment(failed_payment)?;
 | 
			
		||||
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Failed payment {} for company {}",
 | 
			
		||||
            payment_intent_id,
 | 
			
		||||
            updated_payment.company_id
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Ok(Some(updated_payment))
 | 
			
		||||
    } else {
 | 
			
		||||
        log::warn!("Payment not found for intent ID: {}", payment_intent_id);
 | 
			
		||||
        Ok(None)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets payments by status
 | 
			
		||||
pub fn get_payments_by_status(status: PaymentStatus) -> Result<Vec<Payment>, String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .expect("can open payment collection");
 | 
			
		||||
 | 
			
		||||
    // Get all payments and filter by status
 | 
			
		||||
    // TODO: Use indexed query when available in heromodels
 | 
			
		||||
    let all_payments = collection.get_all().map_err(|e| {
 | 
			
		||||
        log::error!("Failed to get payments: {:?}", e);
 | 
			
		||||
        format!("Failed to get payments: {:?}", e)
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    let filtered_payments = all_payments
 | 
			
		||||
        .into_iter()
 | 
			
		||||
        .filter(|payment| payment.status == status)
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    Ok(filtered_payments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Deletes a payment from the database
 | 
			
		||||
pub fn delete_payment(payment_id: u32) -> Result<(), String> {
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Payment>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    match collection.delete_by_id(payment_id) {
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            log::info!("Successfully deleted payment with ID {}", payment_id);
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to delete payment {}: {:?}", payment_id, e);
 | 
			
		||||
            Err(format!("Failed to delete payment: {:?}", e))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,272 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Database utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
/// Stored registration data linked to payment intent
 | 
			
		||||
/// This preserves all user form data until company creation after payment success
 | 
			
		||||
/// NOTE: This uses file-based storage until we can add the model to heromodels
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct StoredRegistrationData {
 | 
			
		||||
    pub payment_intent_id: String,
 | 
			
		||||
    pub company_name: String,
 | 
			
		||||
    pub company_type: String,
 | 
			
		||||
    pub company_email: String,
 | 
			
		||||
    pub company_phone: String,
 | 
			
		||||
    pub company_website: Option<String>,
 | 
			
		||||
    pub company_address: String,
 | 
			
		||||
    pub company_industry: Option<String>,
 | 
			
		||||
    pub company_purpose: Option<String>,
 | 
			
		||||
    pub fiscal_year_end: Option<String>,
 | 
			
		||||
    pub shareholders: String, // JSON string of shareholders array
 | 
			
		||||
    pub payment_plan: String,
 | 
			
		||||
    pub created_at: i64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// File path for storing registration data
 | 
			
		||||
const REGISTRATION_DATA_FILE: &str = "data/registration_data.json";
 | 
			
		||||
 | 
			
		||||
/// Ensure data directory exists
 | 
			
		||||
fn ensure_data_directory() -> Result<(), String> {
 | 
			
		||||
    let data_dir = Path::new("data");
 | 
			
		||||
    if !data_dir.exists() {
 | 
			
		||||
        fs::create_dir_all(data_dir)
 | 
			
		||||
            .map_err(|e| format!("Failed to create data directory: {}", e))?;
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Load all registration data from file
 | 
			
		||||
fn load_registration_data() -> Result<HashMap<String, StoredRegistrationData>, String> {
 | 
			
		||||
    if !Path::new(REGISTRATION_DATA_FILE).exists() {
 | 
			
		||||
        return Ok(HashMap::new());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let content = fs::read_to_string(REGISTRATION_DATA_FILE)
 | 
			
		||||
        .map_err(|e| format!("Failed to read registration data file: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    let data: HashMap<String, StoredRegistrationData> = serde_json::from_str(&content)
 | 
			
		||||
        .map_err(|e| format!("Failed to parse registration data: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Save all registration data to file
 | 
			
		||||
fn save_registration_data(data: &HashMap<String, StoredRegistrationData>) -> Result<(), String> {
 | 
			
		||||
    ensure_data_directory()?;
 | 
			
		||||
 | 
			
		||||
    let content = serde_json::to_string_pretty(data)
 | 
			
		||||
        .map_err(|e| format!("Failed to serialize registration data: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    fs::write(REGISTRATION_DATA_FILE, content)
 | 
			
		||||
        .map_err(|e| format!("Failed to write registration data file: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl StoredRegistrationData {
 | 
			
		||||
    /// Create new stored registration data
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        payment_intent_id: String,
 | 
			
		||||
        company_name: String,
 | 
			
		||||
        company_type: String,
 | 
			
		||||
        company_email: String,
 | 
			
		||||
        company_phone: String,
 | 
			
		||||
        company_website: Option<String>,
 | 
			
		||||
        company_address: String,
 | 
			
		||||
        company_industry: Option<String>,
 | 
			
		||||
        company_purpose: Option<String>,
 | 
			
		||||
        fiscal_year_end: Option<String>,
 | 
			
		||||
        shareholders: String,
 | 
			
		||||
        payment_plan: String,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            payment_intent_id,
 | 
			
		||||
            company_name,
 | 
			
		||||
            company_type,
 | 
			
		||||
            company_email,
 | 
			
		||||
            company_phone,
 | 
			
		||||
            company_website,
 | 
			
		||||
            company_address,
 | 
			
		||||
            company_industry,
 | 
			
		||||
            company_purpose,
 | 
			
		||||
            fiscal_year_end,
 | 
			
		||||
            shareholders,
 | 
			
		||||
            payment_plan,
 | 
			
		||||
            created_at: chrono::Utc::now().timestamp(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Store registration data linked to payment intent
 | 
			
		||||
pub fn store_registration_data(
 | 
			
		||||
    payment_intent_id: String,
 | 
			
		||||
    data: crate::controllers::payment::CompanyRegistrationData,
 | 
			
		||||
) -> Result<(u32, StoredRegistrationData), String> {
 | 
			
		||||
    // Create stored registration data
 | 
			
		||||
    let stored_data = StoredRegistrationData::new(
 | 
			
		||||
        payment_intent_id.clone(),
 | 
			
		||||
        data.company_name,
 | 
			
		||||
        data.company_type,
 | 
			
		||||
        data.company_email
 | 
			
		||||
            .unwrap_or_else(|| "noemail@example.com".to_string()),
 | 
			
		||||
        data.company_phone
 | 
			
		||||
            .unwrap_or_else(|| "+1234567890".to_string()),
 | 
			
		||||
        data.company_website,
 | 
			
		||||
        data.company_address
 | 
			
		||||
            .unwrap_or_else(|| "No address provided".to_string()),
 | 
			
		||||
        data.company_industry,
 | 
			
		||||
        data.company_purpose,
 | 
			
		||||
        data.fiscal_year_end,
 | 
			
		||||
        data.shareholders,
 | 
			
		||||
        data.payment_plan,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Load existing data
 | 
			
		||||
    let mut all_data = load_registration_data()?;
 | 
			
		||||
 | 
			
		||||
    // Add new data
 | 
			
		||||
    all_data.insert(payment_intent_id.clone(), stored_data.clone());
 | 
			
		||||
 | 
			
		||||
    // Save to file
 | 
			
		||||
    save_registration_data(&all_data)?;
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Stored registration data for payment intent {}",
 | 
			
		||||
        payment_intent_id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Return with a generated ID (timestamp-based)
 | 
			
		||||
    let id = chrono::Utc::now().timestamp() as u32;
 | 
			
		||||
    Ok((id, stored_data))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Retrieve registration data by payment intent ID
 | 
			
		||||
pub fn get_registration_data(
 | 
			
		||||
    payment_intent_id: &str,
 | 
			
		||||
) -> Result<Option<StoredRegistrationData>, String> {
 | 
			
		||||
    let all_data = load_registration_data()?;
 | 
			
		||||
    Ok(all_data.get(payment_intent_id).cloned())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get all stored registration data
 | 
			
		||||
pub fn get_all_registration_data() -> Result<Vec<StoredRegistrationData>, String> {
 | 
			
		||||
    let all_data = load_registration_data()?;
 | 
			
		||||
    Ok(all_data.into_values().collect())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Delete registration data by payment intent ID
 | 
			
		||||
pub fn delete_registration_data(payment_intent_id: &str) -> Result<bool, String> {
 | 
			
		||||
    let mut all_data = load_registration_data()?;
 | 
			
		||||
 | 
			
		||||
    if all_data.remove(payment_intent_id).is_some() {
 | 
			
		||||
        save_registration_data(&all_data)?;
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "Deleted registration data for payment intent: {}",
 | 
			
		||||
            payment_intent_id
 | 
			
		||||
        );
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    } else {
 | 
			
		||||
        log::warn!(
 | 
			
		||||
            "Registration data not found for payment intent: {}",
 | 
			
		||||
            payment_intent_id
 | 
			
		||||
        );
 | 
			
		||||
        Ok(false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Update registration data
 | 
			
		||||
pub fn update_registration_data(
 | 
			
		||||
    payment_intent_id: &str,
 | 
			
		||||
    updated_data: StoredRegistrationData,
 | 
			
		||||
) -> Result<Option<StoredRegistrationData>, String> {
 | 
			
		||||
    let mut all_data = load_registration_data()?;
 | 
			
		||||
 | 
			
		||||
    all_data.insert(payment_intent_id.to_string(), updated_data.clone());
 | 
			
		||||
    save_registration_data(&all_data)?;
 | 
			
		||||
 | 
			
		||||
    log::info!(
 | 
			
		||||
        "Updated registration data for payment intent: {}",
 | 
			
		||||
        payment_intent_id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(Some(updated_data))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Convert StoredRegistrationData back to CompanyRegistrationData for processing
 | 
			
		||||
pub fn stored_to_registration_data(
 | 
			
		||||
    stored: &StoredRegistrationData,
 | 
			
		||||
) -> crate::controllers::payment::CompanyRegistrationData {
 | 
			
		||||
    crate::controllers::payment::CompanyRegistrationData {
 | 
			
		||||
        company_name: stored.company_name.clone(),
 | 
			
		||||
        company_type: stored.company_type.clone(),
 | 
			
		||||
        company_email: Some(stored.company_email.clone()),
 | 
			
		||||
        company_phone: Some(stored.company_phone.clone()),
 | 
			
		||||
        company_website: stored.company_website.clone(),
 | 
			
		||||
        company_address: Some(stored.company_address.clone()),
 | 
			
		||||
        company_industry: stored.company_industry.clone(),
 | 
			
		||||
        company_purpose: stored.company_purpose.clone(),
 | 
			
		||||
        fiscal_year_end: stored.fiscal_year_end.clone(),
 | 
			
		||||
        shareholders: stored.shareholders.clone(),
 | 
			
		||||
        payment_plan: stored.payment_plan.clone(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_stored_registration_data_creation() {
 | 
			
		||||
        let data = StoredRegistrationData::new(
 | 
			
		||||
            "pi_test123".to_string(),
 | 
			
		||||
            "Test Company".to_string(),
 | 
			
		||||
            "Single FZC".to_string(),
 | 
			
		||||
            "test@example.com".to_string(),
 | 
			
		||||
            "+1234567890".to_string(),
 | 
			
		||||
            Some("https://example.com".to_string()),
 | 
			
		||||
            "123 Test St".to_string(),
 | 
			
		||||
            Some("Technology".to_string()),
 | 
			
		||||
            Some("Software development".to_string()),
 | 
			
		||||
            Some("December".to_string()),
 | 
			
		||||
            "[]".to_string(),
 | 
			
		||||
            "monthly".to_string(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert_eq!(data.payment_intent_id, "pi_test123");
 | 
			
		||||
        assert_eq!(data.company_name, "Test Company");
 | 
			
		||||
        assert_eq!(data.company_type, "Single FZC");
 | 
			
		||||
        assert_eq!(data.company_email, "test@example.com");
 | 
			
		||||
        assert!(data.created_at > 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_stored_to_registration_data_conversion() {
 | 
			
		||||
        let stored = StoredRegistrationData::new(
 | 
			
		||||
            "pi_test123".to_string(),
 | 
			
		||||
            "Test Company".to_string(),
 | 
			
		||||
            "Single FZC".to_string(),
 | 
			
		||||
            "test@example.com".to_string(),
 | 
			
		||||
            "+1234567890".to_string(),
 | 
			
		||||
            Some("https://example.com".to_string()),
 | 
			
		||||
            "123 Test St".to_string(),
 | 
			
		||||
            Some("Technology".to_string()),
 | 
			
		||||
            Some("Software development".to_string()),
 | 
			
		||||
            Some("December".to_string()),
 | 
			
		||||
            "[]".to_string(),
 | 
			
		||||
            "monthly".to_string(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let registration_data = stored_to_registration_data(&stored);
 | 
			
		||||
 | 
			
		||||
        assert_eq!(registration_data.company_name, "Test Company");
 | 
			
		||||
        assert_eq!(registration_data.company_type, "Single FZC");
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            registration_data.company_email,
 | 
			
		||||
            Some("test@example.com".to_string())
 | 
			
		||||
        );
 | 
			
		||||
        assert_eq!(registration_data.payment_plan, "monthly");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
// Library exports for testing and external use
 | 
			
		||||
 | 
			
		||||
use actix_web::cookie::Key;
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
 | 
			
		||||
pub mod config;
 | 
			
		||||
pub mod controllers;
 | 
			
		||||
pub mod db;
 | 
			
		||||
pub mod middleware;
 | 
			
		||||
pub mod models;
 | 
			
		||||
pub mod routes;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
pub mod validators;
 | 
			
		||||
 | 
			
		||||
// Session key needed by routes
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    pub static ref SESSION_KEY: Key = {
 | 
			
		||||
        // In production, this should be a proper secret key from environment variables
 | 
			
		||||
        let secret = std::env::var("SESSION_SECRET").unwrap_or_else(|_| {
 | 
			
		||||
            // Create a key that's at least 64 bytes long
 | 
			
		||||
            "my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string()
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Ensure the key is at least 64 bytes
 | 
			
		||||
        let mut key_bytes = secret.into_bytes();
 | 
			
		||||
        while key_bytes.len() < 64 {
 | 
			
		||||
            key_bytes.extend_from_slice(b"padding");
 | 
			
		||||
        }
 | 
			
		||||
        key_bytes.truncate(64);
 | 
			
		||||
 | 
			
		||||
        Key::from(&key_bytes)
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Re-export commonly used types for easier testing
 | 
			
		||||
pub use controllers::payment::CompanyRegistrationData;
 | 
			
		||||
pub use validators::{CompanyRegistrationValidator, ValidationError, ValidationResult};
 | 
			
		||||
@@ -1,24 +1,22 @@
 | 
			
		||||
use actix_files as fs;
 | 
			
		||||
use actix_web::middleware::Logger;
 | 
			
		||||
use actix_web::{App, HttpServer, web};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use std::env;
 | 
			
		||||
use std::io;
 | 
			
		||||
use actix_web::middleware::Logger;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::env;
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
 | 
			
		||||
mod config;
 | 
			
		||||
mod controllers;
 | 
			
		||||
mod db;
 | 
			
		||||
mod middleware;
 | 
			
		||||
mod models;
 | 
			
		||||
mod routes;
 | 
			
		||||
mod utils;
 | 
			
		||||
mod validators;
 | 
			
		||||
 | 
			
		||||
// Import middleware components
 | 
			
		||||
use middleware::{JwtAuth, RequestTimer, SecurityHeaders};
 | 
			
		||||
use models::initialize_mock_data;
 | 
			
		||||
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
 | 
			
		||||
use utils::redis_service;
 | 
			
		||||
use models::initialize_mock_data;
 | 
			
		||||
 | 
			
		||||
// Initialize lazy_static for in-memory storage
 | 
			
		||||
extern crate lazy_static;
 | 
			
		||||
@@ -31,13 +29,13 @@ lazy_static! {
 | 
			
		||||
            // Create a key that's at least 64 bytes long
 | 
			
		||||
            "my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string()
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Ensure the key is at least 64 bytes
 | 
			
		||||
        let mut key_bytes = secret.as_bytes().to_vec();
 | 
			
		||||
        while key_bytes.len() < 64 {
 | 
			
		||||
            key_bytes.extend_from_slice(b"0123456789abcdef");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        actix_web::cookie::Key::from(&key_bytes[0..64])
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -47,22 +45,14 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
    // Initialize environment
 | 
			
		||||
    dotenv::dotenv().ok();
 | 
			
		||||
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Load configuration
 | 
			
		||||
    let config = config::get_config();
 | 
			
		||||
 | 
			
		||||
    // Check for port override from environment variable or command line arguments
 | 
			
		||||
    
 | 
			
		||||
    // Check for port override from command line arguments
 | 
			
		||||
    let args: Vec<String> = env::args().collect();
 | 
			
		||||
    let mut port = config.server.port;
 | 
			
		||||
 | 
			
		||||
    // First check environment variable
 | 
			
		||||
    if let Ok(env_port) = env::var("PORT") {
 | 
			
		||||
        if let Ok(p) = env_port.parse::<u16>() {
 | 
			
		||||
            port = p;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Then check command line arguments (takes precedence over env var)
 | 
			
		||||
    
 | 
			
		||||
    for i in 1..args.len() {
 | 
			
		||||
        if args[i] == "--port" && i + 1 < args.len() {
 | 
			
		||||
            if let Ok(p) = args[i + 1].parse::<u16>() {
 | 
			
		||||
@@ -71,28 +61,24 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    let bind_address = format!("{}:{}", config.server.host, port);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Initialize Redis client
 | 
			
		||||
    let redis_url =
 | 
			
		||||
        std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
 | 
			
		||||
    let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
 | 
			
		||||
    if let Err(e) = redis_service::init_redis_client(&redis_url) {
 | 
			
		||||
        log::error!("Failed to initialize Redis client: {}", e);
 | 
			
		||||
        log::warn!("Calendar functionality will not work properly without Redis");
 | 
			
		||||
    } else {
 | 
			
		||||
        log::info!("Redis client initialized successfully");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Initialize mock data for DeFi operations
 | 
			
		||||
    initialize_mock_data();
 | 
			
		||||
    log::info!("DeFi mock data initialized successfully");
 | 
			
		||||
 | 
			
		||||
    // Governance activity tracker is now ready to record real user activities
 | 
			
		||||
    log::info!("Governance activity tracker initialized and ready");
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    log::info!("Starting server at http://{}", bind_address);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Create and configure the HTTP server
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        // Initialize Tera templates
 | 
			
		||||
@@ -103,10 +89,10 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
                ::std::process::exit(1);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Register custom Tera functions
 | 
			
		||||
        utils::register_tera_functions(&mut tera);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        App::new()
 | 
			
		||||
            // Enable logger middleware
 | 
			
		||||
            .wrap(Logger::default())
 | 
			
		||||
@@ -120,8 +106,6 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
            .app_data(web::Data::new(tera))
 | 
			
		||||
            // Configure routes
 | 
			
		||||
            .configure(routes::configure_routes)
 | 
			
		||||
            // Add default handler for 404 errors
 | 
			
		||||
            .default_service(web::route().to(controllers::error::render_generic_not_found))
 | 
			
		||||
    })
 | 
			
		||||
    .bind(bind_address)?
 | 
			
		||||
    .workers(num_cpus::get())
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,6 @@ pub struct Asset {
 | 
			
		||||
    pub external_url: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Asset {
 | 
			
		||||
    /// Creates a new asset
 | 
			
		||||
    pub fn new(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,61 @@
 | 
			
		||||
// No imports needed for this module currently
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
/// Represents a calendar event
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct CalendarEvent {
 | 
			
		||||
    /// Unique identifier for the event
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    /// Title of the event
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    /// Description of the event
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    /// Start time of the event
 | 
			
		||||
    pub start_time: DateTime<Utc>,
 | 
			
		||||
    /// End time of the event
 | 
			
		||||
    pub end_time: DateTime<Utc>,
 | 
			
		||||
    /// Color of the event (hex code)
 | 
			
		||||
    pub color: String,
 | 
			
		||||
    /// Whether the event is an all-day event
 | 
			
		||||
    pub all_day: bool,
 | 
			
		||||
    /// User ID of the event creator
 | 
			
		||||
    pub user_id: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CalendarEvent {
 | 
			
		||||
    /// Creates a new calendar event
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        title: String,
 | 
			
		||||
        description: String,
 | 
			
		||||
        start_time: DateTime<Utc>,
 | 
			
		||||
        end_time: DateTime<Utc>,
 | 
			
		||||
        color: Option<String>,
 | 
			
		||||
        all_day: bool,
 | 
			
		||||
        user_id: Option<String>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id: Uuid::new_v4().to_string(),
 | 
			
		||||
            title,
 | 
			
		||||
            description,
 | 
			
		||||
            start_time,
 | 
			
		||||
            end_time,
 | 
			
		||||
            color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue
 | 
			
		||||
            all_day,
 | 
			
		||||
            user_id,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Converts the event to a JSON string
 | 
			
		||||
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
 | 
			
		||||
        serde_json::to_string(self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates an event from a JSON string
 | 
			
		||||
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
 | 
			
		||||
        serde_json::from_str(json)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a view mode for the calendar
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
			
		||||
@@ -34,4 +91,4 @@ impl CalendarViewMode {
 | 
			
		||||
            Self::Day => "day",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,99 +1,7 @@
 | 
			
		||||
#![allow(dead_code)] // Model utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
/// Contract activity types
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ContractActivityType {
 | 
			
		||||
    Created,
 | 
			
		||||
    SignerAdded,
 | 
			
		||||
    SignerRemoved,
 | 
			
		||||
    SentForSignatures,
 | 
			
		||||
    Signed,
 | 
			
		||||
    Rejected,
 | 
			
		||||
    StatusChanged,
 | 
			
		||||
    Revised,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ContractActivityType {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            ContractActivityType::Created => "Contract Created",
 | 
			
		||||
            ContractActivityType::SignerAdded => "Signer Added",
 | 
			
		||||
            ContractActivityType::SignerRemoved => "Signer Removed",
 | 
			
		||||
            ContractActivityType::SentForSignatures => "Sent for Signatures",
 | 
			
		||||
            ContractActivityType::Signed => "Contract Signed",
 | 
			
		||||
            ContractActivityType::Rejected => "Contract Rejected",
 | 
			
		||||
            ContractActivityType::StatusChanged => "Status Changed",
 | 
			
		||||
            ContractActivityType::Revised => "Contract Revised",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Contract activity model
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ContractActivity {
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    pub contract_id: u32,
 | 
			
		||||
    pub activity_type: ContractActivityType,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub user_name: String,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub metadata: Option<serde_json::Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ContractActivity {
 | 
			
		||||
    /// Creates a new contract activity
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        contract_id: u32,
 | 
			
		||||
        activity_type: ContractActivityType,
 | 
			
		||||
        description: String,
 | 
			
		||||
        user_name: String,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id: Uuid::new_v4().to_string(),
 | 
			
		||||
            contract_id,
 | 
			
		||||
            activity_type,
 | 
			
		||||
            description,
 | 
			
		||||
            user_name,
 | 
			
		||||
            created_at: Utc::now(),
 | 
			
		||||
            metadata: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a contract creation activity
 | 
			
		||||
    pub fn contract_created(contract_id: u32, contract_title: &str, user_name: &str) -> Self {
 | 
			
		||||
        Self::new(
 | 
			
		||||
            contract_id,
 | 
			
		||||
            ContractActivityType::Created,
 | 
			
		||||
            format!("Created contract '{}'", contract_title),
 | 
			
		||||
            user_name.to_string(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a signer added activity
 | 
			
		||||
    pub fn signer_added(contract_id: u32, signer_name: &str, user_name: &str) -> Self {
 | 
			
		||||
        Self::new(
 | 
			
		||||
            contract_id,
 | 
			
		||||
            ContractActivityType::SignerAdded,
 | 
			
		||||
            format!("Added signer: {}", signer_name),
 | 
			
		||||
            user_name.to_string(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a sent for signatures activity
 | 
			
		||||
    pub fn sent_for_signatures(contract_id: u32, signer_count: usize, user_name: &str) -> Self {
 | 
			
		||||
        Self::new(
 | 
			
		||||
            contract_id,
 | 
			
		||||
            ContractActivityType::SentForSignatures,
 | 
			
		||||
            format!("Sent contract for signatures to {} signer(s)", signer_count),
 | 
			
		||||
            user_name.to_string(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Contract status enum
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ContractStatus {
 | 
			
		||||
@@ -102,7 +10,7 @@ pub enum ContractStatus {
 | 
			
		||||
    Signed,
 | 
			
		||||
    Active,
 | 
			
		||||
    Expired,
 | 
			
		||||
    Cancelled,
 | 
			
		||||
    Cancelled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ContractStatus {
 | 
			
		||||
@@ -129,7 +37,7 @@ pub enum ContractType {
 | 
			
		||||
    Distribution,
 | 
			
		||||
    License,
 | 
			
		||||
    Membership,
 | 
			
		||||
    Other,
 | 
			
		||||
    Other
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ContractType {
 | 
			
		||||
@@ -153,7 +61,7 @@ impl ContractType {
 | 
			
		||||
pub enum SignerStatus {
 | 
			
		||||
    Pending,
 | 
			
		||||
    Signed,
 | 
			
		||||
    Rejected,
 | 
			
		||||
    Rejected
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SignerStatus {
 | 
			
		||||
@@ -177,7 +85,6 @@ pub struct ContractSigner {
 | 
			
		||||
    pub comments: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl ContractSigner {
 | 
			
		||||
    /// Creates a new contract signer
 | 
			
		||||
    pub fn new(name: String, email: String) -> Self {
 | 
			
		||||
@@ -216,15 +123,9 @@ pub struct ContractRevision {
 | 
			
		||||
    pub comments: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl ContractRevision {
 | 
			
		||||
    /// Creates a new contract revision
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        version: u32,
 | 
			
		||||
        content: String,
 | 
			
		||||
        created_by: String,
 | 
			
		||||
        comments: Option<String>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
    pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            version,
 | 
			
		||||
            content,
 | 
			
		||||
@@ -265,16 +166,9 @@ pub struct Contract {
 | 
			
		||||
    pub toc: Option<Vec<TocItem>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Contract {
 | 
			
		||||
    /// Creates a new contract
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        title: String,
 | 
			
		||||
        description: String,
 | 
			
		||||
        contract_type: ContractType,
 | 
			
		||||
        created_by: String,
 | 
			
		||||
        organization_id: Option<String>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
    pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id: Uuid::new_v4().to_string(),
 | 
			
		||||
            title,
 | 
			
		||||
@@ -332,9 +226,7 @@ impl Contract {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.signers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .all(|signer| signer.status == SignerStatus::Signed)
 | 
			
		||||
        self.signers.iter().all(|signer| signer.status == SignerStatus::Signed)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Marks the contract as signed if all signers have signed
 | 
			
		||||
@@ -366,26 +258,17 @@ impl Contract {
 | 
			
		||||
 | 
			
		||||
    /// Gets the number of pending signers
 | 
			
		||||
    pub fn pending_signers_count(&self) -> usize {
 | 
			
		||||
        self.signers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|s| s.status == SignerStatus::Pending)
 | 
			
		||||
            .count()
 | 
			
		||||
        self.signers.iter().filter(|s| s.status == SignerStatus::Pending).count()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the number of signed signers
 | 
			
		||||
    pub fn signed_signers_count(&self) -> usize {
 | 
			
		||||
        self.signers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|s| s.status == SignerStatus::Signed)
 | 
			
		||||
            .count()
 | 
			
		||||
        self.signers.iter().filter(|s| s.status == SignerStatus::Signed).count()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the number of rejected signers
 | 
			
		||||
    pub fn rejected_signers_count(&self) -> usize {
 | 
			
		||||
        self.signers
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|s| s.status == SignerStatus::Rejected)
 | 
			
		||||
            .count()
 | 
			
		||||
        self.signers.iter().filter(|s| s.status == SignerStatus::Rejected).count()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -413,26 +296,11 @@ impl ContractStatistics {
 | 
			
		||||
    /// Creates new contract statistics from a list of contracts
 | 
			
		||||
    pub fn new(contracts: &[Contract]) -> Self {
 | 
			
		||||
        let total_contracts = contracts.len();
 | 
			
		||||
        let draft_contracts = contracts
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|c| c.status == ContractStatus::Draft)
 | 
			
		||||
            .count();
 | 
			
		||||
        let pending_signature_contracts = contracts
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|c| c.status == ContractStatus::PendingSignatures)
 | 
			
		||||
            .count();
 | 
			
		||||
        let signed_contracts = contracts
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|c| c.status == ContractStatus::Signed)
 | 
			
		||||
            .count();
 | 
			
		||||
        let expired_contracts = contracts
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|c| c.status == ContractStatus::Expired)
 | 
			
		||||
            .count();
 | 
			
		||||
        let cancelled_contracts = contracts
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|c| c.status == ContractStatus::Cancelled)
 | 
			
		||||
            .count();
 | 
			
		||||
        let draft_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Draft).count();
 | 
			
		||||
        let pending_signature_contracts = contracts.iter().filter(|c| c.status == ContractStatus::PendingSignatures).count();
 | 
			
		||||
        let signed_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Signed).count();
 | 
			
		||||
        let expired_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Expired).count();
 | 
			
		||||
        let cancelled_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Cancelled).count();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            total_contracts,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ pub enum DefiPositionStatus {
 | 
			
		||||
    Cancelled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl DefiPositionStatus {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
@@ -36,7 +35,6 @@ pub enum DefiPositionType {
 | 
			
		||||
    Collateral,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl DefiPositionType {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
@@ -97,7 +95,6 @@ pub struct DefiDatabase {
 | 
			
		||||
    receiving_positions: HashMap<String, ReceivingPosition>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl DefiDatabase {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,254 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Model utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
/// Document type enumeration
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum DocumentType {
 | 
			
		||||
    Articles,    // Articles of Incorporation
 | 
			
		||||
    Certificate, // Business certificates
 | 
			
		||||
    License,     // Business licenses
 | 
			
		||||
    Contract,    // Contracts and agreements
 | 
			
		||||
    Financial,   // Financial documents
 | 
			
		||||
    Legal,       // Legal documents
 | 
			
		||||
    Other,       // Other documents
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for DocumentType {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        DocumentType::Other
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DocumentType {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
            DocumentType::Articles => "Articles of Incorporation",
 | 
			
		||||
            DocumentType::Certificate => "Business Certificate",
 | 
			
		||||
            DocumentType::License => "Business License",
 | 
			
		||||
            DocumentType::Contract => "Contract/Agreement",
 | 
			
		||||
            DocumentType::Financial => "Financial Document",
 | 
			
		||||
            DocumentType::Legal => "Legal Document",
 | 
			
		||||
            DocumentType::Other => "Other",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_str(s: &str) -> Self {
 | 
			
		||||
        match s {
 | 
			
		||||
            "Articles" => DocumentType::Articles,
 | 
			
		||||
            "Certificate" => DocumentType::Certificate,
 | 
			
		||||
            "License" => DocumentType::License,
 | 
			
		||||
            "Contract" => DocumentType::Contract,
 | 
			
		||||
            "Financial" => DocumentType::Financial,
 | 
			
		||||
            "Legal" => DocumentType::Legal,
 | 
			
		||||
            _ => DocumentType::Other,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn all() -> Vec<DocumentType> {
 | 
			
		||||
        vec![
 | 
			
		||||
            DocumentType::Articles,
 | 
			
		||||
            DocumentType::Certificate,
 | 
			
		||||
            DocumentType::License,
 | 
			
		||||
            DocumentType::Contract,
 | 
			
		||||
            DocumentType::Financial,
 | 
			
		||||
            DocumentType::Legal,
 | 
			
		||||
            DocumentType::Other,
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Document model for company document management
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Document {
 | 
			
		||||
    pub id: u32,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub file_path: String,
 | 
			
		||||
    pub file_size: u64,
 | 
			
		||||
    pub mime_type: String,
 | 
			
		||||
    pub company_id: u32,
 | 
			
		||||
    pub document_type: DocumentType,
 | 
			
		||||
    pub uploaded_by: String,
 | 
			
		||||
    pub upload_date: DateTime<Utc>,
 | 
			
		||||
    pub description: Option<String>,
 | 
			
		||||
    pub is_public: bool,
 | 
			
		||||
    pub checksum: Option<String>,
 | 
			
		||||
    // Template-friendly fields
 | 
			
		||||
    pub is_pdf: bool,
 | 
			
		||||
    pub is_image: bool,
 | 
			
		||||
    pub document_type_str: String,
 | 
			
		||||
    pub formatted_file_size: String,
 | 
			
		||||
    pub formatted_upload_date: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Document {
 | 
			
		||||
    /// Creates a new document (ID will be assigned by database)
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        name: String,
 | 
			
		||||
        file_path: String,
 | 
			
		||||
        file_size: u64,
 | 
			
		||||
        mime_type: String,
 | 
			
		||||
        company_id: u32,
 | 
			
		||||
        uploaded_by: String,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let upload_date = Utc::now();
 | 
			
		||||
        let is_pdf = mime_type == "application/pdf";
 | 
			
		||||
        let is_image = mime_type.starts_with("image/");
 | 
			
		||||
        let document_type = DocumentType::default();
 | 
			
		||||
        let document_type_str = document_type.as_str().to_string();
 | 
			
		||||
        let formatted_file_size = Self::format_size_bytes(file_size);
 | 
			
		||||
        let formatted_upload_date = upload_date.format("%Y-%m-%d %H:%M").to_string();
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            id: 0, // Will be assigned by database
 | 
			
		||||
            name,
 | 
			
		||||
            file_path,
 | 
			
		||||
            file_size,
 | 
			
		||||
            mime_type,
 | 
			
		||||
            company_id,
 | 
			
		||||
            document_type,
 | 
			
		||||
            uploaded_by,
 | 
			
		||||
            upload_date,
 | 
			
		||||
            description: None,
 | 
			
		||||
            is_public: false,
 | 
			
		||||
            checksum: None,
 | 
			
		||||
            is_pdf,
 | 
			
		||||
            is_image,
 | 
			
		||||
            document_type_str,
 | 
			
		||||
            formatted_file_size,
 | 
			
		||||
            formatted_upload_date,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Builder pattern methods
 | 
			
		||||
    pub fn document_type(mut self, document_type: DocumentType) -> Self {
 | 
			
		||||
        self.document_type_str = document_type.as_str().to_string();
 | 
			
		||||
        self.document_type = document_type;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn description(mut self, description: String) -> Self {
 | 
			
		||||
        self.description = Some(description);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn is_public(mut self, is_public: bool) -> Self {
 | 
			
		||||
        self.is_public = is_public;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn checksum(mut self, checksum: String) -> Self {
 | 
			
		||||
        self.checksum = Some(checksum);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the file extension from the filename
 | 
			
		||||
    pub fn file_extension(&self) -> Option<String> {
 | 
			
		||||
        std::path::Path::new(&self.name)
 | 
			
		||||
            .extension()
 | 
			
		||||
            .and_then(|ext| ext.to_str())
 | 
			
		||||
            .map(|ext| ext.to_lowercase())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Checks if the document is an image
 | 
			
		||||
    pub fn is_image(&self) -> bool {
 | 
			
		||||
        self.mime_type.starts_with("image/")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Checks if the document is a PDF
 | 
			
		||||
    pub fn is_pdf(&self) -> bool {
 | 
			
		||||
        self.mime_type == "application/pdf"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets a human-readable file size
 | 
			
		||||
    pub fn formatted_file_size(&self) -> String {
 | 
			
		||||
        let size = self.file_size as f64;
 | 
			
		||||
        if size < 1024.0 {
 | 
			
		||||
            format!("{} B", size)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} KB", size / 1024.0)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} MB", size / (1024.0 * 1024.0))
 | 
			
		||||
        } else {
 | 
			
		||||
            format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Gets the upload date formatted for display
 | 
			
		||||
    pub fn formatted_upload_date(&self) -> String {
 | 
			
		||||
        self.upload_date.format("%Y-%m-%d %H:%M").to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Static method to format file size
 | 
			
		||||
    fn format_size_bytes(bytes: u64) -> String {
 | 
			
		||||
        let size = bytes as f64;
 | 
			
		||||
        if size < 1024.0 {
 | 
			
		||||
            format!("{} B", size)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} KB", size / 1024.0)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} MB", size / (1024.0 * 1024.0))
 | 
			
		||||
        } else {
 | 
			
		||||
            format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Document statistics for dashboard
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct DocumentStatistics {
 | 
			
		||||
    pub total_documents: usize,
 | 
			
		||||
    pub total_size: u64,
 | 
			
		||||
    pub formatted_total_size: String,
 | 
			
		||||
    pub by_type: std::collections::HashMap<String, usize>,
 | 
			
		||||
    pub recent_uploads: usize, // Last 30 days
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DocumentStatistics {
 | 
			
		||||
    pub fn new(documents: &[Document]) -> Self {
 | 
			
		||||
        let mut by_type = std::collections::HashMap::new();
 | 
			
		||||
        let mut total_size = 0;
 | 
			
		||||
        let mut recent_uploads = 0;
 | 
			
		||||
 | 
			
		||||
        let thirty_days_ago = Utc::now() - chrono::Duration::days(30);
 | 
			
		||||
 | 
			
		||||
        for doc in documents {
 | 
			
		||||
            total_size += doc.file_size;
 | 
			
		||||
 | 
			
		||||
            let type_key = doc.document_type.as_str().to_string();
 | 
			
		||||
            *by_type.entry(type_key).or_insert(0) += 1;
 | 
			
		||||
 | 
			
		||||
            if doc.upload_date > thirty_days_ago {
 | 
			
		||||
                recent_uploads += 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let formatted_total_size = Self::format_size_bytes(total_size);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            total_documents: documents.len(),
 | 
			
		||||
            total_size,
 | 
			
		||||
            formatted_total_size,
 | 
			
		||||
            by_type,
 | 
			
		||||
            recent_uploads,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn formatted_total_size(&self) -> String {
 | 
			
		||||
        Self::format_size_bytes(self.total_size)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn format_size_bytes(bytes: u64) -> String {
 | 
			
		||||
        let size = bytes as f64;
 | 
			
		||||
        if size < 1024.0 {
 | 
			
		||||
            format!("{} B", size)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} KB", size / 1024.0)
 | 
			
		||||
        } else if size < 1024.0 * 1024.0 * 1024.0 {
 | 
			
		||||
            format!("{:.1} MB", size / (1024.0 * 1024.0))
 | 
			
		||||
        } else {
 | 
			
		||||
            format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -110,7 +110,6 @@ pub struct FlowStep {
 | 
			
		||||
    pub logs: Vec<FlowLog>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl FlowStep {
 | 
			
		||||
    /// Creates a new flow step
 | 
			
		||||
    pub fn new(name: String, description: String, order: u32) -> Self {
 | 
			
		||||
@@ -190,7 +189,6 @@ pub struct FlowLog {
 | 
			
		||||
    pub timestamp: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl FlowLog {
 | 
			
		||||
    /// Creates a new flow log
 | 
			
		||||
    pub fn new(message: String) -> Self {
 | 
			
		||||
@@ -233,7 +231,6 @@ pub struct Flow {
 | 
			
		||||
    pub current_step: Option<FlowStep>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Flow {
 | 
			
		||||
    /// Creates a new flow
 | 
			
		||||
    pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										248
									
								
								actix_mvc_app/src/models/governance.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								actix_mvc_app/src/models/governance.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,248 @@
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
/// Represents the status of a governance proposal
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ProposalStatus {
 | 
			
		||||
    /// Proposal is in draft status, not yet open for voting
 | 
			
		||||
    Draft,
 | 
			
		||||
    /// Proposal is active and open for voting
 | 
			
		||||
    Active,
 | 
			
		||||
    /// Proposal has been approved by the community
 | 
			
		||||
    Approved,
 | 
			
		||||
    /// Proposal has been rejected by the community
 | 
			
		||||
    Rejected,
 | 
			
		||||
    /// Proposal has been cancelled by the creator
 | 
			
		||||
    Cancelled,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Display for ProposalStatus {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            ProposalStatus::Draft => write!(f, "Draft"),
 | 
			
		||||
            ProposalStatus::Active => write!(f, "Active"),
 | 
			
		||||
            ProposalStatus::Approved => write!(f, "Approved"),
 | 
			
		||||
            ProposalStatus::Rejected => write!(f, "Rejected"),
 | 
			
		||||
            ProposalStatus::Cancelled => write!(f, "Cancelled"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a vote on a governance proposal
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum VoteType {
 | 
			
		||||
    /// Vote in favor of the proposal
 | 
			
		||||
    Yes,
 | 
			
		||||
    /// Vote against the proposal
 | 
			
		||||
    No,
 | 
			
		||||
    /// Abstain from voting on the proposal
 | 
			
		||||
    Abstain,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Display for VoteType {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            VoteType::Yes => write!(f, "Yes"),
 | 
			
		||||
            VoteType::No => write!(f, "No"),
 | 
			
		||||
            VoteType::Abstain => write!(f, "Abstain"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a governance proposal in the system
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Proposal {
 | 
			
		||||
    /// Unique identifier for the proposal
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    /// User ID of the proposal creator
 | 
			
		||||
    pub creator_id: i32,
 | 
			
		||||
    /// Name of the proposal creator
 | 
			
		||||
    pub creator_name: String,
 | 
			
		||||
    /// Title of the proposal
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    /// Detailed description of the proposal
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    /// Current status of the proposal
 | 
			
		||||
    pub status: ProposalStatus,
 | 
			
		||||
    /// Date and time when the proposal was created
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    /// Date and time when the proposal was last updated
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
    /// Date and time when voting starts
 | 
			
		||||
    pub voting_starts_at: Option<DateTime<Utc>>,
 | 
			
		||||
    /// Date and time when voting ends
 | 
			
		||||
    pub voting_ends_at: Option<DateTime<Utc>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Proposal {
 | 
			
		||||
    /// Creates a new proposal
 | 
			
		||||
    pub fn new(creator_id: i32, creator_name: String, title: String, description: String) -> Self {
 | 
			
		||||
        let now = Utc::now();
 | 
			
		||||
        Self {
 | 
			
		||||
            id: Uuid::new_v4().to_string(),
 | 
			
		||||
            creator_id,
 | 
			
		||||
            creator_name,
 | 
			
		||||
            title,
 | 
			
		||||
            description,
 | 
			
		||||
            status: ProposalStatus::Draft,
 | 
			
		||||
            created_at: now,
 | 
			
		||||
            updated_at: now,
 | 
			
		||||
            voting_starts_at: None,
 | 
			
		||||
            voting_ends_at: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Updates the proposal status
 | 
			
		||||
    pub fn update_status(&mut self, status: ProposalStatus) {
 | 
			
		||||
        self.status = status;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sets the voting period for the proposal
 | 
			
		||||
    pub fn set_voting_period(&mut self, starts_at: DateTime<Utc>, ends_at: DateTime<Utc>) {
 | 
			
		||||
        self.voting_starts_at = Some(starts_at);
 | 
			
		||||
        self.voting_ends_at = Some(ends_at);
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Activates the proposal for voting
 | 
			
		||||
    pub fn activate(&mut self) {
 | 
			
		||||
        self.status = ProposalStatus::Active;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Cancels the proposal
 | 
			
		||||
    pub fn cancel(&mut self) {
 | 
			
		||||
        self.status = ProposalStatus::Cancelled;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a vote cast on a proposal
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Vote {
 | 
			
		||||
    /// Unique identifier for the vote
 | 
			
		||||
    pub id: String,
 | 
			
		||||
    /// ID of the proposal being voted on
 | 
			
		||||
    pub proposal_id: String,
 | 
			
		||||
    /// User ID of the voter
 | 
			
		||||
    pub voter_id: i32,
 | 
			
		||||
    /// Name of the voter
 | 
			
		||||
    pub voter_name: String,
 | 
			
		||||
    /// Type of vote cast
 | 
			
		||||
    pub vote_type: VoteType,
 | 
			
		||||
    /// Optional comment explaining the vote
 | 
			
		||||
    pub comment: Option<String>,
 | 
			
		||||
    /// Date and time when the vote was cast
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    /// Date and time when the vote was last updated
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Vote {
 | 
			
		||||
    /// Creates a new vote
 | 
			
		||||
    pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option<String>) -> Self {
 | 
			
		||||
        let now = Utc::now();
 | 
			
		||||
        Self {
 | 
			
		||||
            id: Uuid::new_v4().to_string(),
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            voter_id,
 | 
			
		||||
            voter_name,
 | 
			
		||||
            vote_type,
 | 
			
		||||
            comment,
 | 
			
		||||
            created_at: now,
 | 
			
		||||
            updated_at: now,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Updates the vote type
 | 
			
		||||
    pub fn update_vote(&mut self, vote_type: VoteType, comment: Option<String>) {
 | 
			
		||||
        self.vote_type = vote_type;
 | 
			
		||||
        self.comment = comment;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a filter for searching proposals
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ProposalFilter {
 | 
			
		||||
    /// Filter by proposal status
 | 
			
		||||
    pub status: Option<String>,
 | 
			
		||||
    /// Filter by creator ID
 | 
			
		||||
    pub creator_id: Option<i32>,
 | 
			
		||||
    /// Search term for title and description
 | 
			
		||||
    pub search: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for ProposalFilter {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            status: None,
 | 
			
		||||
            creator_id: None,
 | 
			
		||||
            search: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents the voting results for a proposal
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct VotingResults {
 | 
			
		||||
    /// Proposal ID
 | 
			
		||||
    pub proposal_id: String,
 | 
			
		||||
    /// Number of yes votes
 | 
			
		||||
    pub yes_count: usize,
 | 
			
		||||
    /// Number of no votes
 | 
			
		||||
    pub no_count: usize,
 | 
			
		||||
    /// Number of abstain votes
 | 
			
		||||
    pub abstain_count: usize,
 | 
			
		||||
    /// Total number of votes
 | 
			
		||||
    pub total_votes: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VotingResults {
 | 
			
		||||
    /// Creates a new empty voting results object
 | 
			
		||||
    pub fn new(proposal_id: String) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            yes_count: 0,
 | 
			
		||||
            no_count: 0,
 | 
			
		||||
            abstain_count: 0,
 | 
			
		||||
            total_votes: 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds a vote to the results
 | 
			
		||||
    pub fn add_vote(&mut self, vote_type: &VoteType) {
 | 
			
		||||
        match vote_type {
 | 
			
		||||
            VoteType::Yes => self.yes_count += 1,
 | 
			
		||||
            VoteType::No => self.no_count += 1,
 | 
			
		||||
            VoteType::Abstain => self.abstain_count += 1,
 | 
			
		||||
        }
 | 
			
		||||
        self.total_votes += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of yes votes
 | 
			
		||||
    pub fn yes_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.yes_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of no votes
 | 
			
		||||
    pub fn no_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.no_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of abstain votes
 | 
			
		||||
    pub fn abstain_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.abstain_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
use crate::models::asset::AssetType;
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
use crate::models::asset::{Asset, AssetType};
 | 
			
		||||
 | 
			
		||||
/// Status of a marketplace listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
@@ -12,7 +12,6 @@ pub enum ListingStatus {
 | 
			
		||||
    Expired,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl ListingStatus {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
@@ -64,7 +63,6 @@ pub enum BidStatus {
 | 
			
		||||
    Cancelled,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl BidStatus {
 | 
			
		||||
    pub fn as_str(&self) -> &str {
 | 
			
		||||
        match self {
 | 
			
		||||
@@ -105,7 +103,6 @@ pub struct Listing {
 | 
			
		||||
    pub image_url: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Listing {
 | 
			
		||||
    /// Creates a new listing
 | 
			
		||||
    pub fn new(
 | 
			
		||||
@@ -153,13 +150,7 @@ impl Listing {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds a bid to the listing
 | 
			
		||||
    pub fn add_bid(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        bidder_id: String,
 | 
			
		||||
        bidder_name: String,
 | 
			
		||||
        amount: f64,
 | 
			
		||||
        currency: String,
 | 
			
		||||
    ) -> Result<(), String> {
 | 
			
		||||
    pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Listing is not active".to_string());
 | 
			
		||||
        }
 | 
			
		||||
@@ -169,10 +160,7 @@ impl Listing {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if currency != self.currency {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Currency mismatch: expected {}, got {}",
 | 
			
		||||
                self.currency, currency
 | 
			
		||||
            ));
 | 
			
		||||
            return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if bid amount is higher than current highest bid or starting price
 | 
			
		||||
@@ -205,19 +193,13 @@ impl Listing {
 | 
			
		||||
 | 
			
		||||
    /// Gets the highest bid on the listing
 | 
			
		||||
    pub fn highest_bid(&self) -> Option<&Bid> {
 | 
			
		||||
        self.bids
 | 
			
		||||
            .iter()
 | 
			
		||||
        self.bids.iter()
 | 
			
		||||
            .filter(|b| b.status == BidStatus::Active)
 | 
			
		||||
            .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Marks the listing as sold
 | 
			
		||||
    pub fn mark_as_sold(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        buyer_id: String,
 | 
			
		||||
        buyer_name: String,
 | 
			
		||||
        sale_price: f64,
 | 
			
		||||
    ) -> Result<(), String> {
 | 
			
		||||
    pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Listing is not active".to_string());
 | 
			
		||||
        }
 | 
			
		||||
@@ -275,13 +257,11 @@ impl MarketplaceStatistics {
 | 
			
		||||
        let mut listings_by_type = std::collections::HashMap::new();
 | 
			
		||||
        let mut sales_by_asset_type = std::collections::HashMap::new();
 | 
			
		||||
 | 
			
		||||
        let active_listings = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let active_listings = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Active)
 | 
			
		||||
            .count();
 | 
			
		||||
 | 
			
		||||
        let sold_listings = listings
 | 
			
		||||
            .iter()
 | 
			
		||||
        let sold_listings = listings.iter()
 | 
			
		||||
            .filter(|l| l.status == ListingStatus::Sold)
 | 
			
		||||
            .count();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
#![allow(dead_code)] // Mock user utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
/// Mock user object for development and testing
 | 
			
		||||
/// This will be replaced with real user authentication later
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct MockUser {
 | 
			
		||||
    pub id: u32,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    pub role: String,
 | 
			
		||||
    pub created_at: i64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MockUser {
 | 
			
		||||
    /// Create a new mock user
 | 
			
		||||
    pub fn new(id: u32, name: String, email: String, role: String) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id,
 | 
			
		||||
            name,
 | 
			
		||||
            email,
 | 
			
		||||
            role,
 | 
			
		||||
            created_at: chrono::Utc::now().timestamp(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// System-wide mock user constant
 | 
			
		||||
/// Use this throughout the application until real authentication is implemented
 | 
			
		||||
pub const MOCK_USER_ID: u32 = 1;
 | 
			
		||||
 | 
			
		||||
/// Get the default mock user object
 | 
			
		||||
/// This provides a consistent mock user across the entire system
 | 
			
		||||
pub fn get_mock_user() -> MockUser {
 | 
			
		||||
    MockUser::new(
 | 
			
		||||
        MOCK_USER_ID,
 | 
			
		||||
        "Mock User".to_string(),
 | 
			
		||||
        "mock@example.com".to_string(),
 | 
			
		||||
        "admin".to_string(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get mock user ID for database operations
 | 
			
		||||
/// Use this function instead of hardcoding user IDs
 | 
			
		||||
pub fn get_mock_user_id() -> u32 {
 | 
			
		||||
    MOCK_USER_ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_mock_user_creation() {
 | 
			
		||||
        let user = get_mock_user();
 | 
			
		||||
        assert_eq!(user.id, MOCK_USER_ID);
 | 
			
		||||
        assert_eq!(user.name, "Mock User");
 | 
			
		||||
        assert_eq!(user.email, "mock@example.com");
 | 
			
		||||
        assert_eq!(user.role, "admin");
 | 
			
		||||
        assert!(user.created_at > 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_mock_user_id_consistency() {
 | 
			
		||||
        assert_eq!(get_mock_user_id(), MOCK_USER_ID);
 | 
			
		||||
        assert_eq!(get_mock_user().id, MOCK_USER_ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_mock_user_immutability() {
 | 
			
		||||
        let user1 = get_mock_user();
 | 
			
		||||
        let user2 = get_mock_user();
 | 
			
		||||
 | 
			
		||||
        // Should have same ID and basic info
 | 
			
		||||
        assert_eq!(user1.id, user2.id);
 | 
			
		||||
        assert_eq!(user1.name, user2.name);
 | 
			
		||||
        assert_eq!(user1.email, user2.email);
 | 
			
		||||
        assert_eq!(user1.role, user2.role);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +1,17 @@
 | 
			
		||||
// Export models
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod defi;
 | 
			
		||||
pub mod document;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
pub mod mock_user;
 | 
			
		||||
pub mod ticket;
 | 
			
		||||
pub mod user;
 | 
			
		||||
pub mod ticket;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
pub mod governance;
 | 
			
		||||
pub mod flow;
 | 
			
		||||
pub mod contract;
 | 
			
		||||
pub mod asset;
 | 
			
		||||
pub mod marketplace;
 | 
			
		||||
pub mod defi;
 | 
			
		||||
 | 
			
		||||
// Re-export models for easier imports
 | 
			
		||||
pub use calendar::CalendarViewMode;
 | 
			
		||||
pub use defi::initialize_mock_data;
 | 
			
		||||
// Mock user exports removed - import directly from mock_user module when needed
 | 
			
		||||
pub use ticket::{Ticket, TicketComment, TicketPriority, TicketStatus};
 | 
			
		||||
pub use user::User;
 | 
			
		||||
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
 | 
			
		||||
pub use calendar::{CalendarEvent, CalendarViewMode};
 | 
			
		||||
pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
 | 
			
		||||
pub use defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB, initialize_mock_data};
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,6 @@ pub struct Ticket {
 | 
			
		||||
    pub assigned_to: Option<i32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl Ticket {
 | 
			
		||||
    /// Creates a new ticket
 | 
			
		||||
    pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ use bcrypt::{hash, verify, DEFAULT_COST};
 | 
			
		||||
 | 
			
		||||
/// Represents a user in the system
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct User {
 | 
			
		||||
    /// Unique identifier for the user
 | 
			
		||||
    pub id: Option<i32>,
 | 
			
		||||
@@ -32,7 +31,6 @@ pub enum UserRole {
 | 
			
		||||
    Admin,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl User {
 | 
			
		||||
    /// Creates a new user with default values
 | 
			
		||||
    pub fn new(name: String, email: String) -> Self {
 | 
			
		||||
@@ -127,7 +125,6 @@ impl User {
 | 
			
		||||
 | 
			
		||||
/// Represents user login credentials
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct LoginCredentials {
 | 
			
		||||
    pub email: String,
 | 
			
		||||
    pub password: String,
 | 
			
		||||
@@ -135,7 +132,6 @@ pub struct LoginCredentials {
 | 
			
		||||
 | 
			
		||||
/// Represents user registration data
 | 
			
		||||
#[derive(Debug, Deserialize)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct RegistrationData {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub email: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,28 @@
 | 
			
		||||
use crate::SESSION_KEY;
 | 
			
		||||
use crate::controllers::asset::AssetController;
 | 
			
		||||
use crate::controllers::auth::AuthController;
 | 
			
		||||
use crate::controllers::calendar::CalendarController;
 | 
			
		||||
use crate::controllers::company::CompanyController;
 | 
			
		||||
use crate::controllers::contract::ContractController;
 | 
			
		||||
use crate::controllers::defi::DefiController;
 | 
			
		||||
use crate::controllers::document::DocumentController;
 | 
			
		||||
use crate::controllers::flow::FlowController;
 | 
			
		||||
use crate::controllers::governance::GovernanceController;
 | 
			
		||||
use crate::controllers::home::HomeController;
 | 
			
		||||
use crate::controllers::marketplace::MarketplaceController;
 | 
			
		||||
use crate::controllers::payment::PaymentController;
 | 
			
		||||
use crate::controllers::ticket::TicketController;
 | 
			
		||||
use crate::middleware::JwtAuth;
 | 
			
		||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
 | 
			
		||||
use actix_web::web;
 | 
			
		||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
 | 
			
		||||
use crate::controllers::home::HomeController;
 | 
			
		||||
use crate::controllers::auth::AuthController;
 | 
			
		||||
use crate::controllers::ticket::TicketController;
 | 
			
		||||
use crate::controllers::calendar::CalendarController;
 | 
			
		||||
use crate::controllers::governance::GovernanceController;
 | 
			
		||||
use crate::controllers::flow::FlowController;
 | 
			
		||||
use crate::controllers::contract::ContractController;
 | 
			
		||||
use crate::controllers::asset::AssetController;
 | 
			
		||||
use crate::controllers::marketplace::MarketplaceController;
 | 
			
		||||
use crate::controllers::defi::DefiController;
 | 
			
		||||
use crate::controllers::company::CompanyController;
 | 
			
		||||
use crate::middleware::JwtAuth;
 | 
			
		||||
use crate::SESSION_KEY;
 | 
			
		||||
 | 
			
		||||
/// Configures all application routes
 | 
			
		||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
    // Configure health check routes (no authentication required)
 | 
			
		||||
    crate::controllers::health::configure_health_routes(cfg);
 | 
			
		||||
 | 
			
		||||
    // Configure session middleware with the consistent key
 | 
			
		||||
    let session_middleware =
 | 
			
		||||
        SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
 | 
			
		||||
            .cookie_secure(false) // Set to true in production with HTTPS
 | 
			
		||||
            .build();
 | 
			
		||||
    let session_middleware = SessionMiddleware::builder(
 | 
			
		||||
        CookieSessionStore::default(),
 | 
			
		||||
        SESSION_KEY.clone()
 | 
			
		||||
    )
 | 
			
		||||
    .cookie_secure(false) // Set to true in production with HTTPS
 | 
			
		||||
    .build();
 | 
			
		||||
 | 
			
		||||
    // Public routes that don't require authentication
 | 
			
		||||
    cfg.service(
 | 
			
		||||
@@ -36,187 +33,67 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
            .route("/about", web::get().to(HomeController::about))
 | 
			
		||||
            .route("/contact", web::get().to(HomeController::contact))
 | 
			
		||||
            .route("/contact", web::post().to(HomeController::submit_contact))
 | 
			
		||||
            
 | 
			
		||||
            // Auth routes
 | 
			
		||||
            .route("/login", web::get().to(AuthController::login_page))
 | 
			
		||||
            .route("/login", web::post().to(AuthController::login))
 | 
			
		||||
            .route("/register", web::get().to(AuthController::register_page))
 | 
			
		||||
            .route("/register", web::post().to(AuthController::register))
 | 
			
		||||
            .route("/logout", web::get().to(AuthController::logout))
 | 
			
		||||
            
 | 
			
		||||
            // Protected routes that require authentication
 | 
			
		||||
            // These routes will be protected by the JwtAuth middleware in the main.rs file
 | 
			
		||||
            .route("/editor", web::get().to(HomeController::editor))
 | 
			
		||||
            
 | 
			
		||||
            // Ticket routes
 | 
			
		||||
            .route("/tickets", web::get().to(TicketController::list_tickets))
 | 
			
		||||
            .route("/tickets/new", web::get().to(TicketController::new_ticket))
 | 
			
		||||
            .route("/tickets", web::post().to(TicketController::create_ticket))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/tickets/{id}",
 | 
			
		||||
                web::get().to(TicketController::show_ticket),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/tickets/{id}/comment",
 | 
			
		||||
                web::post().to(TicketController::add_comment),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/tickets/{id}/status/{status}",
 | 
			
		||||
                web::post().to(TicketController::update_status),
 | 
			
		||||
            )
 | 
			
		||||
            .route("/tickets/{id}", web::get().to(TicketController::show_ticket))
 | 
			
		||||
            .route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
 | 
			
		||||
            .route("/tickets/{id}/status/{status}", web::post().to(TicketController::update_status))
 | 
			
		||||
            .route("/my-tickets", web::get().to(TicketController::my_tickets))
 | 
			
		||||
            
 | 
			
		||||
            // Calendar routes
 | 
			
		||||
            .route("/calendar", web::get().to(CalendarController::calendar))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/calendar/events/new",
 | 
			
		||||
                web::get().to(CalendarController::new_event),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/calendar/events",
 | 
			
		||||
                web::post().to(CalendarController::create_event),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/calendar/events/{id}/delete",
 | 
			
		||||
                web::post().to(CalendarController::delete_event),
 | 
			
		||||
            )
 | 
			
		||||
            .route("/calendar/events/new", web::get().to(CalendarController::new_event))
 | 
			
		||||
            .route("/calendar/events", web::post().to(CalendarController::create_event))
 | 
			
		||||
            .route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
 | 
			
		||||
            
 | 
			
		||||
            // Governance routes
 | 
			
		||||
            .route("/governance", web::get().to(GovernanceController::index))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/proposals",
 | 
			
		||||
                web::get().to(GovernanceController::proposals),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/proposals/{id}",
 | 
			
		||||
                web::get().to(GovernanceController::proposal_detail),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/proposals/{id}/vote",
 | 
			
		||||
                web::post().to(GovernanceController::submit_vote),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/create",
 | 
			
		||||
                web::get().to(GovernanceController::create_proposal_form),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/create",
 | 
			
		||||
                web::post().to(GovernanceController::submit_proposal),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/my-votes",
 | 
			
		||||
                web::get().to(GovernanceController::my_votes),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/governance/activities",
 | 
			
		||||
                web::get().to(GovernanceController::all_activities),
 | 
			
		||||
            )
 | 
			
		||||
            .route("/governance/proposals", web::get().to(GovernanceController::proposals))
 | 
			
		||||
            .route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
 | 
			
		||||
            .route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
 | 
			
		||||
            .route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
 | 
			
		||||
            .route("/governance/create", web::post().to(GovernanceController::submit_proposal))
 | 
			
		||||
            .route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
 | 
			
		||||
            
 | 
			
		||||
            // Flow routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/flows")
 | 
			
		||||
                    .route("", web::get().to(FlowController::index))
 | 
			
		||||
                    .route("/list", web::get().to(FlowController::list_flows))
 | 
			
		||||
                    .route("/{id}", web::get().to(FlowController::flow_detail))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/advance",
 | 
			
		||||
                        web::post().to(FlowController::advance_flow_step),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/stuck",
 | 
			
		||||
                        web::post().to(FlowController::mark_flow_step_stuck),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/step/{step_id}/log",
 | 
			
		||||
                        web::post().to(FlowController::add_log_to_flow_step),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/{id}/advance", web::post().to(FlowController::advance_flow_step))
 | 
			
		||||
                    .route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck))
 | 
			
		||||
                    .route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step))
 | 
			
		||||
                    .route("/create", web::get().to(FlowController::create_flow_form))
 | 
			
		||||
                    .route("/create", web::post().to(FlowController::create_flow))
 | 
			
		||||
                    .route("/my-flows", web::get().to(FlowController::my_flows)),
 | 
			
		||||
                    .route("/my-flows", web::get().to(FlowController::my_flows))
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            // Contract routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/contracts")
 | 
			
		||||
                    .route("", web::get().to(ContractController::index))
 | 
			
		||||
                    .route("/", web::get().to(ContractController::index)) // Handle trailing slash
 | 
			
		||||
                    .route("/list", web::get().to(ContractController::list))
 | 
			
		||||
                    .route("/list/", web::get().to(ContractController::list)) // Handle trailing slash
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/my-contracts",
 | 
			
		||||
                        web::get().to(ContractController::my_contracts),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/my-contracts/",
 | 
			
		||||
                        web::get().to(ContractController::my_contracts),
 | 
			
		||||
                    ) // Handle trailing slash
 | 
			
		||||
                    .route("/create", web::get().to(ContractController::create_form))
 | 
			
		||||
                    .route("/create/", web::get().to(ContractController::create_form)) // Handle trailing slash
 | 
			
		||||
                    .route("/create", web::post().to(ContractController::create))
 | 
			
		||||
                    .route("/create/", web::post().to(ContractController::create)) // Handle trailing slash
 | 
			
		||||
                    .route("/statistics", web::get().to(ContractController::statistics))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/activities",
 | 
			
		||||
                        web::get().to(ContractController::all_activities),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/{id}/edit", web::get().to(ContractController::edit_form))
 | 
			
		||||
                    .route("/{id}/edit", web::post().to(ContractController::update))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/filter/{status}",
 | 
			
		||||
                        web::get().to(ContractController::filter_by_status),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/my", web::get().to(ContractController::my_contracts))
 | 
			
		||||
                    .route("/{id}", web::get().to(ContractController::detail))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/status/{status}",
 | 
			
		||||
                        web::post().to(ContractController::update_status),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/{id}/delete", web::post().to(ContractController::delete))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/add-signer",
 | 
			
		||||
                        web::get().to(ContractController::add_signer_form),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/add-signer",
 | 
			
		||||
                        web::post().to(ContractController::add_signer),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/remind",
 | 
			
		||||
                        web::post().to(ContractController::remind_to_sign),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/send",
 | 
			
		||||
                        web::post().to(ContractController::send_for_signatures),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/reminder-status",
 | 
			
		||||
                        web::get().to(ContractController::get_reminder_status),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/add-revision",
 | 
			
		||||
                        web::post().to(ContractController::add_revision),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/signer/{signer_id}/status/{status}",
 | 
			
		||||
                        web::post().to(ContractController::update_signer_status),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/sign/{signer_id}",
 | 
			
		||||
                        web::post().to(ContractController::sign_contract),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/reject/{signer_id}",
 | 
			
		||||
                        web::post().to(ContractController::reject_contract),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/cancel",
 | 
			
		||||
                        web::post().to(ContractController::cancel_contract),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/clone",
 | 
			
		||||
                        web::post().to(ContractController::clone_contract),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/signed/{signer_id}",
 | 
			
		||||
                        web::get().to(ContractController::view_signed_document),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/share",
 | 
			
		||||
                        web::post().to(ContractController::share_contract),
 | 
			
		||||
                    ),
 | 
			
		||||
                    .route("/create", web::get().to(ContractController::create_form))
 | 
			
		||||
                    .route("/create", web::post().to(ContractController::create))
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            // Asset routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/assets")
 | 
			
		||||
@@ -227,113 +104,49 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
                    .route("/create", web::post().to(AssetController::create))
 | 
			
		||||
                    .route("/test", web::get().to(AssetController::test))
 | 
			
		||||
                    .route("/{id}", web::get().to(AssetController::detail))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/valuation",
 | 
			
		||||
                        web::post().to(AssetController::add_valuation),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/transaction",
 | 
			
		||||
                        web::post().to(AssetController::add_transaction),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/status/{status}",
 | 
			
		||||
                        web::post().to(AssetController::update_status),
 | 
			
		||||
                    ),
 | 
			
		||||
                    .route("/{id}/valuation", web::post().to(AssetController::add_valuation))
 | 
			
		||||
                    .route("/{id}/transaction", web::post().to(AssetController::add_transaction))
 | 
			
		||||
                    .route("/{id}/status/{status}", web::post().to(AssetController::update_status))
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            // Marketplace routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/marketplace")
 | 
			
		||||
                    .route("", web::get().to(MarketplaceController::index))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/listings",
 | 
			
		||||
                        web::get().to(MarketplaceController::list_listings),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/listings", web::get().to(MarketplaceController::list_listings))
 | 
			
		||||
                    .route("/my", web::get().to(MarketplaceController::my_listings))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/create",
 | 
			
		||||
                        web::get().to(MarketplaceController::create_listing_form),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/create",
 | 
			
		||||
                        web::post().to(MarketplaceController::create_listing),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}",
 | 
			
		||||
                        web::get().to(MarketplaceController::listing_detail),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/bid",
 | 
			
		||||
                        web::post().to(MarketplaceController::submit_bid),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/purchase",
 | 
			
		||||
                        web::post().to(MarketplaceController::purchase_listing),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/{id}/cancel",
 | 
			
		||||
                        web::post().to(MarketplaceController::cancel_listing),
 | 
			
		||||
                    ),
 | 
			
		||||
                    .route("/create", web::get().to(MarketplaceController::create_listing_form))
 | 
			
		||||
                    .route("/create", web::post().to(MarketplaceController::create_listing))
 | 
			
		||||
                    .route("/{id}", web::get().to(MarketplaceController::listing_detail))
 | 
			
		||||
                    .route("/{id}/bid", web::post().to(MarketplaceController::submit_bid))
 | 
			
		||||
                    .route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing))
 | 
			
		||||
                    .route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing))
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            // DeFi routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/defi")
 | 
			
		||||
                    .route("", web::get().to(DefiController::index))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/providing",
 | 
			
		||||
                        web::post().to(DefiController::create_providing),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/receiving",
 | 
			
		||||
                        web::post().to(DefiController::create_receiving),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route("/providing", web::post().to(DefiController::create_providing))
 | 
			
		||||
                    .route("/receiving", web::post().to(DefiController::create_receiving))
 | 
			
		||||
                    .route("/liquidity", web::post().to(DefiController::add_liquidity))
 | 
			
		||||
                    .route("/staking", web::post().to(DefiController::create_staking))
 | 
			
		||||
                    .route("/swap", web::post().to(DefiController::swap_tokens))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/collateral",
 | 
			
		||||
                        web::post().to(DefiController::create_collateral),
 | 
			
		||||
                    ),
 | 
			
		||||
                    .route("/collateral", web::post().to(DefiController::create_collateral))
 | 
			
		||||
            )
 | 
			
		||||
            // Company routes
 | 
			
		||||
            .service(
 | 
			
		||||
                web::scope("/company")
 | 
			
		||||
                    .route("", web::get().to(CompanyController::index))
 | 
			
		||||
                    // OLD REGISTRATION ROUTE REMOVED - Now only payment flow creates companies
 | 
			
		||||
                    .route("/register", web::post().to(CompanyController::register))
 | 
			
		||||
                    .route("/view/{id}", web::get().to(CompanyController::view_company))
 | 
			
		||||
                    .route("/edit/{id}", web::get().to(CompanyController::edit_form))
 | 
			
		||||
                    .route("/edit/{id}", web::post().to(CompanyController::edit))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/switch/{id}",
 | 
			
		||||
                        web::get().to(CompanyController::switch_entity),
 | 
			
		||||
                    )
 | 
			
		||||
                    // Payment routes - ONLY way to create companies now
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/create-payment-intent",
 | 
			
		||||
                        web::post().to(PaymentController::create_payment_intent),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/payment-success",
 | 
			
		||||
                        web::get().to(PaymentController::payment_success),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/payment-webhook",
 | 
			
		||||
                        web::post().to(PaymentController::webhook),
 | 
			
		||||
                    )
 | 
			
		||||
                    // Document management routes
 | 
			
		||||
                    .route("/documents/{id}", web::get().to(DocumentController::index))
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/documents/{id}/upload",
 | 
			
		||||
                        web::post().to(DocumentController::upload),
 | 
			
		||||
                    )
 | 
			
		||||
                    .route(
 | 
			
		||||
                        "/documents/{company_id}/delete/{document_id}",
 | 
			
		||||
                        web::get().to(DocumentController::delete),
 | 
			
		||||
                    ),
 | 
			
		||||
            ),
 | 
			
		||||
                    .route("/switch/{id}", web::get().to(CompanyController::switch_entity))
 | 
			
		||||
            )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Keep the /protected scope for any future routes that should be under that path
 | 
			
		||||
    cfg.service(
 | 
			
		||||
        web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware
 | 
			
		||||
        web::scope("/protected")
 | 
			
		||||
            .wrap(JwtAuth)  // Apply JWT authentication middleware
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,20 +1,16 @@
 | 
			
		||||
use actix_web::{Error, HttpResponse};
 | 
			
		||||
use actix_web::{error, Error, HttpResponse};
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use pulldown_cmark::{Options, Parser, html};
 | 
			
		||||
use std::error::Error as StdError;
 | 
			
		||||
use tera::{self, Context, Function, Tera, Value};
 | 
			
		||||
use std::error::Error as StdError;
 | 
			
		||||
 | 
			
		||||
// Export modules
 | 
			
		||||
pub mod redis_service;
 | 
			
		||||
pub mod secure_logging;
 | 
			
		||||
pub mod stripe_security;
 | 
			
		||||
 | 
			
		||||
// Re-export for easier imports
 | 
			
		||||
// pub use redis_service::RedisCalendarService; // Currently unused
 | 
			
		||||
pub use redis_service::RedisCalendarService;
 | 
			
		||||
 | 
			
		||||
/// Error type for template rendering
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub struct TemplateError {
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub details: String,
 | 
			
		||||
@@ -29,16 +25,10 @@ impl std::fmt::Display for TemplateError {
 | 
			
		||||
 | 
			
		||||
impl std::error::Error for TemplateError {}
 | 
			
		||||
 | 
			
		||||
/// Registers custom Tera functions and filters
 | 
			
		||||
/// Registers custom Tera functions
 | 
			
		||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
 | 
			
		||||
    tera.register_function("now", NowFunction);
 | 
			
		||||
    tera.register_function("format_date", FormatDateFunction);
 | 
			
		||||
    tera.register_function("local_time", LocalTimeFunction);
 | 
			
		||||
 | 
			
		||||
    // Register custom filters
 | 
			
		||||
    tera.register_filter("format_hour", format_hour_filter);
 | 
			
		||||
    tera.register_filter("extract_hour", extract_hour_filter);
 | 
			
		||||
    tera.register_filter("format_time", format_time_filter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera function to get the current date/time
 | 
			
		||||
@@ -56,7 +46,7 @@ impl Function for NowFunction {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let now = Utc::now();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Special case for just getting the year
 | 
			
		||||
        if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) {
 | 
			
		||||
            return Ok(Value::String(now.format("%Y").to_string()));
 | 
			
		||||
@@ -78,10 +68,14 @@ impl Function for FormatDateFunction {
 | 
			
		||||
                None => {
 | 
			
		||||
                    return Err(tera::Error::msg(
 | 
			
		||||
                        "The 'timestamp' argument must be a valid timestamp",
 | 
			
		||||
                    ));
 | 
			
		||||
                    ))
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => return Err(tera::Error::msg("The 'timestamp' argument is required")),
 | 
			
		||||
            None => {
 | 
			
		||||
                return Err(tera::Error::msg(
 | 
			
		||||
                    "The 'timestamp' argument is required",
 | 
			
		||||
                ))
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let format = match args.get("format") {
 | 
			
		||||
@@ -95,130 +89,23 @@ impl Function for FormatDateFunction {
 | 
			
		||||
        // Convert timestamp to DateTime using the non-deprecated method
 | 
			
		||||
        let datetime = match DateTime::from_timestamp(timestamp, 0) {
 | 
			
		||||
            Some(dt) => dt,
 | 
			
		||||
            None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")),
 | 
			
		||||
            None => {
 | 
			
		||||
                return Err(tera::Error::msg(
 | 
			
		||||
                    "Failed to convert timestamp to datetime",
 | 
			
		||||
                ))
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(Value::String(datetime.format(format).to_string()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera function to convert UTC datetime to local time
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct LocalTimeFunction;
 | 
			
		||||
 | 
			
		||||
impl Function for LocalTimeFunction {
 | 
			
		||||
    fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
 | 
			
		||||
        let datetime_value = match args.get("datetime") {
 | 
			
		||||
            Some(val) => val,
 | 
			
		||||
            None => return Err(tera::Error::msg("The 'datetime' argument is required")),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let format = match args.get("format") {
 | 
			
		||||
            Some(val) => match val.as_str() {
 | 
			
		||||
                Some(s) => s,
 | 
			
		||||
                None => "%Y-%m-%d %H:%M",
 | 
			
		||||
            },
 | 
			
		||||
            None => "%Y-%m-%d %H:%M",
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // The datetime comes from Rust as a serialized DateTime<Utc>
 | 
			
		||||
        // We need to handle it properly
 | 
			
		||||
        let utc_datetime = if let Some(dt_str) = datetime_value.as_str() {
 | 
			
		||||
            // Try to parse as RFC3339 first
 | 
			
		||||
            match DateTime::parse_from_rfc3339(dt_str) {
 | 
			
		||||
                Ok(dt) => dt.with_timezone(&Utc),
 | 
			
		||||
                Err(_) => {
 | 
			
		||||
                    // Try to parse as our standard format
 | 
			
		||||
                    match DateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
 | 
			
		||||
                        Ok(dt) => dt.with_timezone(&Utc),
 | 
			
		||||
                        Err(_) => return Err(tera::Error::msg("Invalid datetime string format")),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            return Err(tera::Error::msg("Datetime must be a string"));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Convert UTC to local time (EEST = UTC+3)
 | 
			
		||||
        // In a real application, you'd want to get the user's timezone from their profile
 | 
			
		||||
        let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
 | 
			
		||||
        let local_datetime = utc_datetime.with_timezone(&local_offset);
 | 
			
		||||
 | 
			
		||||
        Ok(Value::String(local_datetime.format(format).to_string()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to format hour with zero padding
 | 
			
		||||
pub fn format_hour_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    _args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    match value.as_i64() {
 | 
			
		||||
        Some(hour) => Ok(Value::String(format!("{:02}", hour))),
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a number")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to extract hour from datetime string
 | 
			
		||||
pub fn extract_hour_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    _args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    match value.as_str() {
 | 
			
		||||
        Some(datetime_str) => {
 | 
			
		||||
            // Try to parse as RFC3339 first
 | 
			
		||||
            if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
 | 
			
		||||
                Ok(Value::String(dt.format("%H").to_string()))
 | 
			
		||||
            } else {
 | 
			
		||||
                // Try to parse as our standard format
 | 
			
		||||
                match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
 | 
			
		||||
                    Ok(dt) => Ok(Value::String(dt.format("%H").to_string())),
 | 
			
		||||
                    Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a string")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to format time from datetime string
 | 
			
		||||
pub fn format_time_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    let format = match args.get("format") {
 | 
			
		||||
        Some(val) => match val.as_str() {
 | 
			
		||||
            Some(s) => s,
 | 
			
		||||
            None => "%H:%M",
 | 
			
		||||
        },
 | 
			
		||||
        None => "%H:%M",
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match value.as_str() {
 | 
			
		||||
        Some(datetime_str) => {
 | 
			
		||||
            // Try to parse as RFC3339 first
 | 
			
		||||
            if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
 | 
			
		||||
                Ok(Value::String(dt.format(format).to_string()))
 | 
			
		||||
            } else {
 | 
			
		||||
                // Try to parse as our standard format
 | 
			
		||||
                match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
 | 
			
		||||
                    Ok(dt) => Ok(Value::String(dt.format(format).to_string())),
 | 
			
		||||
                    Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a string")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Formats a date for display
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
 | 
			
		||||
    date.format(format).to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Truncates a string to a maximum length and adds an ellipsis if truncated
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub fn truncate_string(s: &str, max_length: usize) -> String {
 | 
			
		||||
    if s.len() <= max_length {
 | 
			
		||||
        s.to_string()
 | 
			
		||||
@@ -227,26 +114,6 @@ pub fn truncate_string(s: &str, max_length: usize) -> String {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Parses markdown content and returns HTML
 | 
			
		||||
pub fn parse_markdown(markdown_content: &str) -> String {
 | 
			
		||||
    // Set up markdown parser options
 | 
			
		||||
    let mut options = Options::empty();
 | 
			
		||||
    options.insert(Options::ENABLE_TABLES);
 | 
			
		||||
    options.insert(Options::ENABLE_FOOTNOTES);
 | 
			
		||||
    options.insert(Options::ENABLE_STRIKETHROUGH);
 | 
			
		||||
    options.insert(Options::ENABLE_TASKLISTS);
 | 
			
		||||
    options.insert(Options::ENABLE_SMART_PUNCTUATION);
 | 
			
		||||
 | 
			
		||||
    // Create parser
 | 
			
		||||
    let parser = Parser::new_ext(markdown_content, options);
 | 
			
		||||
 | 
			
		||||
    // Render to HTML
 | 
			
		||||
    let mut html_output = String::new();
 | 
			
		||||
    html::push_html(&mut html_output, parser);
 | 
			
		||||
 | 
			
		||||
    html_output
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Renders a template with error handling
 | 
			
		||||
///
 | 
			
		||||
/// This function attempts to render a template and handles any errors by rendering
 | 
			
		||||
@@ -257,41 +124,38 @@ pub fn render_template(
 | 
			
		||||
    ctx: &Context,
 | 
			
		||||
) -> Result<HttpResponse, Error> {
 | 
			
		||||
    println!("DEBUG: Attempting to render template: {}", template_name);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Print all context keys for debugging
 | 
			
		||||
    let mut keys = Vec::new();
 | 
			
		||||
    for (key, _) in ctx.clone().into_json().as_object().unwrap().iter() {
 | 
			
		||||
        keys.push(key.clone());
 | 
			
		||||
    }
 | 
			
		||||
    println!("DEBUG: Context keys: {:?}", keys);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    match tmpl.render(template_name, ctx) {
 | 
			
		||||
        Ok(content) => {
 | 
			
		||||
            println!("DEBUG: Successfully rendered template: {}", template_name);
 | 
			
		||||
            Ok(HttpResponse::Ok().content_type("text/html").body(content))
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            // Log the error with more details
 | 
			
		||||
            println!(
 | 
			
		||||
                "DEBUG: Template rendering error for {}: {}",
 | 
			
		||||
                template_name, e
 | 
			
		||||
            );
 | 
			
		||||
            println!("DEBUG: Template rendering error for {}: {}", template_name, e);
 | 
			
		||||
            println!("DEBUG: Error details: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Print the error cause chain for better debugging
 | 
			
		||||
            let mut current_error: Option<&dyn StdError> = Some(&e);
 | 
			
		||||
            let mut error_chain = Vec::new();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            while let Some(error) = current_error {
 | 
			
		||||
                error_chain.push(format!("{}", error));
 | 
			
		||||
                current_error = error.source();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            println!("DEBUG: Error chain: {:?}", error_chain);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Log the error
 | 
			
		||||
            log::error!("Template rendering error: {}", e);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Create a simple error response with more detailed information
 | 
			
		||||
            let error_html = format!(
 | 
			
		||||
                r#"<!DOCTYPE html>
 | 
			
		||||
@@ -323,9 +187,9 @@ pub fn render_template(
 | 
			
		||||
                e,
 | 
			
		||||
                error_chain.join("\n")
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            println!("DEBUG: Returning simple error page");
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            Ok(HttpResponse::InternalServerError()
 | 
			
		||||
                .content_type("text/html")
 | 
			
		||||
                .body(error_html))
 | 
			
		||||
@@ -343,4 +207,4 @@ mod tests {
 | 
			
		||||
        assert_eq!(truncate_string("Hello, world!", 5), "Hello...");
 | 
			
		||||
        assert_eq!(truncate_string("", 5), "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
#![allow(dead_code)] // Redis utility functions may not all be used yet
 | 
			
		||||
 | 
			
		||||
use heromodels::models::Event as CalendarEvent;
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use redis::{Client, Commands, Connection, RedisError};
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use crate::models::CalendarEvent;
 | 
			
		||||
 | 
			
		||||
// Create a lazy static Redis client that can be used throughout the application
 | 
			
		||||
lazy_static! {
 | 
			
		||||
@@ -13,21 +11,21 @@ lazy_static! {
 | 
			
		||||
/// Initialize the Redis client
 | 
			
		||||
pub fn init_redis_client(redis_url: &str) -> Result<(), RedisError> {
 | 
			
		||||
    let client = redis::Client::open(redis_url)?;
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Test the connection
 | 
			
		||||
    let _: Connection = client.get_connection()?;
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Store the client in the lazy static
 | 
			
		||||
    let mut client_guard = REDIS_CLIENT.lock().unwrap();
 | 
			
		||||
    *client_guard = Some(client);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get a Redis connection
 | 
			
		||||
pub fn get_connection() -> Result<Connection, RedisError> {
 | 
			
		||||
    let client_guard = REDIS_CLIENT.lock().unwrap();
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    if let Some(client) = &*client_guard {
 | 
			
		||||
        client.get_connection()
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -44,14 +42,14 @@ pub struct RedisCalendarService;
 | 
			
		||||
impl RedisCalendarService {
 | 
			
		||||
    /// Key prefix for calendar events
 | 
			
		||||
    const EVENT_KEY_PREFIX: &'static str = "calendar:event:";
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Key for the set of all event IDs
 | 
			
		||||
    const ALL_EVENTS_KEY: &'static str = "calendar:all_events";
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Save a calendar event to Redis
 | 
			
		||||
    pub fn save_event(event: &CalendarEvent) -> Result<(), RedisError> {
 | 
			
		||||
        let mut conn = get_connection()?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Convert the event to JSON
 | 
			
		||||
        let json = event.to_json().map_err(|e| {
 | 
			
		||||
            RedisError::from(std::io::Error::new(
 | 
			
		||||
@@ -59,25 +57,25 @@ impl RedisCalendarService {
 | 
			
		||||
                format!("Failed to serialize event: {}", e),
 | 
			
		||||
            ))
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Save the event
 | 
			
		||||
        let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.base_data.id);
 | 
			
		||||
        let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id);
 | 
			
		||||
        let _: () = conn.set(event_key, json)?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Add the event ID to the set of all events
 | 
			
		||||
        let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.base_data.id)?;
 | 
			
		||||
 | 
			
		||||
        let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.id)?;
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Get a calendar event from Redis by ID
 | 
			
		||||
    pub fn get_event(id: &str) -> Result<Option<CalendarEvent>, RedisError> {
 | 
			
		||||
        let mut conn = get_connection()?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get the event JSON
 | 
			
		||||
        let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id);
 | 
			
		||||
        let json: Option<String> = conn.get(event_key)?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Parse the JSON
 | 
			
		||||
        if let Some(json) = json {
 | 
			
		||||
            let event = CalendarEvent::from_json(&json).map_err(|e| {
 | 
			
		||||
@@ -86,34 +84,34 @@ impl RedisCalendarService {
 | 
			
		||||
                    format!("Failed to deserialize event: {}", e),
 | 
			
		||||
                ))
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            Ok(Some(event))
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(None)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Delete a calendar event from Redis
 | 
			
		||||
    pub fn delete_event(id: &str) -> Result<bool, RedisError> {
 | 
			
		||||
        let mut conn = get_connection()?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Delete the event
 | 
			
		||||
        let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id);
 | 
			
		||||
        let deleted: i32 = conn.del(event_key)?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Remove the event ID from the set of all events
 | 
			
		||||
        let _: () = conn.srem(Self::ALL_EVENTS_KEY, id)?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(deleted > 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Get all calendar events from Redis
 | 
			
		||||
    pub fn get_all_events() -> Result<Vec<CalendarEvent>, RedisError> {
 | 
			
		||||
        let mut conn = get_connection()?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get all event IDs
 | 
			
		||||
        let event_ids: Vec<String> = conn.smembers(Self::ALL_EVENTS_KEY)?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Get all events
 | 
			
		||||
        let mut events = Vec::new();
 | 
			
		||||
        for id in event_ids {
 | 
			
		||||
@@ -121,23 +119,23 @@ impl RedisCalendarService {
 | 
			
		||||
                events.push(event);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(events)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    /// Get events for a specific date range
 | 
			
		||||
    pub fn get_events_in_range(
 | 
			
		||||
        start: chrono::DateTime<chrono::Utc>,
 | 
			
		||||
        end: chrono::DateTime<chrono::Utc>,
 | 
			
		||||
    ) -> Result<Vec<CalendarEvent>, RedisError> {
 | 
			
		||||
        let all_events = Self::get_all_events()?;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Filter events that fall within the date range
 | 
			
		||||
        let filtered_events = all_events
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|event| event.start_time <= end && event.end_time >= start)
 | 
			
		||||
            .collect();
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        Ok(filtered_events)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,315 +0,0 @@
 | 
			
		||||
use serde_json::json;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
/// Secure logging utilities that prevent sensitive data exposure
 | 
			
		||||
pub struct SecureLogger;
 | 
			
		||||
 | 
			
		||||
impl SecureLogger {
 | 
			
		||||
    /// Log payment events without exposing sensitive data
 | 
			
		||||
    pub fn log_payment_event(event: &str, payment_id: &str, success: bool, details: Option<&str>) {
 | 
			
		||||
        if success {
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Payment event: {} for payment ID: {} - SUCCESS{}",
 | 
			
		||||
                event,
 | 
			
		||||
                Self::sanitize_payment_id(payment_id),
 | 
			
		||||
                details.map(|d| format!(" ({})", d)).unwrap_or_default()
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            log::error!(
 | 
			
		||||
                "Payment event: {} for payment ID: {} - FAILED{}",
 | 
			
		||||
                event,
 | 
			
		||||
                Self::sanitize_payment_id(payment_id),
 | 
			
		||||
                details.map(|d| format!(" ({})", d)).unwrap_or_default()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log security events with IP tracking
 | 
			
		||||
    pub fn log_security_event(event: &str, ip: &str, success: bool, details: Option<&str>) {
 | 
			
		||||
        let status = if success { "ALLOWED" } else { "BLOCKED" };
 | 
			
		||||
        log::warn!(
 | 
			
		||||
            "Security event: {} from IP: {} - {}{}",
 | 
			
		||||
            event,
 | 
			
		||||
            Self::sanitize_ip(ip),
 | 
			
		||||
            status,
 | 
			
		||||
            details.map(|d| format!(" ({})", d)).unwrap_or_default()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log webhook events securely
 | 
			
		||||
    pub fn log_webhook_event(event_type: &str, success: bool, payment_intent_id: Option<&str>) {
 | 
			
		||||
        let payment_info = payment_intent_id
 | 
			
		||||
            .map(|id| format!(" for payment {}", Self::sanitize_payment_id(id)))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        if success {
 | 
			
		||||
            log::info!("Webhook event: {} - SUCCESS{}", event_type, payment_info);
 | 
			
		||||
        } else {
 | 
			
		||||
            log::error!("Webhook event: {} - FAILED{}", event_type, payment_info);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log company registration events
 | 
			
		||||
    pub fn log_company_event(event: &str, company_id: u32, company_name: &str, success: bool) {
 | 
			
		||||
        let sanitized_name = Self::sanitize_company_name(company_name);
 | 
			
		||||
        if success {
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Company event: {} for company ID: {} ({}) - SUCCESS",
 | 
			
		||||
                event, company_id, sanitized_name
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            log::error!(
 | 
			
		||||
                "Company event: {} for company ID: {} ({}) - FAILED",
 | 
			
		||||
                event, company_id, sanitized_name
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log validation errors without exposing user data
 | 
			
		||||
    pub fn log_validation_error(field: &str, error_code: &str, ip: Option<&str>) {
 | 
			
		||||
        let ip_info = ip
 | 
			
		||||
            .map(|ip| format!(" from IP: {}", Self::sanitize_ip(ip)))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
        
 | 
			
		||||
        log::warn!(
 | 
			
		||||
            "Validation error: field '{}' failed with code '{}'{}", 
 | 
			
		||||
            field, error_code, ip_info
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log performance metrics
 | 
			
		||||
    pub fn log_performance_metric(operation: &str, duration_ms: u64, success: bool) {
 | 
			
		||||
        if success {
 | 
			
		||||
            log::info!("Performance: {} completed in {}ms", operation, duration_ms);
 | 
			
		||||
        } else {
 | 
			
		||||
            log::warn!("Performance: {} failed after {}ms", operation, duration_ms);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log database operations
 | 
			
		||||
    pub fn log_database_operation(operation: &str, table: &str, success: bool, duration_ms: Option<u64>) {
 | 
			
		||||
        let duration_info = duration_ms
 | 
			
		||||
            .map(|ms| format!(" in {}ms", ms))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        if success {
 | 
			
		||||
            log::debug!("Database: {} on {} - SUCCESS{}", operation, table, duration_info);
 | 
			
		||||
        } else {
 | 
			
		||||
            log::error!("Database: {} on {} - FAILED{}", operation, table, duration_info);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create structured log entry for monitoring systems
 | 
			
		||||
    pub fn create_structured_log(
 | 
			
		||||
        level: &str,
 | 
			
		||||
        event: &str,
 | 
			
		||||
        details: HashMap<String, serde_json::Value>,
 | 
			
		||||
    ) -> String {
 | 
			
		||||
        let mut log_entry = json!({
 | 
			
		||||
            "timestamp": chrono::Utc::now().to_rfc3339(),
 | 
			
		||||
            "level": level,
 | 
			
		||||
            "event": event,
 | 
			
		||||
            "service": "freezone-registration"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add sanitized details
 | 
			
		||||
        for (key, value) in details {
 | 
			
		||||
            let sanitized_key = Self::sanitize_log_key(&key);
 | 
			
		||||
            let sanitized_value = Self::sanitize_log_value(&value);
 | 
			
		||||
            log_entry[sanitized_key] = sanitized_value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        serde_json::to_string(&log_entry).unwrap_or_else(|_| {
 | 
			
		||||
            format!("{{\"error\": \"Failed to serialize log entry for event: {}\"}}", event)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sanitize payment ID for logging (show only last 4 characters)
 | 
			
		||||
    fn sanitize_payment_id(payment_id: &str) -> String {
 | 
			
		||||
        if payment_id.len() > 4 {
 | 
			
		||||
            format!("****{}", &payment_id[payment_id.len() - 4..])
 | 
			
		||||
        } else {
 | 
			
		||||
            "****".to_string()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sanitize IP address for logging (mask last octet)
 | 
			
		||||
    fn sanitize_ip(ip: &str) -> String {
 | 
			
		||||
        if let Some(last_dot) = ip.rfind('.') {
 | 
			
		||||
            format!("{}.***", &ip[..last_dot])
 | 
			
		||||
        } else {
 | 
			
		||||
            "***".to_string()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sanitize company name for logging (truncate and remove special chars)
 | 
			
		||||
    fn sanitize_company_name(name: &str) -> String {
 | 
			
		||||
        let sanitized = name
 | 
			
		||||
            .chars()
 | 
			
		||||
            .filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '-' || *c == '.')
 | 
			
		||||
            .take(50)
 | 
			
		||||
            .collect::<String>();
 | 
			
		||||
        
 | 
			
		||||
        if sanitized.is_empty() {
 | 
			
		||||
            "***".to_string()
 | 
			
		||||
        } else {
 | 
			
		||||
            sanitized
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sanitize log keys to prevent injection
 | 
			
		||||
    fn sanitize_log_key(key: &str) -> String {
 | 
			
		||||
        key.chars()
 | 
			
		||||
            .filter(|c| c.is_alphanumeric() || *c == '_')
 | 
			
		||||
            .take(50)
 | 
			
		||||
            .collect()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sanitize log values to prevent sensitive data exposure
 | 
			
		||||
    fn sanitize_log_value(value: &serde_json::Value) -> serde_json::Value {
 | 
			
		||||
        match value {
 | 
			
		||||
            serde_json::Value::String(s) => {
 | 
			
		||||
                // Check if this looks like sensitive data
 | 
			
		||||
                if Self::is_sensitive_data(s) {
 | 
			
		||||
                    json!("***REDACTED***")
 | 
			
		||||
                } else {
 | 
			
		||||
                    json!(s.chars().take(200).collect::<String>())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            serde_json::Value::Number(n) => json!(n),
 | 
			
		||||
            serde_json::Value::Bool(b) => json!(b),
 | 
			
		||||
            serde_json::Value::Array(arr) => {
 | 
			
		||||
                json!(arr.iter().take(10).map(|v| Self::sanitize_log_value(v)).collect::<Vec<_>>())
 | 
			
		||||
            }
 | 
			
		||||
            serde_json::Value::Object(obj) => {
 | 
			
		||||
                let sanitized: serde_json::Map<String, serde_json::Value> = obj
 | 
			
		||||
                    .iter()
 | 
			
		||||
                    .take(20)
 | 
			
		||||
                    .map(|(k, v)| (Self::sanitize_log_key(k), Self::sanitize_log_value(v)))
 | 
			
		||||
                    .collect();
 | 
			
		||||
                json!(sanitized)
 | 
			
		||||
            }
 | 
			
		||||
            serde_json::Value::Null => json!(null),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Check if a string contains sensitive data patterns
 | 
			
		||||
    fn is_sensitive_data(s: &str) -> bool {
 | 
			
		||||
        let sensitive_patterns = [
 | 
			
		||||
            "password", "secret", "key", "token", "card", "cvv", "cvc",
 | 
			
		||||
            "ssn", "social", "credit", "bank", "account", "pin"
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        let lower_s = s.to_lowercase();
 | 
			
		||||
        sensitive_patterns.iter().any(|pattern| lower_s.contains(pattern)) ||
 | 
			
		||||
        s.len() > 100 || // Long strings might contain sensitive data
 | 
			
		||||
        s.chars().all(|c| c.is_ascii_digit()) && s.len() > 8 // Might be a card number
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Audit trail logging for compliance
 | 
			
		||||
pub struct AuditLogger;
 | 
			
		||||
 | 
			
		||||
impl AuditLogger {
 | 
			
		||||
    /// Log user actions for audit trail
 | 
			
		||||
    pub fn log_user_action(
 | 
			
		||||
        user_id: u32,
 | 
			
		||||
        action: &str,
 | 
			
		||||
        resource: &str,
 | 
			
		||||
        success: bool,
 | 
			
		||||
        ip: Option<&str>,
 | 
			
		||||
    ) {
 | 
			
		||||
        let ip_info = ip
 | 
			
		||||
            .map(|ip| format!(" from {}", SecureLogger::sanitize_ip(ip)))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        let status = if success { "SUCCESS" } else { "FAILED" };
 | 
			
		||||
        
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "AUDIT: User {} performed '{}' on '{}' - {}{}",
 | 
			
		||||
            user_id, action, resource, status, ip_info
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log administrative actions
 | 
			
		||||
    pub fn log_admin_action(
 | 
			
		||||
        admin_id: u32,
 | 
			
		||||
        action: &str,
 | 
			
		||||
        target: &str,
 | 
			
		||||
        success: bool,
 | 
			
		||||
        details: Option<&str>,
 | 
			
		||||
    ) {
 | 
			
		||||
        let details_info = details
 | 
			
		||||
            .map(|d| format!(" ({})", d))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        let status = if success { "SUCCESS" } else { "FAILED" };
 | 
			
		||||
        
 | 
			
		||||
        log::warn!(
 | 
			
		||||
            "ADMIN_AUDIT: Admin {} performed '{}' on '{}' - {}{}",
 | 
			
		||||
            admin_id, action, target, status, details_info
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Log data access for compliance
 | 
			
		||||
    pub fn log_data_access(
 | 
			
		||||
        user_id: u32,
 | 
			
		||||
        data_type: &str,
 | 
			
		||||
        operation: &str,
 | 
			
		||||
        record_count: Option<usize>,
 | 
			
		||||
    ) {
 | 
			
		||||
        let count_info = record_count
 | 
			
		||||
            .map(|c| format!(" ({} records)", c))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        log::info!(
 | 
			
		||||
            "DATA_ACCESS: User {} performed '{}' on '{}'{}", 
 | 
			
		||||
            user_id, operation, data_type, count_info
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_sanitize_payment_id() {
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_payment_id("pi_1234567890"), "****7890");
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_payment_id("123"), "****");
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_payment_id(""), "****");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_sanitize_ip() {
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_ip("192.168.1.100"), "192.168.1.***");
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_ip("invalid"), "***");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_sanitize_company_name() {
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_company_name("Test Company Ltd."), "Test Company Ltd.");
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_company_name("Test<script>alert(1)</script>"), "Testscriptalert1script");
 | 
			
		||||
        assert_eq!(SecureLogger::sanitize_company_name(""), "***");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_is_sensitive_data() {
 | 
			
		||||
        assert!(SecureLogger::is_sensitive_data("password123"));
 | 
			
		||||
        assert!(SecureLogger::is_sensitive_data("secret_key"));
 | 
			
		||||
        assert!(SecureLogger::is_sensitive_data("4111111111111111")); // Card number pattern
 | 
			
		||||
        assert!(!SecureLogger::is_sensitive_data("normal text"));
 | 
			
		||||
        assert!(!SecureLogger::is_sensitive_data("123"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_structured_log_creation() {
 | 
			
		||||
        let mut details = HashMap::new();
 | 
			
		||||
        details.insert("user_id".to_string(), json!(123));
 | 
			
		||||
        details.insert("action".to_string(), json!("payment_created"));
 | 
			
		||||
        
 | 
			
		||||
        let log_entry = SecureLogger::create_structured_log("INFO", "payment_event", details);
 | 
			
		||||
        assert!(log_entry.contains("payment_event"));
 | 
			
		||||
        assert!(log_entry.contains("freezone-registration"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,257 +0,0 @@
 | 
			
		||||
use hmac::{Hmac, Mac};
 | 
			
		||||
use sha2::Sha256;
 | 
			
		||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
			
		||||
 | 
			
		||||
type HmacSha256 = Hmac<Sha256>;
 | 
			
		||||
 | 
			
		||||
/// Stripe webhook signature verification
 | 
			
		||||
/// Implements proper HMAC-SHA256 verification as per Stripe documentation
 | 
			
		||||
pub struct StripeWebhookVerifier;
 | 
			
		||||
 | 
			
		||||
impl StripeWebhookVerifier {
 | 
			
		||||
    /// Verify Stripe webhook signature
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `payload` - Raw webhook payload bytes
 | 
			
		||||
    /// * `signature_header` - Stripe-Signature header value
 | 
			
		||||
    /// * `webhook_secret` - Webhook endpoint secret from Stripe
 | 
			
		||||
    /// * `tolerance_seconds` - Maximum age of webhook (default: 300 seconds)
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    /// * `Ok(true)` - Signature is valid
 | 
			
		||||
    /// * `Ok(false)` - Signature is invalid
 | 
			
		||||
    /// * `Err(String)` - Verification error
 | 
			
		||||
    pub fn verify_signature(
 | 
			
		||||
        payload: &[u8],
 | 
			
		||||
        signature_header: &str,
 | 
			
		||||
        webhook_secret: &str,
 | 
			
		||||
        tolerance_seconds: Option<u64>,
 | 
			
		||||
    ) -> Result<bool, String> {
 | 
			
		||||
        let tolerance = tolerance_seconds.unwrap_or(300); // 5 minutes default
 | 
			
		||||
 | 
			
		||||
        // Parse signature header
 | 
			
		||||
        let (timestamp, signatures) = Self::parse_signature_header(signature_header)?;
 | 
			
		||||
 | 
			
		||||
        // Check timestamp tolerance
 | 
			
		||||
        Self::verify_timestamp(timestamp, tolerance)?;
 | 
			
		||||
 | 
			
		||||
        // Verify signature
 | 
			
		||||
        Self::verify_hmac(payload, timestamp, signatures, webhook_secret)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Parse Stripe signature header
 | 
			
		||||
    /// Format: "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
 | 
			
		||||
    fn parse_signature_header(signature_header: &str) -> Result<(u64, Vec<String>), String> {
 | 
			
		||||
        let mut timestamp = None;
 | 
			
		||||
        let mut signatures = Vec::new();
 | 
			
		||||
 | 
			
		||||
        for element in signature_header.split(',') {
 | 
			
		||||
            let parts: Vec<&str> = element.splitn(2, '=').collect();
 | 
			
		||||
            if parts.len() != 2 {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            match parts[0] {
 | 
			
		||||
                "t" => {
 | 
			
		||||
                    timestamp = Some(
 | 
			
		||||
                        parts[1]
 | 
			
		||||
                            .parse::<u64>()
 | 
			
		||||
                            .map_err(|_| "Invalid timestamp in signature header".to_string())?,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                "v1" => {
 | 
			
		||||
                    signatures.push(parts[1].to_string());
 | 
			
		||||
                }
 | 
			
		||||
                _ => {
 | 
			
		||||
                    // Ignore unknown signature schemes
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let timestamp = timestamp.ok_or("Missing timestamp in signature header")?;
 | 
			
		||||
 | 
			
		||||
        if signatures.is_empty() {
 | 
			
		||||
            return Err("No valid signatures found in header".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok((timestamp, signatures))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Verify timestamp is within tolerance
 | 
			
		||||
    fn verify_timestamp(timestamp: u64, tolerance_seconds: u64) -> Result<(), String> {
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .map_err(|_| "Failed to get current time")?
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        let age = current_time.saturating_sub(timestamp);
 | 
			
		||||
 | 
			
		||||
        if age > tolerance_seconds {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Webhook timestamp too old: {} seconds (max: {})",
 | 
			
		||||
                age, tolerance_seconds
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Verify HMAC signature
 | 
			
		||||
    fn verify_hmac(
 | 
			
		||||
        payload: &[u8],
 | 
			
		||||
        timestamp: u64,
 | 
			
		||||
        signatures: Vec<String>,
 | 
			
		||||
        webhook_secret: &str,
 | 
			
		||||
    ) -> Result<bool, String> {
 | 
			
		||||
        // Create signed payload: timestamp + "." + payload
 | 
			
		||||
        let signed_payload = format!(
 | 
			
		||||
            "{}.{}",
 | 
			
		||||
            timestamp,
 | 
			
		||||
            std::str::from_utf8(payload).map_err(|_| "Invalid UTF-8 in payload")?
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Create HMAC
 | 
			
		||||
        let mut mac = HmacSha256::new_from_slice(webhook_secret.as_bytes())
 | 
			
		||||
            .map_err(|_| "Invalid webhook secret")?;
 | 
			
		||||
        mac.update(signed_payload.as_bytes());
 | 
			
		||||
 | 
			
		||||
        // Get expected signature
 | 
			
		||||
        let expected_signature = hex::encode(mac.finalize().into_bytes());
 | 
			
		||||
 | 
			
		||||
        // Compare with provided signatures (constant-time comparison)
 | 
			
		||||
        for signature in signatures {
 | 
			
		||||
            if constant_time_compare(&expected_signature, &signature) {
 | 
			
		||||
                return Ok(true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Constant-time string comparison to prevent timing attacks
 | 
			
		||||
fn constant_time_compare(a: &str, b: &str) -> bool {
 | 
			
		||||
    if a.len() != b.len() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut result = 0u8;
 | 
			
		||||
    for (byte_a, byte_b) in a.bytes().zip(b.bytes()) {
 | 
			
		||||
        result |= byte_a ^ byte_b;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_parse_signature_header() {
 | 
			
		||||
        let header =
 | 
			
		||||
            "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd";
 | 
			
		||||
        let (timestamp, signatures) =
 | 
			
		||||
            StripeWebhookVerifier::parse_signature_header(header).unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(timestamp, 1492774577);
 | 
			
		||||
        assert_eq!(signatures.len(), 1);
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            signatures[0],
 | 
			
		||||
            "5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_parse_signature_header_multiple_signatures() {
 | 
			
		||||
        let header = "t=1492774577,v1=sig1,v1=sig2";
 | 
			
		||||
        let (timestamp, signatures) =
 | 
			
		||||
            StripeWebhookVerifier::parse_signature_header(header).unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(timestamp, 1492774577);
 | 
			
		||||
        assert_eq!(signatures.len(), 2);
 | 
			
		||||
        assert_eq!(signatures[0], "sig1");
 | 
			
		||||
        assert_eq!(signatures[1], "sig2");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_parse_signature_header_invalid() {
 | 
			
		||||
        let header = "invalid_header";
 | 
			
		||||
        let result = StripeWebhookVerifier::parse_signature_header(header);
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_constant_time_compare() {
 | 
			
		||||
        assert!(constant_time_compare("hello", "hello"));
 | 
			
		||||
        assert!(!constant_time_compare("hello", "world"));
 | 
			
		||||
        assert!(!constant_time_compare("hello", "hello123"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_verify_timestamp_valid() {
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        // Test with current timestamp (should pass)
 | 
			
		||||
        assert!(StripeWebhookVerifier::verify_timestamp(current_time, 300).is_ok());
 | 
			
		||||
 | 
			
		||||
        // Test with timestamp 100 seconds ago (should pass)
 | 
			
		||||
        assert!(StripeWebhookVerifier::verify_timestamp(current_time - 100, 300).is_ok());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_verify_timestamp_too_old() {
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        // Test with timestamp 400 seconds ago (should fail with 300s tolerance)
 | 
			
		||||
        let result = StripeWebhookVerifier::verify_timestamp(current_time - 400, 300);
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
        assert!(result.unwrap_err().contains("too old"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_verify_signature_integration() {
 | 
			
		||||
        // Test with known good signature from Stripe documentation
 | 
			
		||||
        let payload = b"test payload";
 | 
			
		||||
        let webhook_secret = "whsec_test_secret";
 | 
			
		||||
        let timestamp = 1492774577u64;
 | 
			
		||||
 | 
			
		||||
        // Create expected signature manually for testing
 | 
			
		||||
        let signed_payload = format!("{}.{}", timestamp, std::str::from_utf8(payload).unwrap());
 | 
			
		||||
        let mut mac = HmacSha256::new_from_slice(webhook_secret.as_bytes()).unwrap();
 | 
			
		||||
        mac.update(signed_payload.as_bytes());
 | 
			
		||||
        let expected_sig = hex::encode(mac.finalize().into_bytes());
 | 
			
		||||
 | 
			
		||||
        let _signature_header = format!("t={},v1={}", timestamp, expected_sig);
 | 
			
		||||
 | 
			
		||||
        // This would fail due to timestamp being too old, so we test with a recent timestamp
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        let signed_payload_current =
 | 
			
		||||
            format!("{}.{}", current_time, std::str::from_utf8(payload).unwrap());
 | 
			
		||||
        let mut mac_current = HmacSha256::new_from_slice(webhook_secret.as_bytes()).unwrap();
 | 
			
		||||
        mac_current.update(signed_payload_current.as_bytes());
 | 
			
		||||
        let current_sig = hex::encode(mac_current.finalize().into_bytes());
 | 
			
		||||
 | 
			
		||||
        let current_signature_header = format!("t={},v1={}", current_time, current_sig);
 | 
			
		||||
 | 
			
		||||
        let result = StripeWebhookVerifier::verify_signature(
 | 
			
		||||
            payload,
 | 
			
		||||
            ¤t_signature_header,
 | 
			
		||||
            webhook_secret,
 | 
			
		||||
            Some(300),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert!(result.is_ok());
 | 
			
		||||
        assert!(result.unwrap());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,403 +0,0 @@
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
/// Validation error details
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ValidationError {
 | 
			
		||||
    pub field: String,
 | 
			
		||||
    pub message: String,
 | 
			
		||||
    pub code: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Validation result
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ValidationResult {
 | 
			
		||||
    pub is_valid: bool,
 | 
			
		||||
    pub errors: Vec<ValidationError>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ValidationResult {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            is_valid: true,
 | 
			
		||||
            errors: Vec::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_error(&mut self, field: &str, message: &str, code: &str) {
 | 
			
		||||
        self.is_valid = false;
 | 
			
		||||
        self.errors.push(ValidationError {
 | 
			
		||||
            field: field.to_string(),
 | 
			
		||||
            message: message.to_string(),
 | 
			
		||||
            code: code.to_string(),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn merge(&mut self, other: ValidationResult) {
 | 
			
		||||
        if !other.is_valid {
 | 
			
		||||
            self.is_valid = false;
 | 
			
		||||
            self.errors.extend(other.errors);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Company registration data validator
 | 
			
		||||
pub struct CompanyRegistrationValidator;
 | 
			
		||||
 | 
			
		||||
impl CompanyRegistrationValidator {
 | 
			
		||||
    /// Validate complete company registration data
 | 
			
		||||
    pub fn validate(
 | 
			
		||||
        data: &crate::controllers::payment::CompanyRegistrationData,
 | 
			
		||||
    ) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        // Validate company name
 | 
			
		||||
        result.merge(Self::validate_company_name(&data.company_name));
 | 
			
		||||
 | 
			
		||||
        // Validate company type
 | 
			
		||||
        result.merge(Self::validate_company_type(&data.company_type));
 | 
			
		||||
 | 
			
		||||
        // Validate email (if provided)
 | 
			
		||||
        if let Some(ref email) = data.company_email {
 | 
			
		||||
            if !email.is_empty() {
 | 
			
		||||
                result.merge(Self::validate_email(email));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate phone (if provided)
 | 
			
		||||
        if let Some(ref phone) = data.company_phone {
 | 
			
		||||
            if !phone.is_empty() {
 | 
			
		||||
                result.merge(Self::validate_phone(phone));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate website (if provided)
 | 
			
		||||
        if let Some(ref website) = data.company_website {
 | 
			
		||||
            if !website.is_empty() {
 | 
			
		||||
                result.merge(Self::validate_website(website));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate address (if provided)
 | 
			
		||||
        if let Some(ref address) = data.company_address {
 | 
			
		||||
            if !address.is_empty() {
 | 
			
		||||
                result.merge(Self::validate_address(address));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate shareholders JSON
 | 
			
		||||
        result.merge(Self::validate_shareholders(&data.shareholders));
 | 
			
		||||
 | 
			
		||||
        // Validate payment plan
 | 
			
		||||
        result.merge(Self::validate_payment_plan(&data.payment_plan));
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate company name
 | 
			
		||||
    fn validate_company_name(name: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if name.trim().is_empty() {
 | 
			
		||||
            result.add_error("company_name", "Company name is required", "required");
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if name.len() < 2 {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_name",
 | 
			
		||||
                "Company name must be at least 2 characters long",
 | 
			
		||||
                "min_length",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if name.len() > 100 {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_name",
 | 
			
		||||
                "Company name must be less than 100 characters",
 | 
			
		||||
                "max_length",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for valid characters (letters, numbers, spaces, common punctuation)
 | 
			
		||||
        let valid_name_regex = Regex::new(r"^[a-zA-Z0-9\s\-\.\&\(\)]+$").unwrap();
 | 
			
		||||
        if !valid_name_regex.is_match(name) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_name",
 | 
			
		||||
                "Company name contains invalid characters",
 | 
			
		||||
                "invalid_format",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate company type
 | 
			
		||||
    fn validate_company_type(company_type: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        let valid_types = vec![
 | 
			
		||||
            "Single FZC",
 | 
			
		||||
            "Startup FZC",
 | 
			
		||||
            "Growth FZC",
 | 
			
		||||
            "Global FZC",
 | 
			
		||||
            "Cooperative FZC",
 | 
			
		||||
            "Twin FZC",
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if !valid_types.contains(&company_type) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_type",
 | 
			
		||||
                "Invalid company type selected",
 | 
			
		||||
                "invalid_option",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate email address
 | 
			
		||||
    fn validate_email(email: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if email.trim().is_empty() {
 | 
			
		||||
            return result; // Email is optional
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Basic email regex
 | 
			
		||||
        let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
 | 
			
		||||
        if !email_regex.is_match(email) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_email",
 | 
			
		||||
                "Please enter a valid email address",
 | 
			
		||||
                "invalid_format",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if email.len() > 254 {
 | 
			
		||||
            result.add_error("company_email", "Email address is too long", "max_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate phone number
 | 
			
		||||
    fn validate_phone(phone: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if phone.trim().is_empty() {
 | 
			
		||||
            return result; // Phone is optional
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove common formatting characters
 | 
			
		||||
        let cleaned_phone = phone.replace(&[' ', '-', '(', ')', '+'][..], "");
 | 
			
		||||
 | 
			
		||||
        if cleaned_phone.len() < 7 {
 | 
			
		||||
            result.add_error("company_phone", "Phone number is too short", "min_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if cleaned_phone.len() > 15 {
 | 
			
		||||
            result.add_error("company_phone", "Phone number is too long", "max_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if contains only digits after cleaning
 | 
			
		||||
        if !cleaned_phone.chars().all(|c| c.is_ascii_digit()) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_phone",
 | 
			
		||||
                "Phone number contains invalid characters",
 | 
			
		||||
                "invalid_format",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate website URL
 | 
			
		||||
    fn validate_website(website: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if website.trim().is_empty() {
 | 
			
		||||
            return result; // Website is optional
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Basic URL validation
 | 
			
		||||
        let url_regex = Regex::new(r"^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$").unwrap();
 | 
			
		||||
        if !url_regex.is_match(website) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "company_website",
 | 
			
		||||
                "Please enter a valid website URL (e.g., https://example.com)",
 | 
			
		||||
                "invalid_format",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if website.len() > 255 {
 | 
			
		||||
            result.add_error("company_website", "Website URL is too long", "max_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate address
 | 
			
		||||
    fn validate_address(address: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if address.trim().is_empty() {
 | 
			
		||||
            return result; // Address is optional
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if address.len() < 5 {
 | 
			
		||||
            result.add_error("company_address", "Address is too short", "min_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if address.len() > 500 {
 | 
			
		||||
            result.add_error("company_address", "Address is too long", "max_length");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate shareholders JSON
 | 
			
		||||
    fn validate_shareholders(shareholders: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        if shareholders.trim().is_empty() {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "shareholders",
 | 
			
		||||
                "Shareholders information is required",
 | 
			
		||||
                "required",
 | 
			
		||||
            );
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Try to parse as JSON
 | 
			
		||||
        match serde_json::from_str::<serde_json::Value>(shareholders) {
 | 
			
		||||
            Ok(json) => {
 | 
			
		||||
                if let Some(array) = json.as_array() {
 | 
			
		||||
                    if array.is_empty() {
 | 
			
		||||
                        result.add_error(
 | 
			
		||||
                            "shareholders",
 | 
			
		||||
                            "At least one shareholder is required",
 | 
			
		||||
                            "min_items",
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    result.add_error(
 | 
			
		||||
                        "shareholders",
 | 
			
		||||
                        "Shareholders must be a valid JSON array",
 | 
			
		||||
                        "invalid_format",
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                result.add_error(
 | 
			
		||||
                    "shareholders",
 | 
			
		||||
                    "Invalid shareholders data format",
 | 
			
		||||
                    "invalid_json",
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Validate payment plan
 | 
			
		||||
    fn validate_payment_plan(payment_plan: &str) -> ValidationResult {
 | 
			
		||||
        let mut result = ValidationResult::new();
 | 
			
		||||
 | 
			
		||||
        let valid_plans = vec!["monthly", "yearly", "two_year"];
 | 
			
		||||
 | 
			
		||||
        if !valid_plans.contains(&payment_plan) {
 | 
			
		||||
            result.add_error(
 | 
			
		||||
                "payment_plan",
 | 
			
		||||
                "Invalid payment plan selected",
 | 
			
		||||
                "invalid_option",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use crate::controllers::payment::CompanyRegistrationData;
 | 
			
		||||
 | 
			
		||||
    fn create_valid_registration_data() -> CompanyRegistrationData {
 | 
			
		||||
        CompanyRegistrationData {
 | 
			
		||||
            company_name: "Test Company Ltd".to_string(),
 | 
			
		||||
            company_type: "Single FZC".to_string(),
 | 
			
		||||
            company_email: Some("test@example.com".to_string()),
 | 
			
		||||
            company_phone: Some("+1234567890".to_string()),
 | 
			
		||||
            company_website: Some("https://example.com".to_string()),
 | 
			
		||||
            company_address: Some("123 Test Street, Test City".to_string()),
 | 
			
		||||
            company_industry: Some("Technology".to_string()),
 | 
			
		||||
            company_purpose: Some("Software development".to_string()),
 | 
			
		||||
            fiscal_year_end: Some("December".to_string()),
 | 
			
		||||
            shareholders: r#"[{"name": "John Doe", "percentage": 100}]"#.to_string(),
 | 
			
		||||
            payment_plan: "monthly".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_valid_registration_data() {
 | 
			
		||||
        let data = create_valid_registration_data();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(result.is_valid, "Valid data should pass validation");
 | 
			
		||||
        assert!(result.errors.is_empty(), "Valid data should have no errors");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_company_name() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_name = "".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_name"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_email() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_email = Some("invalid-email".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_email"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_phone() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_phone = Some("123".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_phone"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_website() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_website = Some("not-a-url".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_website"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_shareholders() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.shareholders = "invalid json".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "shareholders"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_invalid_payment_plan() {
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.payment_plan = "invalid_plan".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "payment_plan"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
pub mod company;
 | 
			
		||||
 | 
			
		||||
// Re-export for easier imports
 | 
			
		||||
pub use company::{CompanyRegistrationValidator, ValidationError, ValidationResult};
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -5,29 +5,29 @@
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <h1>Create New Event</h1>
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    {% if error %}
 | 
			
		||||
    <div class="alert alert-danger" role="alert">
 | 
			
		||||
        {{ error }}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <form action="/calendar/events" method="post">
 | 
			
		||||
    
 | 
			
		||||
    <form action="/calendar/new" method="post">
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
            <label for="title" class="form-label">Event Title</label>
 | 
			
		||||
            <input type="text" class="form-control" id="title" name="title" required>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
            <label for="description" class="form-label">Description</label>
 | 
			
		||||
            <textarea class="form-control" id="description" name="description" rows="3"></textarea>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="mb-3 form-check">
 | 
			
		||||
            <input type="checkbox" class="form-check-input" id="all_day" name="all_day">
 | 
			
		||||
            <label class="form-check-label" for="all_day">All Day Event</label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="row mb-3">
 | 
			
		||||
            <div class="col">
 | 
			
		||||
                <label for="start_time" class="form-label">Start Time</label>
 | 
			
		||||
@@ -38,14 +38,7 @@
 | 
			
		||||
                <input type="datetime-local" class="form-control" id="end_time" name="end_time" required>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Show selected date info when coming from calendar date click -->
 | 
			
		||||
        <div id="selected-date-info" class="alert alert-info" style="display: none;">
 | 
			
		||||
            <strong>Selected Date:</strong> <span id="selected-date-display"></span>
 | 
			
		||||
            <br>
 | 
			
		||||
            <small>The date is pre-selected. You can only modify the time portion.</small>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
            <label for="color" class="form-label">Event Color</label>
 | 
			
		||||
            <select class="form-control" id="color" name="color">
 | 
			
		||||
@@ -57,7 +50,7 @@
 | 
			
		||||
                <option value="#24C1E0">Cyan</option>
 | 
			
		||||
            </select>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="mb-3">
 | 
			
		||||
            <button type="submit" class="btn btn-primary">Create Event</button>
 | 
			
		||||
            <a href="/calendar" class="btn btn-secondary">Cancel</a>
 | 
			
		||||
@@ -66,106 +59,37 @@
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Check if we came from a date click (URL parameter)
 | 
			
		||||
        const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
        const selectedDate = urlParams.get('date');
 | 
			
		||||
 | 
			
		||||
        if (selectedDate) {
 | 
			
		||||
            // Show the selected date info
 | 
			
		||||
            document.getElementById('selected-date-info').style.display = 'block';
 | 
			
		||||
            document.getElementById('selected-date-display').textContent = new Date(selectedDate).toLocaleDateString();
 | 
			
		||||
 | 
			
		||||
            // Pre-fill the date portion and restrict date changes
 | 
			
		||||
            const startTimeInput = document.getElementById('start_time');
 | 
			
		||||
            const endTimeInput = document.getElementById('end_time');
 | 
			
		||||
 | 
			
		||||
            // Set default times (9 AM to 10 AM on the selected date)
 | 
			
		||||
            const startDateTime = new Date(selectedDate + 'T09:00');
 | 
			
		||||
            const endDateTime = new Date(selectedDate + 'T10:00');
 | 
			
		||||
 | 
			
		||||
            // Format for datetime-local input (YYYY-MM-DDTHH:MM)
 | 
			
		||||
            startTimeInput.value = startDateTime.toISOString().slice(0, 16);
 | 
			
		||||
            endTimeInput.value = endDateTime.toISOString().slice(0, 16);
 | 
			
		||||
 | 
			
		||||
            // Set minimum and maximum date to the selected date to prevent changing the date
 | 
			
		||||
            const minDate = selectedDate + 'T00:00';
 | 
			
		||||
            const maxDate = selectedDate + 'T23:59';
 | 
			
		||||
            startTimeInput.min = minDate;
 | 
			
		||||
            startTimeInput.max = maxDate;
 | 
			
		||||
            endTimeInput.min = minDate;
 | 
			
		||||
            endTimeInput.max = maxDate;
 | 
			
		||||
 | 
			
		||||
            // Add event listeners to ensure end time is after start time
 | 
			
		||||
            startTimeInput.addEventListener('change', function () {
 | 
			
		||||
                const startTime = new Date(this.value);
 | 
			
		||||
                const endTime = new Date(endTimeInput.value);
 | 
			
		||||
 | 
			
		||||
                if (endTime <= startTime) {
 | 
			
		||||
                    // Set end time to 1 hour after start time
 | 
			
		||||
                    const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
 | 
			
		||||
                    endTimeInput.value = newEndTime.toISOString().slice(0, 16);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Update end time minimum to be after start time
 | 
			
		||||
                endTimeInput.min = this.value;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            endTimeInput.addEventListener('change', function () {
 | 
			
		||||
                const startTime = new Date(startTimeInput.value);
 | 
			
		||||
                const endTime = new Date(this.value);
 | 
			
		||||
 | 
			
		||||
                if (endTime <= startTime) {
 | 
			
		||||
                    // Reset to 1 hour after start time
 | 
			
		||||
                    const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
 | 
			
		||||
                    this.value = newEndTime.toISOString().slice(0, 16);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            // No date selected, set default to current time
 | 
			
		||||
            const now = new Date();
 | 
			
		||||
            const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
 | 
			
		||||
 | 
			
		||||
            document.getElementById('start_time').value = now.toISOString().slice(0, 16);
 | 
			
		||||
            document.getElementById('end_time').value = oneHourLater.toISOString().slice(0, 16);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        // Convert datetime-local inputs to RFC3339 format on form submission
 | 
			
		||||
        document.querySelector('form').addEventListener('submit', function (e) {
 | 
			
		||||
        document.querySelector('form').addEventListener('submit', function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            const startTime = document.getElementById('start_time').value;
 | 
			
		||||
            const endTime = document.getElementById('end_time').value;
 | 
			
		||||
 | 
			
		||||
            // Validate that end time is after start time
 | 
			
		||||
            if (new Date(endTime) <= new Date(startTime)) {
 | 
			
		||||
                alert('End time must be after start time');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Convert to RFC3339 format
 | 
			
		||||
            const startRFC = new Date(startTime).toISOString();
 | 
			
		||||
            const endRFC = new Date(endTime).toISOString();
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Create hidden inputs for the RFC3339 values
 | 
			
		||||
            const startInput = document.createElement('input');
 | 
			
		||||
            startInput.type = 'hidden';
 | 
			
		||||
            startInput.name = 'start_time';
 | 
			
		||||
            startInput.value = startRFC;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            const endInput = document.createElement('input');
 | 
			
		||||
            endInput.type = 'hidden';
 | 
			
		||||
            endInput.name = 'end_time';
 | 
			
		||||
            endInput.value = endRFC;
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Remove the original inputs
 | 
			
		||||
            document.getElementById('start_time').removeAttribute('name');
 | 
			
		||||
            document.getElementById('end_time').removeAttribute('name');
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Add the hidden inputs to the form
 | 
			
		||||
            this.appendChild(startInput);
 | 
			
		||||
            this.appendChild(endInput);
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            // Submit the form
 | 
			
		||||
            this.submit();
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,417 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ company.name }} - Document Management{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
{{ super() }}
 | 
			
		||||
<style>
 | 
			
		||||
    .document-card {
 | 
			
		||||
        transition: transform 0.2s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .document-card:hover {
 | 
			
		||||
        transform: translateY(-2px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .file-icon {
 | 
			
		||||
        font-size: 2rem;
 | 
			
		||||
        margin-bottom: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .upload-area {
 | 
			
		||||
        border: 2px dashed #dee2e6;
 | 
			
		||||
        border-radius: 0.375rem;
 | 
			
		||||
        padding: 2rem;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        transition: border-color 0.2s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .upload-area:hover {
 | 
			
		||||
        border-color: #0d6efd;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .upload-area.dragover {
 | 
			
		||||
        border-color: #0d6efd;
 | 
			
		||||
        background-color: #f8f9fa;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid py-4">
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center mb-4">
 | 
			
		||||
        <h2><i class="bi bi-folder me-2"></i>{{ company.name }} - Documents</h2>
 | 
			
		||||
        <div>
 | 
			
		||||
            <a href="/company/view/{{ company.base_data.id }}" class="btn btn-outline-secondary me-2">
 | 
			
		||||
                <i class="bi bi-arrow-left me-1"></i>Back to Company
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="/company" class="btn btn-outline-secondary">
 | 
			
		||||
                <i class="bi bi-building me-1"></i>All Companies
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Success/Error Messages -->
 | 
			
		||||
    {% if success %}
 | 
			
		||||
    <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-check-circle me-2"></i>{{ success }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if error %}
 | 
			
		||||
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <!-- Document Statistics -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card text-center">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <i class="bi bi-files text-primary" style="font-size: 2rem;"></i>
 | 
			
		||||
                    <h4 class="mt-2">{{ stats.total_documents }}</h4>
 | 
			
		||||
                    <p class="text-muted mb-0">Total Documents</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card text-center">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <i class="bi bi-hdd text-info" style="font-size: 2rem;"></i>
 | 
			
		||||
                    <h4 class="mt-2">{{ stats.formatted_total_size }}</h4>
 | 
			
		||||
                    <p class="text-muted mb-0">Total Size</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card text-center">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <i class="bi bi-upload text-success" style="font-size: 2rem;"></i>
 | 
			
		||||
                    <h4 class="mt-2">{{ stats.recent_uploads }}</h4>
 | 
			
		||||
                    <p class="text-muted mb-0">Recent Uploads</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card text-center">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <i class="bi bi-folder-plus text-warning" style="font-size: 2rem;"></i>
 | 
			
		||||
                    <h4 class="mt-2">{{ stats.by_type | length }}</h4>
 | 
			
		||||
                    <p class="text-muted mb-0">Document Types</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Document Upload Section -->
 | 
			
		||||
    <div class="card mb-4">
 | 
			
		||||
        <div class="card-header bg-primary text-white">
 | 
			
		||||
            <h5 class="mb-0"><i class="bi bi-cloud-upload me-2"></i>Upload Documents</h5>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <form action="/company/documents/{{ company_id }}/upload" method="post" enctype="multipart/form-data"
 | 
			
		||||
                id="uploadForm">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="document_type" class="form-label">Document Type</label>
 | 
			
		||||
                            <select class="form-select" id="document_type" name="document_type" required>
 | 
			
		||||
                                <option value="">Select document type...</option>
 | 
			
		||||
                                {% for doc_type in document_types %}
 | 
			
		||||
                                <option value="{{ doc_type.0 }}">{{ doc_type.1 }}</option>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Description (Optional)</label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="3"
 | 
			
		||||
                                placeholder="Enter document description..."></textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mb-3 form-check">
 | 
			
		||||
                            <input type="checkbox" class="form-check-input" id="is_public" name="is_public">
 | 
			
		||||
                            <label class="form-check-label" for="is_public">
 | 
			
		||||
                                Make document publicly accessible
 | 
			
		||||
                            </label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="documents" class="form-label">Select Files</label>
 | 
			
		||||
                            <div class="upload-area" id="uploadArea">
 | 
			
		||||
                                <i class="bi bi-cloud-upload file-icon text-muted"></i>
 | 
			
		||||
                                <p class="mb-2">Drag and drop files here or click to browse</p>
 | 
			
		||||
                                <input type="file" class="form-control" id="documents" name="documents" multiple
 | 
			
		||||
                                    accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.txt" style="display: none;">
 | 
			
		||||
                                <button type="button" class="btn btn-outline-primary"
 | 
			
		||||
                                    onclick="document.getElementById('documents').click()">
 | 
			
		||||
                                    <i class="bi bi-folder2-open me-1"></i>Browse Files
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <div id="fileList" class="mt-3"></div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    <button type="submit" class="btn btn-primary" id="uploadBtn">
 | 
			
		||||
                        <i class="bi bi-upload me-1"></i>Upload Documents
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Documents List -->
 | 
			
		||||
    <div class="card">
 | 
			
		||||
        <div class="card-header bg-light">
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <h5 class="mb-0"><i class="bi bi-files me-2"></i>Documents ({{ documents | length }})</h5>
 | 
			
		||||
                <div class="input-group" style="width: 300px;">
 | 
			
		||||
                    <input type="text" class="form-control" id="searchInput" placeholder="Search documents...">
 | 
			
		||||
                    <button class="btn btn-outline-secondary" type="button" id="searchBtn">
 | 
			
		||||
                        <i class="bi bi-search"></i>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            {% if documents and documents | length > 0 %}
 | 
			
		||||
            <div class="row" id="documentsGrid">
 | 
			
		||||
                {% for document in documents %}
 | 
			
		||||
                <div class="col-md-4 mb-3 document-item" data-name="{{ document.name | lower }}"
 | 
			
		||||
                    data-type="{{ document.document_type_str | lower }}">
 | 
			
		||||
                    <div class="card document-card h-100">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <div class="d-flex justify-content-between align-items-start mb-2">
 | 
			
		||||
                                <div class="file-icon">
 | 
			
		||||
                                    {% if document.is_pdf %}
 | 
			
		||||
                                    <i class="bi bi-file-earmark-pdf text-danger"></i>
 | 
			
		||||
                                    {% elif document.is_image %}
 | 
			
		||||
                                    <i class="bi bi-file-earmark-image text-success"></i>
 | 
			
		||||
                                    {% elif document.mime_type == "application/msword" %}
 | 
			
		||||
                                    <i class="bi bi-file-earmark-word text-primary"></i>
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                    <i class="bi bi-file-earmark text-secondary"></i>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="dropdown">
 | 
			
		||||
                                    <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
 | 
			
		||||
                                        data-bs-toggle="dropdown">
 | 
			
		||||
                                        <i class="bi bi-three-dots"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <ul class="dropdown-menu">
 | 
			
		||||
                                        <li><a class="dropdown-item" href="#"
 | 
			
		||||
                                                onclick="downloadDocument({{ document.id }})">
 | 
			
		||||
                                                <i class="bi bi-download me-1"></i>Download
 | 
			
		||||
                                            </a></li>
 | 
			
		||||
                                        <li><a class="dropdown-item" href="#" onclick="editDocument({{ document.id }})">
 | 
			
		||||
                                                <i class="bi bi-pencil me-1"></i>Edit
 | 
			
		||||
                                            </a></li>
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <hr class="dropdown-divider">
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li><a class="dropdown-item text-danger" href="#"
 | 
			
		||||
                                                onclick="deleteDocument({{ document.id }}, '{{ document.name }}')">
 | 
			
		||||
                                                <i class="bi bi-trash me-1"></i>Delete
 | 
			
		||||
                                            </a></li>
 | 
			
		||||
                                    </ul>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <h6 class="card-title text-truncate" title="{{ document.name }}">{{ document.name }}</h6>
 | 
			
		||||
                            <p class="card-text">
 | 
			
		||||
                                <small class="text-muted">
 | 
			
		||||
                                    <span class="badge bg-secondary mb-1">{{ document.document_type_str }}</span><br>
 | 
			
		||||
                                    Size: {{ document.formatted_file_size }}<br>
 | 
			
		||||
                                    Uploaded: {{ document.formatted_upload_date }}<br>
 | 
			
		||||
                                    By: {{ document.uploaded_by }}
 | 
			
		||||
                                    {% if document.is_public %}
 | 
			
		||||
                                    <br><span class="badge bg-success">Public</span>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </small>
 | 
			
		||||
                            </p>
 | 
			
		||||
                            {% if document.description %}
 | 
			
		||||
                            <p class="card-text"><small>{{ document.description }}</small></p>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <div class="text-center py-5">
 | 
			
		||||
                <i class="bi bi-folder-x text-muted" style="font-size: 4rem;"></i>
 | 
			
		||||
                <h4 class="text-muted mt-3">No Documents Found</h4>
 | 
			
		||||
                <p class="text-muted">Upload your first document using the form above.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Delete Confirmation Modal -->
 | 
			
		||||
<div class="modal fade" id="deleteModal" tabindex="-1">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title">Confirm Delete</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <p>Are you sure you want to delete the document "<span id="deleteDocumentName"></span>"?</p>
 | 
			
		||||
                <p class="text-danger"><small>This action cannot be undone.</small></p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                <a href="#" class="btn btn-danger" id="confirmDeleteBtn">Delete Document</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
{{ super() }}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        const uploadArea = document.getElementById('uploadArea');
 | 
			
		||||
        const fileInput = document.getElementById('documents');
 | 
			
		||||
        const fileList = document.getElementById('fileList');
 | 
			
		||||
        const uploadBtn = document.getElementById('uploadBtn');
 | 
			
		||||
        const searchInput = document.getElementById('searchInput');
 | 
			
		||||
 | 
			
		||||
        // File upload handling
 | 
			
		||||
        fileInput.addEventListener('change', function () {
 | 
			
		||||
            console.log('Files selected:', this.files.length);
 | 
			
		||||
            updateFileList();
 | 
			
		||||
            updateUploadButton();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Drag and drop
 | 
			
		||||
        uploadArea.addEventListener('dragover', function (e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            uploadArea.classList.add('dragover');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        uploadArea.addEventListener('dragleave', function (e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            uploadArea.classList.remove('dragover');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        uploadArea.addEventListener('drop', function (e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            uploadArea.classList.remove('dragover');
 | 
			
		||||
 | 
			
		||||
            const files = e.dataTransfer.files;
 | 
			
		||||
            console.log('Files dropped:', files.length);
 | 
			
		||||
 | 
			
		||||
            // Create a new DataTransfer object and assign to input
 | 
			
		||||
            const dt = new DataTransfer();
 | 
			
		||||
            for (let i = 0; i < files.length; i++) {
 | 
			
		||||
                dt.items.add(files[i]);
 | 
			
		||||
            }
 | 
			
		||||
            fileInput.files = dt.files;
 | 
			
		||||
 | 
			
		||||
            updateFileList();
 | 
			
		||||
            updateUploadButton();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click to upload area
 | 
			
		||||
        uploadArea.addEventListener('click', function (e) {
 | 
			
		||||
            if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT') {
 | 
			
		||||
                fileInput.click();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Search functionality
 | 
			
		||||
        searchInput.addEventListener('input', function () {
 | 
			
		||||
            const searchTerm = this.value.toLowerCase();
 | 
			
		||||
            const documentItems = document.querySelectorAll('.document-item');
 | 
			
		||||
 | 
			
		||||
            documentItems.forEach(function (item) {
 | 
			
		||||
                const name = item.dataset.name;
 | 
			
		||||
                const type = item.dataset.type;
 | 
			
		||||
                const matches = name.includes(searchTerm) || type.includes(searchTerm);
 | 
			
		||||
                item.style.display = matches ? 'block' : 'none';
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function updateFileList() {
 | 
			
		||||
            const files = Array.from(fileInput.files);
 | 
			
		||||
            if (files.length === 0) {
 | 
			
		||||
                fileList.innerHTML = '';
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const listHtml = files.map(file =>
 | 
			
		||||
                `<div class="d-flex justify-content-between align-items-center p-2 border rounded mb-1">
 | 
			
		||||
                <span class="text-truncate">${file.name}</span>
 | 
			
		||||
                <small class="text-muted">${formatFileSize(file.size)}</small>
 | 
			
		||||
            </div>`
 | 
			
		||||
            ).join('');
 | 
			
		||||
 | 
			
		||||
            fileList.innerHTML = `<div class="mt-2"><strong>Selected files:</strong>${listHtml}</div>`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function updateUploadButton() {
 | 
			
		||||
            const hasFiles = fileInput.files.length > 0;
 | 
			
		||||
            const hasDocumentType = document.getElementById('document_type').value !== '';
 | 
			
		||||
            uploadBtn.disabled = !hasFiles || !hasDocumentType;
 | 
			
		||||
 | 
			
		||||
            console.log('Update upload button - Files:', hasFiles, 'DocType:', hasDocumentType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Also update button when document type changes
 | 
			
		||||
        document.getElementById('document_type').addEventListener('change', function () {
 | 
			
		||||
            updateUploadButton();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add form submission debugging
 | 
			
		||||
        document.getElementById('uploadForm').addEventListener('submit', function (e) {
 | 
			
		||||
            console.log('Form submitted');
 | 
			
		||||
            console.log('Files:', fileInput.files.length);
 | 
			
		||||
            console.log('Document type:', document.getElementById('document_type').value);
 | 
			
		||||
 | 
			
		||||
            if (fileInput.files.length === 0) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                alert('Please select at least one file to upload.');
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (document.getElementById('document_type').value === '') {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                alert('Please select a document type.');
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function formatFileSize(bytes) {
 | 
			
		||||
            if (bytes === 0) return '0 B';
 | 
			
		||||
            const k = 1024;
 | 
			
		||||
            const sizes = ['B', 'KB', 'MB', 'GB'];
 | 
			
		||||
            const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
            return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function deleteDocument(documentId, documentName) {
 | 
			
		||||
        document.getElementById('deleteDocumentName').textContent = documentName;
 | 
			
		||||
        document.getElementById('confirmDeleteBtn').href = `/company/documents/{{ company_id }}/delete/${documentId}`;
 | 
			
		||||
        new bootstrap.Modal(document.getElementById('deleteModal')).show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function downloadDocument(documentId) {
 | 
			
		||||
        // TODO: Implement download functionality
 | 
			
		||||
        alert('Download functionality will be implemented soon');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function editDocument(documentId) {
 | 
			
		||||
        // TODO: Implement edit functionality
 | 
			
		||||
        alert('Edit functionality will be implemented soon');
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,249 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Edit {{ company.name }} - Company Management{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid py-4">
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center mb-4">
 | 
			
		||||
        <h2><i class="bi bi-pencil-square me-2"></i>Edit Company</h2>
 | 
			
		||||
        <div>
 | 
			
		||||
            <a href="/company/view/{{ company.base_data.id }}" class="btn btn-outline-secondary me-2">
 | 
			
		||||
                <i class="bi bi-arrow-left me-1"></i>Back to Company
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="/company" class="btn btn-outline-secondary">
 | 
			
		||||
                <i class="bi bi-building me-1"></i>All Companies
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Success/Error Messages -->
 | 
			
		||||
    {% if success %}
 | 
			
		||||
    <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-check-circle me-2"></i>{{ success }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if error %}
 | 
			
		||||
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <!-- Edit Form -->
 | 
			
		||||
    <div class="card">
 | 
			
		||||
        <div class="card-header bg-light">
 | 
			
		||||
            <h5 class="mb-0"><i class="bi bi-building me-2"></i>Company Information</h5>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <form action="/company/edit/{{ company.base_data.id }}" method="post" id="editCompanyForm">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <!-- Basic Information -->
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <h6 class="text-muted mb-3">Basic Information</h6>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="company_name" class="form-label">Company Name <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <input type="text" class="form-control" id="company_name" name="company_name"
 | 
			
		||||
                                value="{{ company.name }}" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="company_type" class="form-label">Company Type <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <select class="form-select" id="company_type" name="company_type" required>
 | 
			
		||||
                                <option value="Startup FZC" {% if company.business_type=="Starter" %}selected{% endif
 | 
			
		||||
                                    %}>Startup FZC</option>
 | 
			
		||||
                                <option value="Growth FZC" {% if company.business_type=="Global" %}selected{% endif %}>
 | 
			
		||||
                                    Growth FZC</option>
 | 
			
		||||
                                <option value="Cooperative FZC" {% if company.business_type=="Coop" %}selected{% endif
 | 
			
		||||
                                    %}>Cooperative FZC</option>
 | 
			
		||||
                                <option value="Single FZC" {% if company.business_type=="Single" %}selected{% endif %}>
 | 
			
		||||
                                    Single FZC</option>
 | 
			
		||||
                                <option value="Twin FZC" {% if company.business_type=="Twin" %}selected{% endif %}>Twin
 | 
			
		||||
                                    FZC</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="status" class="form-label">Status</label>
 | 
			
		||||
                            <select class="form-select" id="status" name="status">
 | 
			
		||||
                                <option value="Active" {% if company.status=="Active" %}selected{% endif %}>Active
 | 
			
		||||
                                </option>
 | 
			
		||||
                                <option value="Inactive" {% if company.status=="Inactive" %}selected{% endif %}>Inactive
 | 
			
		||||
                                </option>
 | 
			
		||||
                                <option value="Suspended" {% if company.status=="Suspended" %}selected{% endif %}>
 | 
			
		||||
                                    Suspended</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="industry" class="form-label">Industry</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="industry" name="industry"
 | 
			
		||||
                                value="{{ company.industry | default(value='') }}">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="fiscal_year_end" class="form-label">Fiscal Year End</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="fiscal_year_end" name="fiscal_year_end"
 | 
			
		||||
                                value="{{ company.fiscal_year_end | default(value='') }}"
 | 
			
		||||
                                placeholder="MM-DD (e.g., 12-31)" pattern="^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"
 | 
			
		||||
                                title="Enter date in MM-DD format (e.g., 12-31)">
 | 
			
		||||
                            <div class="form-text">Enter the last day of your company's fiscal year (MM-DD format)</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <!-- Contact Information -->
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <h6 class="text-muted mb-3">Contact Information</h6>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="email" class="form-label">Email</label>
 | 
			
		||||
                            <input type="email" class="form-control" id="email" name="email"
 | 
			
		||||
                                value="{{ company.email | default(value='') }}">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="phone" class="form-label">Phone</label>
 | 
			
		||||
                            <input type="tel" class="form-control" id="phone" name="phone"
 | 
			
		||||
                                value="{{ company.phone | default(value='') }}">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="website" class="form-label">Website</label>
 | 
			
		||||
                            <input type="url" class="form-control" id="website" name="website"
 | 
			
		||||
                                value="{{ company.website | default(value='') }}" placeholder="https://example.com">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="address" class="form-label">Address</label>
 | 
			
		||||
                            <textarea class="form-control" id="address" name="address"
 | 
			
		||||
                                rows="3">{{ company.address | default(value='') }}</textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Description -->
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div class="col-12">
 | 
			
		||||
                        <h6 class="text-muted mb-3">Additional Information</h6>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Company Description</label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="4"
 | 
			
		||||
                                placeholder="Describe the company's purpose and activities">{{ company.description | default(value='') }}</textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Read-only Information -->
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div class="col-12">
 | 
			
		||||
                        <h6 class="text-muted mb-3">Registration Information (Read-only)</h6>
 | 
			
		||||
                        <div class="row">
 | 
			
		||||
                            <div class="col-md-4">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label">Registration Number</label>
 | 
			
		||||
                                    <input type="text" class="form-control" value="{{ company.registration_number }}"
 | 
			
		||||
                                        readonly>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-4">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label">Incorporation Date</label>
 | 
			
		||||
                                    <input type="text" class="form-control" value="{{ incorporation_date_formatted }}"
 | 
			
		||||
                                        readonly>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-4">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label class="form-label">Company ID</label>
 | 
			
		||||
                                    <input type="text" class="form-control" value="{{ company.base_data.id }}" readonly>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Form Actions -->
 | 
			
		||||
                <div class="d-flex justify-content-between">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <a href="/company/view/{{ company.base_data.id }}" class="btn btn-outline-secondary">
 | 
			
		||||
                            <i class="bi bi-x-circle me-1"></i>Cancel
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <button type="submit" class="btn btn-primary">
 | 
			
		||||
                            <i class="bi bi-check-circle me-1"></i>Update Company
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
{{ super() }}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Form validation
 | 
			
		||||
        const form = document.getElementById('editCompanyForm');
 | 
			
		||||
        const companyName = document.getElementById('company_name');
 | 
			
		||||
 | 
			
		||||
        form.addEventListener('submit', function (e) {
 | 
			
		||||
            if (companyName.value.trim() === '') {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                showValidationAlert('Company name is required', companyName);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Function to show validation alert with consistent styling
 | 
			
		||||
        function showValidationAlert(message, focusElement) {
 | 
			
		||||
            // Remove existing alerts
 | 
			
		||||
            const existingAlerts = document.querySelectorAll('.validation-alert');
 | 
			
		||||
            existingAlerts.forEach(alert => alert.remove());
 | 
			
		||||
 | 
			
		||||
            // Create new alert
 | 
			
		||||
            const alertDiv = document.createElement('div');
 | 
			
		||||
            alertDiv.className = 'alert alert-warning alert-dismissible fade show validation-alert mt-3';
 | 
			
		||||
            alertDiv.innerHTML = `
 | 
			
		||||
                <div class="d-flex align-items-center">
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle me-2"></i>
 | 
			
		||||
                    <span>${message}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            // Insert alert at the top of the form
 | 
			
		||||
            const form = document.getElementById('editCompanyForm');
 | 
			
		||||
            form.insertBefore(alertDiv, form.firstChild);
 | 
			
		||||
 | 
			
		||||
            // Focus on the problematic field
 | 
			
		||||
            if (focusElement) {
 | 
			
		||||
                focusElement.focus();
 | 
			
		||||
                focusElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Auto-dismiss after 5 seconds
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                if (alertDiv.parentNode) {
 | 
			
		||||
                    alertDiv.remove();
 | 
			
		||||
                }
 | 
			
		||||
            }, 5000);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Auto-format website URL
 | 
			
		||||
        const websiteInput = document.getElementById('website');
 | 
			
		||||
        websiteInput.addEventListener('blur', function () {
 | 
			
		||||
            let value = this.value.trim();
 | 
			
		||||
            if (value && !value.startsWith('http://') && !value.startsWith('https://')) {
 | 
			
		||||
                this.value = 'https://' + value;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -15,71 +15,55 @@
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {% if companies and companies|length > 0 %}
 | 
			
		||||
                {% for company in companies %}
 | 
			
		||||
                <!-- Example rows -->
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>{{ company.name }}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {% if company.business_type == "Starter" %}Startup FZC
 | 
			
		||||
                        {% elif company.business_type == "Global" %}Growth FZC
 | 
			
		||||
                        {% elif company.business_type == "Coop" %}Cooperative FZC
 | 
			
		||||
                        {% elif company.business_type == "Single" %}Single FZC
 | 
			
		||||
                        {% elif company.business_type == "Twin" %}Twin FZC
 | 
			
		||||
                        {% else %}{{ company.business_type }}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {% if company.status == "Active" %}
 | 
			
		||||
                        <span class="badge bg-success">Active</span>
 | 
			
		||||
                        {% elif company.status == "Inactive" %}
 | 
			
		||||
                        <span class="badge bg-secondary">Inactive</span>
 | 
			
		||||
                        {% elif company.status == "Suspended" %}
 | 
			
		||||
                        <span class="badge bg-warning text-dark">Suspended</span>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <span class="badge bg-secondary">{{ company.status }}</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>{{ company.incorporation_date | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                    <td>Zanzibar Digital Solutions</td>
 | 
			
		||||
                    <td>Startup FZC</td>
 | 
			
		||||
                    <td><span class="badge bg-success">Active</span></td>
 | 
			
		||||
                    <td>2025-04-01</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div class="btn-group">
 | 
			
		||||
                            <a href="/company/view/{{ company.base_data.id }}" class="btn btn-sm btn-outline-primary">
 | 
			
		||||
                                <i class="bi bi-eye"></i> View
 | 
			
		||||
                            </a>
 | 
			
		||||
                            <a href="/company/switch/{{ company.base_data.id }}" class="btn btn-sm btn-primary">
 | 
			
		||||
                                <i class="bi bi-box-arrow-in-right"></i> Switch to Entity
 | 
			
		||||
                            </a>
 | 
			
		||||
                            <a href="/company/view/company1" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
 | 
			
		||||
                            <a href="/company/switch/company1" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td colspan="5" class="text-center py-4">
 | 
			
		||||
                        <div class="text-muted">
 | 
			
		||||
                            <i class="bi bi-building display-4 mb-3"></i>
 | 
			
		||||
                            <h5>No Companies Found</h5>
 | 
			
		||||
                            <p>You haven't registered any companies yet. Get started by registering your first company.
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <button class="btn btn-primary" onclick="document.querySelector('#register-tab').click()">
 | 
			
		||||
                                <i class="bi bi-plus-circle me-1"></i> Register Your First Company
 | 
			
		||||
                            </button>
 | 
			
		||||
                    <td>Blockchain Innovations Ltd</td>
 | 
			
		||||
                    <td>Growth FZC</td>
 | 
			
		||||
                    <td><span class="badge bg-success">Active</span></td>
 | 
			
		||||
                    <td>2025-03-15</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div class="btn-group">
 | 
			
		||||
                            <a href="/company/view/company2" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
 | 
			
		||||
                            <a href="/company/switch/company2" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>Sustainable Energy Cooperative</td>
 | 
			
		||||
                    <td>Cooperative FZC</td>
 | 
			
		||||
                    <td><span class="badge bg-warning text-dark">Pending</span></td>
 | 
			
		||||
                    <td>2025-05-01</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div class="btn-group">
 | 
			
		||||
                            <a href="/company/view/company3" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
 | 
			
		||||
                            <a href="/company/switch/company3" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <!-- More rows dynamically rendered here -->
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Company Details Modal -->
 | 
			
		||||
<div class="modal fade" id="companyDetailsModal" tabindex="-1" aria-labelledby="companyDetailsModalLabel"
 | 
			
		||||
    aria-hidden="true">
 | 
			
		||||
<div class="modal fade" id="companyDetailsModal" tabindex="-1" aria-labelledby="companyDetailsModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog modal-lg">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header bg-light">
 | 
			
		||||
                <h5 class="modal-title" id="companyDetailsModalLabel"><i class="bi bi-building me-2"></i>Company Details
 | 
			
		||||
                </h5>
 | 
			
		||||
                <h5 class="modal-title" id="companyDetailsModalLabel"><i class="bi bi-building me-2"></i>Company Details</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
@@ -137,7 +121,7 @@
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="row mb-4">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="card h-100">
 | 
			
		||||
@@ -202,9 +186,8 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
 | 
			
		||||
                <button type="button" class="btn btn-primary" onclick="switchToEntityFromModal()"><i
 | 
			
		||||
                        class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</button>
 | 
			
		||||
                <button type="button" class="btn btn-primary" onclick="switchToEntityFromModal()"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Payment Error - Company Registration{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container mt-4">
 | 
			
		||||
    <div class="row justify-content-center">
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <div class="card border-danger">
 | 
			
		||||
                <div class="card-header bg-danger text-white text-center">
 | 
			
		||||
                    <h3 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-exclamation-triangle-fill me-2"></i>
 | 
			
		||||
                        Payment Error
 | 
			
		||||
                    </h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <i class="bi bi-x-circle text-danger" style="font-size: 4rem;"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <h4 class="text-danger mb-3">Payment Processing Failed</h4>
 | 
			
		||||
                    
 | 
			
		||||
                    <p class="lead mb-4">
 | 
			
		||||
                        We encountered an issue processing your payment. Your company registration could not be completed.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    
 | 
			
		||||
                    {% if error %}
 | 
			
		||||
                    <div class="alert alert-danger">
 | 
			
		||||
                        <h6><i class="bi bi-exclamation-circle me-2"></i>Error Details</h6>
 | 
			
		||||
                        <p class="mb-0">{{ error }}</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        <h6><i class="bi bi-info-circle me-2"></i>What You Can Do</h6>
 | 
			
		||||
                        <ul class="list-unstyled mb-0 text-start">
 | 
			
		||||
                            <li><i class="bi bi-arrow-right me-2"></i>Check your payment method details</li>
 | 
			
		||||
                            <li><i class="bi bi-arrow-right me-2"></i>Ensure you have sufficient funds</li>
 | 
			
		||||
                            <li><i class="bi bi-arrow-right me-2"></i>Try a different payment method</li>
 | 
			
		||||
                            <li><i class="bi bi-arrow-right me-2"></i>Contact your bank if the issue persists</li>
 | 
			
		||||
                            <li><i class="bi bi-arrow-right me-2"></i>Contact our support team for assistance</li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="d-grid gap-2 d-md-flex justify-content-md-center">
 | 
			
		||||
                        <a href="/company?tab=register" class="btn btn-primary btn-lg">
 | 
			
		||||
                            <i class="bi bi-arrow-clockwise me-2"></i>Try Again
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/contact" class="btn btn-outline-primary btn-lg">
 | 
			
		||||
                            <i class="bi bi-envelope me-2"></i>Contact Support
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer text-muted text-center">
 | 
			
		||||
                    <small>
 | 
			
		||||
                        <i class="bi bi-shield-check me-1"></i>
 | 
			
		||||
                        No charges were made to your account
 | 
			
		||||
                    </small>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .card {
 | 
			
		||||
        box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .alert-info {
 | 
			
		||||
        border-left: 4px solid #0dcaf0;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .alert-danger {
 | 
			
		||||
        border-left: 4px solid #dc3545;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,89 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Payment Successful - Company Registration{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container mt-4">
 | 
			
		||||
    <div class="row justify-content-center">
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <div class="card border-success">
 | 
			
		||||
                <div class="card-header bg-success text-white text-center">
 | 
			
		||||
                    <h3 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-check-circle-fill me-2"></i>
 | 
			
		||||
                        Payment Successful!
 | 
			
		||||
                    </h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <h4 class="text-success mb-3">Company Registration Complete</h4>
 | 
			
		||||
                    
 | 
			
		||||
                    <p class="lead mb-4">
 | 
			
		||||
                        Congratulations! Your payment has been processed successfully and your company has been registered.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="row mb-4">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="card bg-light">
 | 
			
		||||
                                <div class="card-body">
 | 
			
		||||
                                    <h6 class="card-title">Company ID</h6>
 | 
			
		||||
                                    <p class="card-text h5 text-primary">{{ company_id }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="card bg-light">
 | 
			
		||||
                                <div class="card-body">
 | 
			
		||||
                                    <h6 class="card-title">Payment ID</h6>
 | 
			
		||||
                                    <p class="card-text h6 text-muted">{{ payment_intent_id }}</p>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        <h6><i class="bi bi-info-circle me-2"></i>What's Next?</h6>
 | 
			
		||||
                        <ul class="list-unstyled mb-0 text-start">
 | 
			
		||||
                            <li><i class="bi bi-check me-2"></i>You will receive a confirmation email shortly</li>
 | 
			
		||||
                            <li><i class="bi bi-check me-2"></i>Your company documents will be prepared within 24 hours</li>
 | 
			
		||||
                            <li><i class="bi bi-check me-2"></i>You can now access your company dashboard</li>
 | 
			
		||||
                            <li><i class="bi bi-check me-2"></i>Your subscription billing will begin next month</li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="d-grid gap-2 d-md-flex justify-content-md-center">
 | 
			
		||||
                        <a href="/company" class="btn btn-primary btn-lg">
 | 
			
		||||
                            <i class="bi bi-building me-2"></i>Go to Company Dashboard
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/company/view/{{ company_id }}" class="btn btn-outline-primary btn-lg">
 | 
			
		||||
                            <i class="bi bi-eye me-2"></i>View Company Details
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer text-muted text-center">
 | 
			
		||||
                    <small>
 | 
			
		||||
                        <i class="bi bi-shield-check me-1"></i>
 | 
			
		||||
                        Your payment was processed securely by Stripe
 | 
			
		||||
                    </small>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .card {
 | 
			
		||||
        box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .bg-light {
 | 
			
		||||
        background-color: #f8f9fa !important;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .alert-info {
 | 
			
		||||
        border-left: 4px solid #0dcaf0;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,80 +1,31 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ company.name }} - Company Details{% endblock %}
 | 
			
		||||
{% block title %}{{ company_name }} - Company Details{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
{{ super() }}
 | 
			
		||||
<style>
 | 
			
		||||
    .badge-signed {
 | 
			
		||||
        background-color: #198754;
 | 
			
		||||
        color: white;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .badge-pending {
 | 
			
		||||
        background-color: #ffc107;
 | 
			
		||||
        color: #212529;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
    {{ super() }}
 | 
			
		||||
    <style>
 | 
			
		||||
        .badge-signed {
 | 
			
		||||
            background-color: #198754;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
        .badge-pending {
 | 
			
		||||
            background-color: #ffc107;
 | 
			
		||||
            color: #212529;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid py-4">
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center mb-4">
 | 
			
		||||
        <h2><i class="bi bi-building me-2"></i>{{ company.name }}</h2>
 | 
			
		||||
        <h2><i class="bi bi-building me-2"></i>{{ company_name }}</h2>
 | 
			
		||||
        <div>
 | 
			
		||||
            <a href="/company" class="btn btn-outline-secondary me-2"><i class="bi bi-arrow-left me-1"></i>Back to
 | 
			
		||||
                Companies</a>
 | 
			
		||||
            <a href="/company/switch/{{ company.base_data.id }}" class="btn btn-primary"><i
 | 
			
		||||
                    class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
 | 
			
		||||
            <a href="/company" class="btn btn-outline-secondary me-2"><i class="bi bi-arrow-left me-1"></i>Back to Companies</a>
 | 
			
		||||
            <a href="/company/switch/{{ company_id }}" class="btn btn-primary"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Profile Completion Status -->
 | 
			
		||||
    {% if not company.email or company.email == "" or not company.phone or company.phone == "" or not company.address or
 | 
			
		||||
    company.address == "" %}
 | 
			
		||||
    <div class="alert alert-info alert-dismissible fade show" role="alert">
 | 
			
		||||
        <div class="d-flex align-items-center">
 | 
			
		||||
            <div class="me-3">
 | 
			
		||||
                <i class="bi bi-info-circle fs-4"></i>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="flex-grow-1">
 | 
			
		||||
                <h6 class="alert-heading mb-1">Complete Your Company Profile</h6>
 | 
			
		||||
                <p class="mb-2">Your company profile is missing some essential information. Add the missing details to
 | 
			
		||||
                    improve your company's visibility and professionalism.</p>
 | 
			
		||||
                <div class="d-flex gap-2">
 | 
			
		||||
                    <a href="/company/edit/{{ company.base_data.id }}" class="btn btn-sm btn-outline-info">
 | 
			
		||||
                        <i class="bi bi-pencil me-1"></i>Complete Profile
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <small class="text-muted align-self-center">
 | 
			
		||||
                        Missing:
 | 
			
		||||
                        {% if not company.email or company.email == "" %}Email{% endif %}
 | 
			
		||||
                        {% if not company.phone or company.phone == "" %}{% if not company.email or company.email == ""
 | 
			
		||||
                        %}, {% endif %}Phone{% endif %}
 | 
			
		||||
                        {% if not company.address or company.address == "" %}{% if not company.email or company.email ==
 | 
			
		||||
                        "" or not company.phone or company.phone == "" %}, {% endif %}Address{% endif %}
 | 
			
		||||
                    </small>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <!-- Success/Error Messages -->
 | 
			
		||||
    {% if success %}
 | 
			
		||||
    <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-check-circle me-2"></i>{{ success }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if error %}
 | 
			
		||||
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
 | 
			
		||||
        <i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
@@ -85,49 +36,29 @@
 | 
			
		||||
                    <table class="table table-borderless">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th style="width: 30%">Company Name:</th>
 | 
			
		||||
                            <td>{{ company.name }}</td>
 | 
			
		||||
                            <td>{{ company_name }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Type:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.business_type == "Starter" %}Startup FZC
 | 
			
		||||
                                {% elif company.business_type == "Global" %}Growth FZC
 | 
			
		||||
                                {% elif company.business_type == "Coop" %}Cooperative FZC
 | 
			
		||||
                                {% elif company.business_type == "Single" %}Single FZC
 | 
			
		||||
                                {% elif company.business_type == "Twin" %}Twin FZC
 | 
			
		||||
                                {% else %}{{ company.business_type }}
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Registration Number:</th>
 | 
			
		||||
                            <td>{{ company.registration_number }}</td>
 | 
			
		||||
                            <td>{{ company_type }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Registration Date:</th>
 | 
			
		||||
                            <td>{{ incorporation_date_formatted }}</td>
 | 
			
		||||
                            <td>{{ registration_date }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Status:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.status == "Active" %}
 | 
			
		||||
                                <span class="badge bg-success">{{ company.status }}</span>
 | 
			
		||||
                                {% elif company.status == "Inactive" %}
 | 
			
		||||
                                <span class="badge bg-secondary">{{ company.status }}</span>
 | 
			
		||||
                                {% elif company.status == "Suspended" %}
 | 
			
		||||
                                <span class="badge bg-warning text-dark">{{ company.status }}</span>
 | 
			
		||||
                                {% if status == "Active" %}
 | 
			
		||||
                                    <span class="badge bg-success">{{ status }}</span>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="badge bg-secondary">{{ company.status }}</span>
 | 
			
		||||
                                    <span class="badge bg-warning text-dark">{{ status }}</span>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Industry:</th>
 | 
			
		||||
                            <td>{{ company.industry | default(value="Not specified") }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Description:</th>
 | 
			
		||||
                            <td>{{ company.description | default(value="No description provided") }}</td>
 | 
			
		||||
                            <th>Purpose:</th>
 | 
			
		||||
                            <td>{{ purpose }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -136,86 +67,28 @@
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header bg-light">
 | 
			
		||||
                    <h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Additional Information</h5>
 | 
			
		||||
                    <h5 class="mb-0"><i class="bi bi-credit-card me-2"></i>Billing Information</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <table class="table table-borderless">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th style="width: 30%">Email:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.email and company.email != "" %}
 | 
			
		||||
                                {{ company.email }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="text-muted">Not provided</span>
 | 
			
		||||
                                <a href="/company/edit/{{ company.base_data.id }}"
 | 
			
		||||
                                    class="btn btn-sm btn-outline-secondary ms-2">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-1"></i>Add
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <th style="width: 30%">Plan:</th>
 | 
			
		||||
                            <td>{{ plan }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Phone:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.phone and company.phone != "" %}
 | 
			
		||||
                                {{ company.phone }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="text-muted">Not provided</span>
 | 
			
		||||
                                <a href="/company/edit/{{ company.base_data.id }}"
 | 
			
		||||
                                    class="btn btn-sm btn-outline-secondary ms-2">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-1"></i>Add
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <th>Next Billing:</th>
 | 
			
		||||
                            <td>{{ next_billing }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Website:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.website and company.website != "" %}
 | 
			
		||||
                                <a href="{{ company.website }}" target="_blank">{{ company.website }}</a>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="text-muted">Not provided</span>
 | 
			
		||||
                                <a href="/company/edit/{{ company.base_data.id }}"
 | 
			
		||||
                                    class="btn btn-sm btn-outline-secondary ms-2">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-1"></i>Add
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Address:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.address and company.address != "" %}
 | 
			
		||||
                                {{ company.address }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="text-muted">Not provided</span>
 | 
			
		||||
                                <a href="/company/edit/{{ company.base_data.id }}"
 | 
			
		||||
                                    class="btn btn-sm btn-outline-secondary ms-2">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-1"></i>Add
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Fiscal Year End:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if company.fiscal_year_end and company.fiscal_year_end != "" %}
 | 
			
		||||
                                {{ company.fiscal_year_end }}
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="text-muted">Not specified</span>
 | 
			
		||||
                                <a href="/company/edit/{{ company.base_data.id }}"
 | 
			
		||||
                                    class="btn btn-sm btn-outline-secondary ms-2">
 | 
			
		||||
                                    <i class="bi bi-plus-circle me-1"></i>Add
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <th>Payment Method:</th>
 | 
			
		||||
                            <td>{{ payment_method }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
@@ -231,21 +104,12 @@
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% if shareholders and shareholders|length > 0 %}
 | 
			
		||||
                            {% for shareholder in shareholders %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td>{{ shareholder.name }}</td>
 | 
			
		||||
                                <td>{{ shareholder.percentage }}%</td>
 | 
			
		||||
                                <td>{{ shareholder.0 }}</td>
 | 
			
		||||
                                <td>{{ shareholder.1 }}</td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td colspan="2" class="text-center text-muted py-3">
 | 
			
		||||
                                    <i class="bi bi-people me-1"></i>
 | 
			
		||||
                                    No shareholders registered yet
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -254,91 +118,49 @@
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header bg-light">
 | 
			
		||||
                    <h5 class="mb-0"><i class="bi bi-credit-card me-2"></i>Billing & Payment</h5>
 | 
			
		||||
                    <h5 class="mb-0"><i class="bi bi-file-earmark-text me-2"></i>Contracts</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if payment_info %}
 | 
			
		||||
                    <table class="table table-borderless">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th style="width: 40%">Payment Status:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% if payment_info.status == "Succeeded" %}
 | 
			
		||||
                                <span class="badge bg-success">
 | 
			
		||||
                                    <i class="bi bi-check-circle me-1"></i>Paid
 | 
			
		||||
                                </span>
 | 
			
		||||
                                {% elif payment_info.status == "Pending" %}
 | 
			
		||||
                                <span class="badge bg-warning">
 | 
			
		||||
                                    <i class="bi bi-clock me-1"></i>Pending
 | 
			
		||||
                                </span>
 | 
			
		||||
                                {% elif payment_info.status == "Failed" %}
 | 
			
		||||
                                <span class="badge bg-danger">
 | 
			
		||||
                                    <i class="bi bi-x-circle me-1"></i>Failed
 | 
			
		||||
                                </span>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <span class="badge bg-secondary">{{ payment_info.status }}</span>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Payment Plan:</th>
 | 
			
		||||
                            <td>{{ payment_plan_display }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Setup Fee:</th>
 | 
			
		||||
                            <td>${{ payment_info.setup_fee }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Monthly Fee:</th>
 | 
			
		||||
                            <td>${{ payment_info.monthly_fee }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Total Paid:</th>
 | 
			
		||||
                            <td><strong>${{ payment_info.total_amount }}</strong></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Payment Date:</th>
 | 
			
		||||
                            <td>{{ payment_created_formatted }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        {% if payment_completed_formatted %}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Completed:</th>
 | 
			
		||||
                            <td>{{ payment_completed_formatted }}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if payment_info.payment_intent_id %}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th>Payment ID:</th>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <code class="small">{{ payment_info.payment_intent_id }}</code>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    <table class="table table-striped">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <th>Contract</th>
 | 
			
		||||
                                <th>Status</th>
 | 
			
		||||
                                <th>Action</th>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% for contract in contracts %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td>{{ contract.0 }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    {% if contract.1 == "Signed" %}
 | 
			
		||||
                                        <span class="badge bg-success">{{ contract.1 }}</span>
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                        <span class="badge bg-warning text-dark">{{ contract.1 }}</span>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <a href="/contracts/view/{{ contract.0 | lower | replace(from=' ', to='-') }}" class="btn btn-sm btn-outline-primary">View</a>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center text-muted py-3">
 | 
			
		||||
                        <i class="bi bi-credit-card me-1"></i>
 | 
			
		||||
                        No payment information available
 | 
			
		||||
                        <br>
 | 
			
		||||
                        <small class="text-muted">This company may have been created before payment integration</small>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    <div class="card mb-4">
 | 
			
		||||
        <div class="card-header bg-light">
 | 
			
		||||
            <h5 class="mb-0"><i class="bi bi-gear me-2"></i>Actions</h5>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <div class="d-flex gap-2">
 | 
			
		||||
                <a href="/company/edit/{{ company.base_data.id }}" class="btn btn-outline-primary"><i
 | 
			
		||||
                        class="bi bi-pencil me-1"></i>Edit Company</a>
 | 
			
		||||
                <a href="/company/documents/{{ company.base_data.id }}" class="btn btn-outline-secondary"><i
 | 
			
		||||
                        class="bi bi-file-earmark me-1"></i>Manage Documents</a>
 | 
			
		||||
                <a href="/company/switch/{{ company.base_data.id }}" class="btn btn-primary"><i
 | 
			
		||||
                        class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
 | 
			
		||||
                <a href="/company/edit/{{ company_id }}" class="btn btn-outline-primary"><i class="bi bi-pencil me-1"></i>Edit Company</a>
 | 
			
		||||
                <a href="/company/documents/{{ company_id }}" class="btn btn-outline-secondary"><i class="bi bi-file-earmark me-1"></i>Manage Documents</a>
 | 
			
		||||
                <a href="/company/switch/{{ company_id }}" class="btn btn-primary"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -346,10 +168,10 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
{{ super() }}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        console.log('Company view page loaded');
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
    {{ super() }}
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
            console.log('Company view page loaded');
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,200 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Add Signer - {{ contract.title }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col">
 | 
			
		||||
            <nav aria-label="breadcrumb">
 | 
			
		||||
                <ol class="breadcrumb">
 | 
			
		||||
                    <li class="breadcrumb-item"><a href="/contracts">Contracts</a></li>
 | 
			
		||||
                    <li class="breadcrumb-item"><a href="/contracts/{{ contract.id }}">{{ contract.title }}</a></li>
 | 
			
		||||
                    <li class="breadcrumb-item active" aria-current="page">Add Signer</li>
 | 
			
		||||
                </ol>
 | 
			
		||||
            </nav>
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h1 class="h3 mb-0">Add Signer</h1>
 | 
			
		||||
                    <p class="text-muted mb-0">Add a new signer to "{{ contract.title }}"</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary">
 | 
			
		||||
                        <i class="bi bi-arrow-left me-1"></i> Back to Contract
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <!-- Add Signer Form -->
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-person-plus me-2"></i>Signer Information
 | 
			
		||||
                    </h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if error %}
 | 
			
		||||
                    <div class="alert alert-danger" role="alert">
 | 
			
		||||
                        <i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <form method="post" action="/contracts/{{ contract.id }}/add-signer">
 | 
			
		||||
                        <div class="row">
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label for="name" class="form-label">
 | 
			
		||||
                                        Full Name <span class="text-danger">*</span>
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                    <input type="text" class="form-control" id="name" name="name" 
 | 
			
		||||
                                           placeholder="Enter signer's full name" required>
 | 
			
		||||
                                    <div class="form-text">The full legal name of the person who will sign</div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <div class="mb-3">
 | 
			
		||||
                                    <label for="email" class="form-label">
 | 
			
		||||
                                        Email Address <span class="text-danger">*</span>
 | 
			
		||||
                                    </label>
 | 
			
		||||
                                    <input type="email" class="form-control" id="email" name="email" 
 | 
			
		||||
                                           placeholder="Enter signer's email address" required>
 | 
			
		||||
                                    <div class="form-text">Email where signing instructions will be sent</div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-4">
 | 
			
		||||
                            <div class="alert alert-info">
 | 
			
		||||
                                <i class="bi bi-info-circle me-2"></i>
 | 
			
		||||
                                <strong>Note:</strong> The signer will receive an email with a secure link to sign the contract once you send it for signatures.
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="d-flex gap-2">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">
 | 
			
		||||
                                <i class="bi bi-person-plus me-1"></i> Add Signer
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <a href="/contracts/{{ contract.id }}" class="btn btn-secondary">
 | 
			
		||||
                                Cancel
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Contract Summary -->
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Contract Summary</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h6 class="card-title">{{ contract.title }}</h6>
 | 
			
		||||
                    <p class="card-text text-muted">{{ contract.description }}</p>
 | 
			
		||||
                    
 | 
			
		||||
                    <hr>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="row text-center">
 | 
			
		||||
                        <div class="col-6">
 | 
			
		||||
                            <div class="border-end">
 | 
			
		||||
                                <div class="h4 mb-0 text-primary">{{ contract.signers|length }}</div>
 | 
			
		||||
                                <small class="text-muted">Current Signers</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-6">
 | 
			
		||||
                            <div class="h4 mb-0 text-success">{{ contract.signed_signers }}</div>
 | 
			
		||||
                            <small class="text-muted">Signed</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Current Signers List -->
 | 
			
		||||
            {% if contract.signers|length > 0 %}
 | 
			
		||||
            <div class="card mt-3">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h6 class="mb-0">Current Signers</h6>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    <ul class="list-group list-group-flush">
 | 
			
		||||
                        {% for signer in contract.signers %}
 | 
			
		||||
                        <li class="list-group-item d-flex justify-content-between align-items-center">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="fw-medium">{{ signer.name }}</div>
 | 
			
		||||
                                <small class="text-muted">{{ signer.email }}</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
 | 
			
		||||
                                {{ signer.status }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        // Form validation
 | 
			
		||||
        const form = document.querySelector('form');
 | 
			
		||||
        const nameInput = document.getElementById('name');
 | 
			
		||||
        const emailInput = document.getElementById('email');
 | 
			
		||||
 | 
			
		||||
        form.addEventListener('submit', function(e) {
 | 
			
		||||
            let isValid = true;
 | 
			
		||||
            
 | 
			
		||||
            // Clear previous validation states
 | 
			
		||||
            nameInput.classList.remove('is-invalid');
 | 
			
		||||
            emailInput.classList.remove('is-invalid');
 | 
			
		||||
            
 | 
			
		||||
            // Validate name
 | 
			
		||||
            if (nameInput.value.trim().length < 2) {
 | 
			
		||||
                nameInput.classList.add('is-invalid');
 | 
			
		||||
                isValid = false;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Validate email format
 | 
			
		||||
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 | 
			
		||||
            if (!emailRegex.test(emailInput.value)) {
 | 
			
		||||
                emailInput.classList.add('is-invalid');
 | 
			
		||||
                isValid = false;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (!isValid) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Real-time validation feedback
 | 
			
		||||
        nameInput.addEventListener('input', function() {
 | 
			
		||||
            if (this.value.trim().length >= 2) {
 | 
			
		||||
                this.classList.remove('is-invalid');
 | 
			
		||||
                this.classList.add('is-valid');
 | 
			
		||||
            } else {
 | 
			
		||||
                this.classList.remove('is-valid');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        emailInput.addEventListener('input', function() {
 | 
			
		||||
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 | 
			
		||||
            if (emailRegex.test(this.value)) {
 | 
			
		||||
                this.classList.remove('is-invalid');
 | 
			
		||||
                this.classList.add('is-valid');
 | 
			
		||||
            } else {
 | 
			
		||||
                this.classList.remove('is-valid');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,128 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}All Contract Activities{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <!-- Header -->
 | 
			
		||||
            <div class="row mb-4">
 | 
			
		||||
                <div class="col-12">
 | 
			
		||||
                    <h1 class="display-5 mb-3">Contract Activities</h1>
 | 
			
		||||
                    <p class="lead">Complete history of contract actions and events across your organization.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Activities List -->
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="card-title mb-0">
 | 
			
		||||
                        <i class="bi bi-activity"></i> Contract Activity History
 | 
			
		||||
                    </h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if activities %}
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div class="col-12">
 | 
			
		||||
                            <div class="table-responsive">
 | 
			
		||||
                                <table class="table table-hover">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th width="50">Type</th>
 | 
			
		||||
                                            <th>User</th>
 | 
			
		||||
                                            <th>Action</th>
 | 
			
		||||
                                            <th>Contract</th>
 | 
			
		||||
                                            <th width="150">Date</th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        {% for activity in activities %}
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <i class="{{ activity.icon }}"></i>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <strong>{{ activity.user }}</strong>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                {{ activity.action }}
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <span class="text-decoration-none">
 | 
			
		||||
                                                    {{ activity.contract_title }}
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <small class="text-muted">
 | 
			
		||||
                                                    {{ activity.created_at | date(format="%Y-%m-%d %H:%M") }}
 | 
			
		||||
                                                </small>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <i class="bi bi-activity display-1 text-muted"></i>
 | 
			
		||||
                        <h4 class="mt-3">No Activities Yet</h4>
 | 
			
		||||
                        <p class="text-muted">
 | 
			
		||||
                            Contract activities will appear here as users create contracts and add signers.
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <a href="/contracts/create" class="btn btn-primary">
 | 
			
		||||
                            <i class="bi bi-plus-circle"></i> Create First Contract
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Activity Statistics -->
 | 
			
		||||
            {% if activities %}
 | 
			
		||||
            <div class="row mt-4">
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">{{ activities | length }}</h5>
 | 
			
		||||
                            <p class="card-text text-muted">Total Activities</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">
 | 
			
		||||
                                <i class="bi bi-file-earmark-text text-primary"></i>
 | 
			
		||||
                            </h5>
 | 
			
		||||
                            <p class="card-text text-muted">Contract Timeline</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">
 | 
			
		||||
                                <i class="bi bi-people text-success"></i>
 | 
			
		||||
                            </h5>
 | 
			
		||||
                            <p class="card-text text-muted">Team Collaboration</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            <!-- Back to Dashboard -->
 | 
			
		||||
            <div class="row mt-4">
 | 
			
		||||
                <div class="col-12 text-center">
 | 
			
		||||
                    <a href="/contracts" class="btn btn-outline-secondary">
 | 
			
		||||
                        <i class="bi bi-arrow-left"></i> Back to Contracts Dashboard
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -36,41 +36,27 @@
 | 
			
		||||
                            <label for="status" class="form-label">Status</label>
 | 
			
		||||
                            <select class="form-select" id="status" name="status">
 | 
			
		||||
                                <option value="">All Statuses</option>
 | 
			
		||||
                                <option value="Draft" {% if current_status_filter=="Draft" %}selected{% endif %}>Draft
 | 
			
		||||
                                </option>
 | 
			
		||||
                                <option value="PendingSignatures" {% if current_status_filter=="PendingSignatures"
 | 
			
		||||
                                    %}selected{% endif %}>Pending Signatures</option>
 | 
			
		||||
                                <option value="Signed" {% if current_status_filter=="Signed" %}selected{% endif %}>
 | 
			
		||||
                                    Signed</option>
 | 
			
		||||
                                <option value="Expired" {% if current_status_filter=="Expired" %}selected{% endif %}>
 | 
			
		||||
                                    Expired</option>
 | 
			
		||||
                                <option value="Cancelled" {% if current_status_filter=="Cancelled" %}selected{% endif
 | 
			
		||||
                                    %}>Cancelled</option>
 | 
			
		||||
                                <option value="Draft">Draft</option>
 | 
			
		||||
                                <option value="PendingSignatures">Pending Signatures</option>
 | 
			
		||||
                                <option value="Signed">Signed</option>
 | 
			
		||||
                                <option value="Expired">Expired</option>
 | 
			
		||||
                                <option value="Cancelled">Cancelled</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="type" class="form-label">Contract Type</label>
 | 
			
		||||
                            <select class="form-select" id="type" name="type">
 | 
			
		||||
                                <option value="">All Types</option>
 | 
			
		||||
                                <option value="Service Agreement" {% if current_type_filter=="Service Agreement"
 | 
			
		||||
                                    %}selected{% endif %}>Service Agreement</option>
 | 
			
		||||
                                <option value="Employment Contract" {% if current_type_filter=="Employment Contract"
 | 
			
		||||
                                    %}selected{% endif %}>Employment Contract</option>
 | 
			
		||||
                                <option value="Non-Disclosure Agreement" {% if
 | 
			
		||||
                                    current_type_filter=="Non-Disclosure Agreement" %}selected{% endif %}>Non-Disclosure
 | 
			
		||||
                                    Agreement</option>
 | 
			
		||||
                                <option value="Service Level Agreement" {% if
 | 
			
		||||
                                    current_type_filter=="Service Level Agreement" %}selected{% endif %}>Service Level
 | 
			
		||||
                                    Agreement</option>
 | 
			
		||||
                                <option value="Other" {% if current_type_filter=="Other" %}selected{% endif %}>Other
 | 
			
		||||
                                </option>
 | 
			
		||||
                                <option value="Service">Service Agreement</option>
 | 
			
		||||
                                <option value="Employment">Employment Contract</option>
 | 
			
		||||
                                <option value="NDA">Non-Disclosure Agreement</option>
 | 
			
		||||
                                <option value="SLA">Service Level Agreement</option>
 | 
			
		||||
                                <option value="Other">Other</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="search" class="form-label">Search</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="search" name="search"
 | 
			
		||||
                                placeholder="Search by title or description"
 | 
			
		||||
                                value="{{ current_search_filter | default(value='') }}">
 | 
			
		||||
                            <input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-3 d-flex align-items-end">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary w-100">Apply Filters</button>
 | 
			
		||||
@@ -112,8 +98,7 @@
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ contract.contract_type }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span
 | 
			
		||||
                                            class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
 | 
			
		||||
                                        <span class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
 | 
			
		||||
                                            {{ contract.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
@@ -127,14 +112,9 @@
 | 
			
		||||
                                                <i class="bi bi-eye"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% if contract.status == 'Draft' %}
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit"
 | 
			
		||||
                                                class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                                                <i class="bi bi-pencil"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            <button class="btn btn-sm btn-outline-danger"
 | 
			
		||||
                                                onclick="deleteContract({{ contract.id }}, '{{ contract.title | replace(from="'", to="\\'") }}')">
 | 
			
		||||
                                                <i class="bi bi-trash"></i>
 | 
			
		||||
                                            </button>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
@@ -157,70 +137,4 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Delete Confirmation Modal -->
 | 
			
		||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <div class="alert alert-danger">
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle me-2"></i>
 | 
			
		||||
                    <strong>Warning:</strong> This action cannot be undone!
 | 
			
		||||
                </div>
 | 
			
		||||
                <p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
 | 
			
		||||
                <p>This will permanently remove the contract and all its associated data.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                <button type="button" class="btn btn-danger" id="confirmDeleteBtn">
 | 
			
		||||
                    <i class="bi bi-trash me-1"></i> Delete Contract
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    console.log('Contracts list scripts loading...');
 | 
			
		||||
 | 
			
		||||
    // Delete function using Bootstrap modal
 | 
			
		||||
    window.deleteContract = function (contractId, contractTitle) {
 | 
			
		||||
        console.log('Delete function called:', contractId, contractTitle);
 | 
			
		||||
 | 
			
		||||
        // Set the contract title in the modal
 | 
			
		||||
        document.getElementById('contractTitle').textContent = contractTitle;
 | 
			
		||||
 | 
			
		||||
        // Store the contract ID for later use
 | 
			
		||||
        window.currentDeleteContractId = contractId;
 | 
			
		||||
 | 
			
		||||
        // Show the modal
 | 
			
		||||
        const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
 | 
			
		||||
        deleteModal.show();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    console.log('deleteContract function defined:', typeof window.deleteContract);
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Handle confirm delete button click
 | 
			
		||||
        document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
 | 
			
		||||
            console.log('User confirmed deletion, submitting form...');
 | 
			
		||||
 | 
			
		||||
            // Create and submit form
 | 
			
		||||
            const form = document.createElement('form');
 | 
			
		||||
            form.method = 'POST';
 | 
			
		||||
            form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
 | 
			
		||||
            form.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
            document.body.appendChild(form);
 | 
			
		||||
            form.submit();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -25,14 +25,12 @@
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/contracts/create" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="title" class="form-label">Contract Title <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <label for="title" class="form-label">Contract Title <span class="text-danger">*</span></label>
 | 
			
		||||
                            <input type="text" class="form-control" id="title" name="title" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="contract_type" class="form-label">Contract Type <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <label for="contract_type" class="form-label">Contract Type <span class="text-danger">*</span></label>
 | 
			
		||||
                            <select class="form-select" id="contract_type" name="contract_type" required>
 | 
			
		||||
                                <option value="" selected disabled>Select a contract type</option>
 | 
			
		||||
                                {% for type in contract_types %}
 | 
			
		||||
@@ -40,59 +38,28 @@
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Description <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="3"
 | 
			
		||||
                                required></textarea>
 | 
			
		||||
                            <label for="description" class="form-label">Description <span class="text-danger">*</span></label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="3" required></textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="content" class="form-label">Contract Content (Markdown)</label>
 | 
			
		||||
                            <textarea class="form-control" id="content" name="content" rows="10" placeholder="# Contract Title
 | 
			
		||||
 | 
			
		||||
## 1. Introduction
 | 
			
		||||
This contract outlines the terms and conditions...
 | 
			
		||||
 | 
			
		||||
## 2. Scope of Work
 | 
			
		||||
- Task 1
 | 
			
		||||
- Task 2
 | 
			
		||||
- Task 3
 | 
			
		||||
 | 
			
		||||
## 3. Payment Terms
 | 
			
		||||
Payment will be made according to the following schedule:
 | 
			
		||||
 | 
			
		||||
| Milestone | Amount | Due Date |
 | 
			
		||||
|-----------|--------|----------|
 | 
			
		||||
| Start | $1,000 | Upon signing |
 | 
			
		||||
| Completion | $2,000 | Upon delivery |
 | 
			
		||||
 | 
			
		||||
## 4. Terms and Conditions
 | 
			
		||||
**Important:** All parties must agree to these terms.
 | 
			
		||||
 | 
			
		||||
> This is a blockquote for important notices.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
*For questions, contact [support@example.com](mailto:support@example.com)*"></textarea>
 | 
			
		||||
                            <div class="form-text">
 | 
			
		||||
                                <strong>Markdown Support:</strong> You can use markdown formatting including headers
 | 
			
		||||
                                (#), lists (-), tables (|), bold (**text**), italic (*text*), links, and more.
 | 
			
		||||
                                <a href="/editor" target="_blank">Open Markdown Editor</a> for a live preview.
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <label for="content" class="form-label">Contract Content</label>
 | 
			
		||||
                            <textarea class="form-control" id="content" name="content" rows="10"></textarea>
 | 
			
		||||
                            <div class="form-text">You can leave this blank and add content later.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="effective_date" class="form-label">Effective Date</label>
 | 
			
		||||
                            <input type="date" class="form-control" id="effective_date" name="effective_date">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="expiration_date" class="form-label">Expiration Date</label>
 | 
			
		||||
                            <input type="date" class="form-control" id="expiration_date" name="expiration_date">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
 | 
			
		||||
                            <a href="/contracts" class="btn btn-outline-secondary me-md-2">Cancel</a>
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">Create Contract</button>
 | 
			
		||||
@@ -101,15 +68,14 @@ Payment will be made according to the following schedule:
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Tips</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <p>Creating a new contract is just the first step. After creating the contract, you'll be able to:
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>Creating a new contract is just the first step. After creating the contract, you'll be able to:</p>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li>Add signers who need to approve the contract</li>
 | 
			
		||||
                        <li>Edit the contract content</li>
 | 
			
		||||
@@ -119,7 +85,7 @@ Payment will be made according to the following schedule:
 | 
			
		||||
                    <p>The contract will be in <strong>Draft</strong> status until you send it for signatures.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Contract Templates</h5>
 | 
			
		||||
@@ -127,20 +93,16 @@ Payment will be made according to the following schedule:
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <p>You can use one of our pre-defined templates to get started quickly:</p>
 | 
			
		||||
                    <div class="list-group">
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action"
 | 
			
		||||
                            onclick="loadTemplate('nda')">
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('nda')">
 | 
			
		||||
                            Non-Disclosure Agreement
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action"
 | 
			
		||||
                            onclick="loadTemplate('service')">
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('service')">
 | 
			
		||||
                            Service Agreement
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action"
 | 
			
		||||
                            onclick="loadTemplate('employment')">
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('employment')">
 | 
			
		||||
                            Employment Contract
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action"
 | 
			
		||||
                            onclick="loadTemplate('sla')">
 | 
			
		||||
                        <button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('sla')">
 | 
			
		||||
                            Service Level Agreement
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -159,101 +121,19 @@ Payment will be made according to the following schedule:
 | 
			
		||||
        let description = '';
 | 
			
		||||
        let content = '';
 | 
			
		||||
        let contractType = '';
 | 
			
		||||
 | 
			
		||||
        switch (type) {
 | 
			
		||||
        
 | 
			
		||||
        switch(type) {
 | 
			
		||||
            case 'nda':
 | 
			
		||||
                title = 'Non-Disclosure Agreement';
 | 
			
		||||
                description = 'Standard NDA for protecting confidential information';
 | 
			
		||||
                contractType = 'Non-Disclosure Agreement';
 | 
			
		||||
                content = `# Non-Disclosure Agreement
 | 
			
		||||
 | 
			
		||||
This Non-Disclosure Agreement (the "**Agreement**") is entered into as of **[DATE]** by and between **[PARTY A]** and **[PARTY B]**.
 | 
			
		||||
 | 
			
		||||
## 1. Definition of Confidential Information
 | 
			
		||||
 | 
			
		||||
"Confidential Information" means any and all information disclosed by either party to the other party, whether orally or in writing, whether or not marked, designated or otherwise identified as "confidential."
 | 
			
		||||
 | 
			
		||||
## 2. Obligations of Receiving Party
 | 
			
		||||
 | 
			
		||||
The receiving party agrees to:
 | 
			
		||||
- Hold all Confidential Information in strict confidence
 | 
			
		||||
- Not disclose any Confidential Information to third parties
 | 
			
		||||
- Use Confidential Information solely for the purpose of evaluating potential business relationships
 | 
			
		||||
 | 
			
		||||
## 3. Term
 | 
			
		||||
 | 
			
		||||
This Agreement shall remain in effect for a period of **[DURATION]** years from the date first written above.
 | 
			
		||||
 | 
			
		||||
## 4. Return of Materials
 | 
			
		||||
 | 
			
		||||
Upon termination of this Agreement, each party shall promptly return all documents and materials containing Confidential Information.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**IN WITNESS WHEREOF**, the parties have executed this Agreement as of the date first written above.
 | 
			
		||||
 | 
			
		||||
**[PARTY A]**                    **[PARTY B]**
 | 
			
		||||
 | 
			
		||||
_____________________           _____________________
 | 
			
		||||
Signature                       Signature
 | 
			
		||||
 | 
			
		||||
_____________________           _____________________
 | 
			
		||||
Print Name                      Print Name
 | 
			
		||||
 | 
			
		||||
_____________________           _____________________
 | 
			
		||||
Date                           Date`;
 | 
			
		||||
                content = 'This Non-Disclosure Agreement (the "Agreement") is entered into as of [DATE] by and between [PARTY A] and [PARTY B].\n\n1. Definition of Confidential Information\n2. Obligations of Receiving Party\n3. Term\n...';
 | 
			
		||||
                break;
 | 
			
		||||
            case 'service':
 | 
			
		||||
                title = 'Service Agreement';
 | 
			
		||||
                description = 'Agreement for providing professional services';
 | 
			
		||||
                contractType = 'Service Agreement';
 | 
			
		||||
                content = `# Service Agreement
 | 
			
		||||
 | 
			
		||||
This Service Agreement (the "**Agreement**") is made and entered into as of **[DATE]** by and between **[SERVICE PROVIDER]** and **[CLIENT]**.
 | 
			
		||||
 | 
			
		||||
## 1. Services to be Provided
 | 
			
		||||
 | 
			
		||||
The Service Provider agrees to provide the following services:
 | 
			
		||||
 | 
			
		||||
- **[SERVICE 1]**: Description of service
 | 
			
		||||
- **[SERVICE 2]**: Description of service
 | 
			
		||||
- **[SERVICE 3]**: Description of service
 | 
			
		||||
 | 
			
		||||
## 2. Compensation
 | 
			
		||||
 | 
			
		||||
| Service | Rate | Payment Terms |
 | 
			
		||||
|---------|------|---------------|
 | 
			
		||||
| [SERVICE 1] | $[AMOUNT] | [TERMS] |
 | 
			
		||||
| [SERVICE 2] | $[AMOUNT] | [TERMS] |
 | 
			
		||||
 | 
			
		||||
**Total Contract Value**: $[TOTAL_AMOUNT]
 | 
			
		||||
 | 
			
		||||
## 3. Payment Schedule
 | 
			
		||||
 | 
			
		||||
- **Deposit**: [PERCENTAGE]% upon signing
 | 
			
		||||
- **Milestone 1**: [PERCENTAGE]% upon [MILESTONE]
 | 
			
		||||
- **Final Payment**: [PERCENTAGE]% upon completion
 | 
			
		||||
 | 
			
		||||
## 4. Term and Termination
 | 
			
		||||
 | 
			
		||||
This Agreement shall commence on **[START_DATE]** and shall continue until **[END_DATE]** unless terminated earlier.
 | 
			
		||||
 | 
			
		||||
> **Important**: Either party may terminate this agreement with [NUMBER] days written notice.
 | 
			
		||||
 | 
			
		||||
## 5. Deliverables
 | 
			
		||||
 | 
			
		||||
The Service Provider shall deliver:
 | 
			
		||||
 | 
			
		||||
1. [DELIVERABLE 1]
 | 
			
		||||
2. [DELIVERABLE 2]
 | 
			
		||||
3. [DELIVERABLE 3]
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Service Provider**              **Client**
 | 
			
		||||
 | 
			
		||||
_____________________           _____________________
 | 
			
		||||
Signature                       Signature`;
 | 
			
		||||
                content = 'This Service Agreement (the "Agreement") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Services to be Provided\n2. Compensation\n3. Term and Termination\n...';
 | 
			
		||||
                break;
 | 
			
		||||
            case 'employment':
 | 
			
		||||
                title = 'Employment Contract';
 | 
			
		||||
@@ -268,19 +148,19 @@ Signature                       Signature`;
 | 
			
		||||
                content = 'This Service Level Agreement (the "SLA") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Service Levels\n2. Performance Metrics\n3. Remedies for Failure\n...';
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        document.getElementById('title').value = title;
 | 
			
		||||
        document.getElementById('description').value = description;
 | 
			
		||||
        document.getElementById('content').value = content;
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        // Set the select option
 | 
			
		||||
        const selectElement = document.getElementById('contract_type');
 | 
			
		||||
        for (let i = 0; i < selectElement.options.length; i++) {
 | 
			
		||||
            if (selectElement.options[i].text === contractType) {
 | 
			
		||||
        for(let i = 0; i < selectElement.options.length; i++) {
 | 
			
		||||
            if(selectElement.options[i].text === contractType) {
 | 
			
		||||
                selectElement.selectedIndex = i;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,215 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Edit Contract{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <nav aria-label="breadcrumb">
 | 
			
		||||
                <ol class="breadcrumb">
 | 
			
		||||
                    <li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
 | 
			
		||||
                    <li class="breadcrumb-item"><a href="/contracts/{{ contract.id }}">{{ contract.title }}</a></li>
 | 
			
		||||
                    <li class="breadcrumb-item active" aria-current="page">Edit Contract</li>
 | 
			
		||||
                </ol>
 | 
			
		||||
            </nav>
 | 
			
		||||
            <h1 class="display-5 mb-3">Edit Contract</h1>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Contract Details</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/contracts/{{ contract.id }}/edit" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="title" class="form-label">Contract Title <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <input type="text" class="form-control" id="title" name="title" value="{{ contract.title }}"
 | 
			
		||||
                                required>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="contract_type" class="form-label">Contract Type <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <select class="form-select" id="contract_type" name="contract_type" required>
 | 
			
		||||
                                {% for type in contract_types %}
 | 
			
		||||
                                <option value="{{ type }}" {% if contract.contract_type==type %}selected{% endif %}>{{
 | 
			
		||||
                                    type }}</option>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Description <span
 | 
			
		||||
                                    class="text-danger">*</span></label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="3"
 | 
			
		||||
                                required>{{ contract.description }}</textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="content" class="form-label">Contract Content</label>
 | 
			
		||||
                            <textarea class="form-control" id="content" name="content"
 | 
			
		||||
                                rows="10">{{ contract.terms_and_conditions | default(value='') }}</textarea>
 | 
			
		||||
                            <div class="form-text">Edit the contract content as needed.</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="effective_date" class="form-label">Effective Date</label>
 | 
			
		||||
                            <input type="date" class="form-control" id="effective_date" name="effective_date"
 | 
			
		||||
                                value="{% if contract.start_date %}{{ contract.start_date | date(format='%Y-%m-%d') }}{% endif %}">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="expiration_date" class="form-label">Expiration Date</label>
 | 
			
		||||
                            <input type="date" class="form-control" id="expiration_date" name="expiration_date"
 | 
			
		||||
                                value="{% if contract.end_date %}{{ contract.end_date | date(format='%Y-%m-%d') }}{% endif %}">
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
 | 
			
		||||
                            <a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary me-md-2">Cancel</a>
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">Update Contract</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Contract Info</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <p><strong>Status:</strong>
 | 
			
		||||
                        <span class="badge bg-secondary">{{ contract.status }}</span>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p><strong>Created:</strong> {{ contract.created_at | date(format="%Y-%m-%d %H:%M") }}</p>
 | 
			
		||||
                    <p><strong>Last Updated:</strong> {{ contract.updated_at | date(format="%Y-%m-%d %H:%M") }}</p>
 | 
			
		||||
                    <p><strong>Version:</strong> {{ contract.current_version }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Edit Notes</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="alert alert-info">
 | 
			
		||||
                        <i class="bi bi-info-circle me-2"></i>
 | 
			
		||||
                        <strong>Note:</strong> Only contracts in <strong>Draft</strong> status can be edited.
 | 
			
		||||
                        Once a contract is sent for signatures, you'll need to create a new revision instead.
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <p>After updating the contract:</p>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li>The contract will remain in Draft status</li>
 | 
			
		||||
                        <li>You can continue to make changes</li>
 | 
			
		||||
                        <li>Add signers when ready</li>
 | 
			
		||||
                        <li>Send for signatures when complete</li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Quick Actions</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-grid gap-2">
 | 
			
		||||
                        <a href="/contracts/{{ contract.id }}" class="btn btn-outline-primary">
 | 
			
		||||
                            <i class="bi bi-eye me-1"></i> View Contract
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a href="/contracts/{{ contract.id }}/add-signer" class="btn btn-outline-success">
 | 
			
		||||
                            <i class="bi bi-person-plus me-1"></i> Add Signer
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <button class="btn btn-outline-danger"
 | 
			
		||||
                            onclick="deleteContract({{ contract.id }}, '{{ contract.title | replace(from="'", to="\\'") }}')">
 | 
			
		||||
                            <i class="bi bi-trash me-1"></i> Delete Contract
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Delete Confirmation Modal -->
 | 
			
		||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <div class="alert alert-danger">
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle me-2"></i>
 | 
			
		||||
                    <strong>Warning:</strong> This action cannot be undone!
 | 
			
		||||
                </div>
 | 
			
		||||
                <p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
 | 
			
		||||
                <p>This will permanently remove the contract and all its associated data.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                <button type="button" class="btn btn-danger" id="confirmDeleteBtn">
 | 
			
		||||
                    <i class="bi bi-trash me-1"></i> Delete Contract
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    console.log('Edit contract scripts loading...');
 | 
			
		||||
 | 
			
		||||
    // Delete function using Bootstrap modal
 | 
			
		||||
    window.deleteContract = function (contractId, contractTitle) {
 | 
			
		||||
        console.log('Delete function called:', contractId, contractTitle);
 | 
			
		||||
 | 
			
		||||
        // Set the contract title in the modal
 | 
			
		||||
        document.getElementById('contractTitle').textContent = contractTitle;
 | 
			
		||||
 | 
			
		||||
        // Store the contract ID for later use
 | 
			
		||||
        window.currentDeleteContractId = contractId;
 | 
			
		||||
 | 
			
		||||
        // Show the modal
 | 
			
		||||
        const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
 | 
			
		||||
        deleteModal.show();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    console.log('deleteContract function defined:', typeof window.deleteContract);
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Handle confirm delete button click
 | 
			
		||||
        document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
 | 
			
		||||
            console.log('User confirmed deletion, submitting form...');
 | 
			
		||||
 | 
			
		||||
            // Create and submit form
 | 
			
		||||
            const form = document.createElement('form');
 | 
			
		||||
            form.method = 'POST';
 | 
			
		||||
            form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
 | 
			
		||||
            form.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
            document.body.appendChild(form);
 | 
			
		||||
            form.submit();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Auto-resize textarea
 | 
			
		||||
        const textarea = document.getElementById('content');
 | 
			
		||||
        if (textarea) {
 | 
			
		||||
            textarea.addEventListener('input', function () {
 | 
			
		||||
                this.style.height = 'auto';
 | 
			
		||||
                this.style.height = this.scrollHeight + 'px';
 | 
			
		||||
            });
 | 
			
		||||
            // Initial resize
 | 
			
		||||
            textarea.style.height = textarea.scrollHeight + 'px';
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -11,108 +11,58 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if stats.total_contracts > 0 %}
 | 
			
		||||
    <!-- Statistics Cards -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-primary h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Total</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.total_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Total</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.total_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-secondary h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Draft</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.draft_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Draft</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.draft_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-warning h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Pending</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.pending_signature_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Pending</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.pending_signature_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-success h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Signed</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.signed_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Signed</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.signed_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-danger h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Expired</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.expired_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Expired</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.expired_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-2 mb-3">
 | 
			
		||||
            <div class="card text-white bg-dark h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title mb-1">Cancelled</h5>
 | 
			
		||||
                    <h3 class="mb-0">{{ stats.cancelled_contracts }}</h3>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Cancelled</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.cancelled_contracts }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% else %}
 | 
			
		||||
    <!-- Empty State Welcome Message -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card border-0 bg-light">
 | 
			
		||||
                <div class="card-body text-center py-5">
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <i class="bi bi-file-earmark-text display-1 text-muted"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <h3 class="text-muted mb-3">Welcome to Contract Management</h3>
 | 
			
		||||
                    <p class="lead text-muted mb-4">
 | 
			
		||||
                        You haven't created any contracts yet. Get started by creating your first contract to manage
 | 
			
		||||
                        legal agreements and track signatures.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <div class="row justify-content-center">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="row g-3">
 | 
			
		||||
                                <div class="col-md-6">
 | 
			
		||||
                                    <div class="card h-100 border-primary">
 | 
			
		||||
                                        <div class="card-body text-center">
 | 
			
		||||
                                            <i class="bi bi-plus-circle text-primary fs-2 mb-2"></i>
 | 
			
		||||
                                            <h6 class="card-title">Create Contract</h6>
 | 
			
		||||
                                            <p class="card-text small text-muted">Start with a new legal agreement</p>
 | 
			
		||||
                                            <a href="/contracts/create" class="btn btn-primary btn-sm">Get Started</a>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="col-md-6">
 | 
			
		||||
                                    <div class="card h-100 border-success">
 | 
			
		||||
                                        <div class="card-body text-center">
 | 
			
		||||
                                            <i class="bi bi-question-circle text-success fs-2 mb-2"></i>
 | 
			
		||||
                                            <h6 class="card-title">Need Help?</h6>
 | 
			
		||||
                                            <p class="card-text small text-muted">Learn how to use the system</p>
 | 
			
		||||
                                            <button class="btn btn-outline-success btn-sm"
 | 
			
		||||
                                                onclick="showHelpModal()">Learn More</button>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    {% if stats.total_contracts > 0 %}
 | 
			
		||||
    <!-- Quick Actions -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
@@ -136,7 +86,6 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <!-- Pending Signature Contracts -->
 | 
			
		||||
    {% if pending_signature_contracts and pending_signature_contracts | length > 0 %}
 | 
			
		||||
@@ -219,8 +168,7 @@
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
 | 
			
		||||
                                                <i class="bi bi-eye"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit"
 | 
			
		||||
                                                class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                                                <i class="bi bi-pencil"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </div>
 | 
			
		||||
@@ -235,115 +183,5 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <!-- Recent Activity Section -->
 | 
			
		||||
    {% if recent_activities and recent_activities | length > 0 %}
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Recent Activity</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    <div class="list-group list-group-flush">
 | 
			
		||||
                        {% for activity in recent_activities %}
 | 
			
		||||
                        <div class="list-group-item border-start-0 border-end-0 py-3">
 | 
			
		||||
                            <div class="d-flex">
 | 
			
		||||
                                <div class="me-3">
 | 
			
		||||
                                    <i class="{{ activity.icon }} fs-5"></i>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="flex-grow-1">
 | 
			
		||||
                                    <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                        <strong>{{ activity.user }}</strong>
 | 
			
		||||
                                        <small class="text-muted">{{ activity.timestamp | date(format="%H:%M")
 | 
			
		||||
                                            }}</small>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <p class="mb-1">{{ activity.description }}</p>
 | 
			
		||||
                                    <small class="text-muted">{{ activity.title }}</small>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer text-center">
 | 
			
		||||
                    <a href="/contracts/activities" class="btn btn-sm btn-outline-info">See More Activities</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Help Modal -->
 | 
			
		||||
<div class="modal fade" id="helpModal" tabindex="-1" aria-labelledby="helpModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog modal-lg">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="helpModalLabel">
 | 
			
		||||
                    <i class="bi bi-question-circle me-2"></i>Getting Started with Contract Management
 | 
			
		||||
                </h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <h6><i class="bi bi-1-circle text-primary me-2"></i>Create Your First Contract</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            Start by creating a new contract. Choose from various contract types like Service
 | 
			
		||||
                            Agreements, NDAs, or Employment Contracts.
 | 
			
		||||
                        </p>
 | 
			
		||||
 | 
			
		||||
                        <h6><i class="bi bi-2-circle text-primary me-2"></i>Add Contract Details</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            Fill in the contract title, description, and terms. You can use Markdown formatting for rich
 | 
			
		||||
                            text content.
 | 
			
		||||
                        </p>
 | 
			
		||||
 | 
			
		||||
                        <h6><i class="bi bi-3-circle text-primary me-2"></i>Add Signers</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            Add people who need to sign the contract. Each signer will receive a unique signing link.
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <h6><i class="bi bi-4-circle text-success me-2"></i>Send for Signatures</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            Once your contract is ready, send it for signatures. Signers can review and sign digitally.
 | 
			
		||||
                        </p>
 | 
			
		||||
 | 
			
		||||
                        <h6><i class="bi bi-5-circle text-success me-2"></i>Track Progress</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            Monitor signature progress, send reminders, and view signed documents from the dashboard.
 | 
			
		||||
                        </p>
 | 
			
		||||
 | 
			
		||||
                        <h6><i class="bi bi-6-circle text-success me-2"></i>Manage Contracts</h6>
 | 
			
		||||
                        <p class="small text-muted mb-3">
 | 
			
		||||
                            View all contracts, filter by status, and manage the complete contract lifecycle.
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="alert alert-info mt-3">
 | 
			
		||||
                    <i class="bi bi-lightbulb me-2"></i>
 | 
			
		||||
                    <strong>Tip:</strong> You can save contracts as drafts and come back to edit them later before
 | 
			
		||||
                    sending for signatures.
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
 | 
			
		||||
                <a href="/contracts/create" class="btn btn-primary">
 | 
			
		||||
                    <i class="bi bi-plus-circle me-1"></i> Create My First Contract
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    function showHelpModal() {
 | 
			
		||||
        const helpModal = new bootstrap.Modal(document.getElementById('helpModal'));
 | 
			
		||||
        helpModal.show();
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -13,10 +13,7 @@
 | 
			
		||||
                </ol>
 | 
			
		||||
            </nav>
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h1 class="display-5 mb-0">My Contracts</h1>
 | 
			
		||||
                    <p class="text-muted mb-0">Manage and track your personal contracts</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <h1 class="display-5 mb-0">My Contracts</h1>
 | 
			
		||||
                <div class="btn-group">
 | 
			
		||||
                    <a href="/contracts/create" class="btn btn-primary">
 | 
			
		||||
                        <i class="bi bi-plus-circle me-1"></i> Create New Contract
 | 
			
		||||
@@ -26,136 +23,41 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Quick Stats -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card bg-primary text-white">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-between">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h6 class="card-title">Total Contracts</h6>
 | 
			
		||||
                            <h3 class="mb-0">{{ contracts|length }}</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="align-self-center">
 | 
			
		||||
                            <i class="bi bi-file-earmark-text fs-2"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card bg-warning text-white">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-between">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h6 class="card-title">Pending Signatures</h6>
 | 
			
		||||
                            <h3 class="mb-0" id="pending-count">0</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="align-self-center">
 | 
			
		||||
                            <i class="bi bi-clock fs-2"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card bg-success text-white">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-between">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h6 class="card-title">Signed</h6>
 | 
			
		||||
                            <h3 class="mb-0" id="signed-count">0</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="align-self-center">
 | 
			
		||||
                            <i class="bi bi-check-circle fs-2"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3">
 | 
			
		||||
            <div class="card bg-secondary text-white">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-between">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <h6 class="card-title">Drafts</h6>
 | 
			
		||||
                            <h3 class="mb-0" id="draft-count">0</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="align-self-center">
 | 
			
		||||
                            <i class="bi bi-pencil fs-2"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Filters -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-funnel me-1"></i> Filters & Search
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
 | 
			
		||||
                        data-bs-target="#filtersCollapse" aria-expanded="false" aria-controls="filtersCollapse">
 | 
			
		||||
                        <i class="bi bi-chevron-down"></i> Toggle Filters
 | 
			
		||||
                    </button>
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Filters</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="collapse show" id="filtersCollapse">
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <form action="/contracts/my-contracts" method="get" class="row g-3">
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="status" class="form-label">Status</label>
 | 
			
		||||
                                <select class="form-select" id="status" name="status">
 | 
			
		||||
                                    <option value="">All Statuses</option>
 | 
			
		||||
                                    <option value="Draft" {% if current_status_filter=="Draft" %}selected{% endif %}>
 | 
			
		||||
                                        Draft</option>
 | 
			
		||||
                                    <option value="PendingSignatures" {% if current_status_filter=="PendingSignatures"
 | 
			
		||||
                                        %}selected{% endif %}>Pending Signatures</option>
 | 
			
		||||
                                    <option value="Signed" {% if current_status_filter=="Signed" %}selected{% endif %}>
 | 
			
		||||
                                        Signed</option>
 | 
			
		||||
                                    <option value="Active" {% if current_status_filter=="Active" %}selected{% endif %}>
 | 
			
		||||
                                        Active</option>
 | 
			
		||||
                                    <option value="Expired" {% if current_status_filter=="Expired" %}selected{% endif
 | 
			
		||||
                                        %}>Expired</option>
 | 
			
		||||
                                    <option value="Cancelled" {% if current_status_filter=="Cancelled" %}selected{%
 | 
			
		||||
                                        endif %}>Cancelled</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="type" class="form-label">Contract Type</label>
 | 
			
		||||
                                <select class="form-select" id="type" name="type">
 | 
			
		||||
                                    <option value="">All Types</option>
 | 
			
		||||
                                    <option value="Service Agreement" {% if current_type_filter=="Service Agreement"
 | 
			
		||||
                                        %}selected{% endif %}>Service Agreement</option>
 | 
			
		||||
                                    <option value="Employment Contract" {% if current_type_filter=="Employment Contract"
 | 
			
		||||
                                        %}selected{% endif %}>Employment Contract</option>
 | 
			
		||||
                                    <option value="Non-Disclosure Agreement" {% if
 | 
			
		||||
                                        current_type_filter=="Non-Disclosure Agreement" %}selected{% endif %}>
 | 
			
		||||
                                        Non-Disclosure Agreement</option>
 | 
			
		||||
                                    <option value="Service Level Agreement" {% if
 | 
			
		||||
                                        current_type_filter=="Service Level Agreement" %}selected{% endif %}>Service
 | 
			
		||||
                                        Level Agreement</option>
 | 
			
		||||
                                    <option value="Partnership Agreement" {% if
 | 
			
		||||
                                        current_type_filter=="Partnership Agreement" %}selected{% endif %}>Partnership
 | 
			
		||||
                                        Agreement</option>
 | 
			
		||||
                                    <option value="Other" {% if current_type_filter=="Other" %}selected{% endif %}>Other
 | 
			
		||||
                                    </option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="search" class="form-label">Search</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="search" name="search"
 | 
			
		||||
                                    placeholder="Search by title or description"
 | 
			
		||||
                                    value="{{ current_search_filter | default(value='') }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-3 d-flex align-items-end">
 | 
			
		||||
                                <button type="submit" class="btn btn-primary w-100">Apply Filters</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/contracts/my-contracts" method="get" class="row g-3">
 | 
			
		||||
                        <div class="col-md-4">
 | 
			
		||||
                            <label for="status" class="form-label">Status</label>
 | 
			
		||||
                            <select class="form-select" id="status" name="status">
 | 
			
		||||
                                <option value="">All Statuses</option>
 | 
			
		||||
                                <option value="Draft">Draft</option>
 | 
			
		||||
                                <option value="PendingSignatures">Pending Signatures</option>
 | 
			
		||||
                                <option value="Signed">Signed</option>
 | 
			
		||||
                                <option value="Expired">Expired</option>
 | 
			
		||||
                                <option value="Cancelled">Cancelled</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-4">
 | 
			
		||||
                            <label for="type" class="form-label">Contract Type</label>
 | 
			
		||||
                            <select class="form-select" id="type" name="type">
 | 
			
		||||
                                <option value="">All Types</option>
 | 
			
		||||
                                <option value="Service">Service Agreement</option>
 | 
			
		||||
                                <option value="Employment">Employment Contract</option>
 | 
			
		||||
                                <option value="NDA">Non-Disclosure Agreement</option>
 | 
			
		||||
                                <option value="SLA">Service Level Agreement</option>
 | 
			
		||||
                                <option value="Other">Other</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-4 d-flex align-items-end">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary w-100">Apply Filters</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -165,122 +67,48 @@
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-file-earmark-text me-1"></i> My Contracts
 | 
			
		||||
                        {% if contracts and contracts | length > 0 %}
 | 
			
		||||
                        <span class="badge bg-primary ms-2">{{ contracts|length }}</span>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <div class="btn-group">
 | 
			
		||||
                        <a href="/contracts/statistics" class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                            <i class="bi bi-graph-up me-1"></i> Statistics
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">My Contracts</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if contracts and contracts | length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover align-middle">
 | 
			
		||||
                            <thead class="table-light">
 | 
			
		||||
                        <table class="table table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th scope="col">
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            Contract Title
 | 
			
		||||
                                            <i class="bi bi-arrow-down-up ms-1 text-muted" style="cursor: pointer;"
 | 
			
		||||
                                                onclick="sortTable(0)"></i>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </th>
 | 
			
		||||
                                    <th scope="col">Type</th>
 | 
			
		||||
                                    <th scope="col">Status</th>
 | 
			
		||||
                                    <th scope="col">Progress</th>
 | 
			
		||||
                                    <th scope="col">
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            Created
 | 
			
		||||
                                            <i class="bi bi-arrow-down-up ms-1 text-muted" style="cursor: pointer;"
 | 
			
		||||
                                                onclick="sortTable(4)"></i>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </th>
 | 
			
		||||
                                    <th scope="col">Last Updated</th>
 | 
			
		||||
                                    <th scope="col" class="text-center">Actions</th>
 | 
			
		||||
                                    <th>Contract Title</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Status</th>
 | 
			
		||||
                                    <th>Signers</th>
 | 
			
		||||
                                    <th>Created</th>
 | 
			
		||||
                                    <th>Updated</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for contract in contracts %}
 | 
			
		||||
                                <tr
 | 
			
		||||
                                    class="{% if contract.status == 'Expired' %}table-danger{% elif contract.status == 'PendingSignatures' %}table-warning{% elif contract.status == 'Signed' %}table-success{% endif %}">
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div>
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}" class="fw-bold text-decoration-none">
 | 
			
		||||
                                                {{ contract.title }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% if contract.description %}
 | 
			
		||||
                                            <div class="small text-muted">{{ contract.description }}</div>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ contract.contract_type }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge bg-light text-dark">{{ contract.contract_type }}</span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span
 | 
			
		||||
                                            class="badge {% if contract.status == 'Signed' or contract.status == 'Active' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% elif contract.status == 'Cancelled' %}bg-dark{% else %}bg-info{% endif %}">
 | 
			
		||||
                                            {% if contract.status == 'PendingSignatures' %}
 | 
			
		||||
                                            <i class="bi bi-clock me-1"></i>
 | 
			
		||||
                                            {% elif contract.status == 'Signed' %}
 | 
			
		||||
                                            <i class="bi bi-check-circle me-1"></i>
 | 
			
		||||
                                            {% elif contract.status == 'Draft' %}
 | 
			
		||||
                                            <i class="bi bi-pencil me-1"></i>
 | 
			
		||||
                                            {% elif contract.status == 'Expired' %}
 | 
			
		||||
                                            <i class="bi bi-exclamation-triangle me-1"></i>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        <span class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
 | 
			
		||||
                                            {{ contract.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ contract.signed_signers }}/{{ contract.signers|length }}</td>
 | 
			
		||||
                                    <td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>{{ contract.updated_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if contract.signers|length > 0 %}
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            <div class="progress me-2" style="width: 60px; height: 8px;">
 | 
			
		||||
                                                <div class="progress-bar bg-success" role="progressbar"
 | 
			
		||||
                                                    style="width: 0%" data-contract-id="{{ contract.id }}">
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <small class="text-muted">{{ contract.signed_signers }}/{{
 | 
			
		||||
                                                contract.signers|length }}</small>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                        <span class="text-muted small">No signers</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="small">
 | 
			
		||||
                                            {{ contract.created_at | date(format="%b %d, %Y") }}
 | 
			
		||||
                                            <div class="text-muted">{{ contract.created_at | date(format="%I:%M %p") }}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="small">
 | 
			
		||||
                                            {{ contract.updated_at | date(format="%b %d, %Y") }}
 | 
			
		||||
                                            <div class="text-muted">{{ contract.updated_at | date(format="%I:%M %p") }}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td class="text-center">
 | 
			
		||||
                                        <div class="btn-group">
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary"
 | 
			
		||||
                                                title="View Details">
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
 | 
			
		||||
                                                <i class="bi bi-eye"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% if contract.status == 'Draft' %}
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit"
 | 
			
		||||
                                                class="btn btn-sm btn-outline-secondary" title="Edit Contract">
 | 
			
		||||
                                            <a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
 | 
			
		||||
                                                <i class="bi bi-pencil"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            <button class="btn btn-sm btn-outline-danger" title="Delete Contract"
 | 
			
		||||
                                                onclick="deleteContract('{{ contract.id }}', '{{ contract.title }}')">
 | 
			
		||||
                                                <i class="bi bi-trash"></i>
 | 
			
		||||
                                            </button>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
@@ -291,20 +119,11 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <div class="mb-4">
 | 
			
		||||
                            <i class="bi bi-file-earmark-text display-1 text-muted"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <h4 class="text-muted mb-3">No Contracts Found</h4>
 | 
			
		||||
                        <p class="text-muted mb-4">You haven't created any contracts yet. Get started by creating your
 | 
			
		||||
                            first contract.</p>
 | 
			
		||||
                        <div class="d-flex justify-content-center gap-2">
 | 
			
		||||
                            <a href="/contracts/create" class="btn btn-primary">
 | 
			
		||||
                                <i class="bi bi-plus-circle me-1"></i> Create Your First Contract
 | 
			
		||||
                            </a>
 | 
			
		||||
                            <a href="/contracts" class="btn btn-outline-secondary">
 | 
			
		||||
                                <i class="bi bi-arrow-left me-1"></i> Back to Dashboard
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <i class="bi bi-file-earmark-text fs-1 text-muted"></i>
 | 
			
		||||
                        <p class="mt-3 text-muted">You don't have any contracts yet</p>
 | 
			
		||||
                        <a href="/contracts/create" class="btn btn-primary mt-2">
 | 
			
		||||
                            <i class="bi bi-plus-circle me-1"></i> Create Your First Contract
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -312,166 +131,4 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Delete Confirmation Modal -->
 | 
			
		||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <div class="alert alert-danger">
 | 
			
		||||
                    <i class="bi bi-exclamation-triangle me-2"></i>
 | 
			
		||||
                    <strong>Warning:</strong> This action cannot be undone!
 | 
			
		||||
                </div>
 | 
			
		||||
                <p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
 | 
			
		||||
                <p>This will permanently remove:</p>
 | 
			
		||||
                <ul>
 | 
			
		||||
                    <li>The contract document and all its content</li>
 | 
			
		||||
                    <li>All signers and their signatures</li>
 | 
			
		||||
                    <li>All revisions and history</li>
 | 
			
		||||
                    <li>Any associated files or attachments</li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                <button type="button" class="btn btn-danger" id="confirmDeleteBtn">
 | 
			
		||||
                    <i class="bi bi-trash me-1"></i> Delete Contract
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    console.log('My Contracts page scripts loading...');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Delete contract functionality using Bootstrap modal
 | 
			
		||||
    window.deleteContract = function (contractId, contractTitle) {
 | 
			
		||||
        console.log('Delete contract called:', contractId, contractTitle);
 | 
			
		||||
 | 
			
		||||
        // Set the contract title in the modal
 | 
			
		||||
        document.getElementById('contractTitle').textContent = contractTitle;
 | 
			
		||||
 | 
			
		||||
        // Store the contract ID for later use
 | 
			
		||||
        window.currentDeleteContractId = contractId;
 | 
			
		||||
 | 
			
		||||
        // Show the modal
 | 
			
		||||
        const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
 | 
			
		||||
        deleteModal.show();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Simple table sorting functionality
 | 
			
		||||
    window.sortTable = function (columnIndex) {
 | 
			
		||||
        console.log('Sorting table by column:', columnIndex);
 | 
			
		||||
        const table = document.querySelector('.table tbody');
 | 
			
		||||
        const rows = Array.from(table.querySelectorAll('tr'));
 | 
			
		||||
 | 
			
		||||
        // Toggle sort direction
 | 
			
		||||
        const isAscending = table.dataset.sortDirection !== 'asc';
 | 
			
		||||
        table.dataset.sortDirection = isAscending ? 'asc' : 'desc';
 | 
			
		||||
 | 
			
		||||
        rows.sort((a, b) => {
 | 
			
		||||
            const aText = a.cells[columnIndex].textContent.trim();
 | 
			
		||||
            const bText = b.cells[columnIndex].textContent.trim();
 | 
			
		||||
 | 
			
		||||
            // Handle date sorting for created/updated columns
 | 
			
		||||
            if (columnIndex === 4 || columnIndex === 5) {
 | 
			
		||||
                const aDate = new Date(aText);
 | 
			
		||||
                const bDate = new Date(bText);
 | 
			
		||||
                return isAscending ? aDate - bDate : bDate - aDate;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle text sorting
 | 
			
		||||
            return isAscending ? aText.localeCompare(bText) : bText.localeCompare(aText);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Re-append sorted rows
 | 
			
		||||
        rows.forEach(row => table.appendChild(row));
 | 
			
		||||
 | 
			
		||||
        // Update sort indicators
 | 
			
		||||
        document.querySelectorAll('.bi-arrow-down-up').forEach(icon => {
 | 
			
		||||
            icon.className = 'bi bi-arrow-down-up ms-1 text-muted';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const currentIcon = document.querySelectorAll('.bi-arrow-down-up')[columnIndex === 4 ? 1 : 0];
 | 
			
		||||
        if (currentIcon) {
 | 
			
		||||
            currentIcon.className = `bi ${isAscending ? 'bi-arrow-up' : 'bi-arrow-down'} ms-1 text-primary`;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Calculate statistics and update progress bars
 | 
			
		||||
    function updateStatistics() {
 | 
			
		||||
        const rows = document.querySelectorAll('.table tbody tr');
 | 
			
		||||
        let totalContracts = rows.length;
 | 
			
		||||
        let pendingCount = 0;
 | 
			
		||||
        let signedCount = 0;
 | 
			
		||||
        let draftCount = 0;
 | 
			
		||||
 | 
			
		||||
        rows.forEach(row => {
 | 
			
		||||
            const statusCell = row.cells[2];
 | 
			
		||||
            const statusText = statusCell.textContent.trim();
 | 
			
		||||
 | 
			
		||||
            if (statusText.includes('PendingSignatures') || statusText.includes('Pending')) {
 | 
			
		||||
                pendingCount++;
 | 
			
		||||
            } else if (statusText.includes('Signed')) {
 | 
			
		||||
                signedCount++;
 | 
			
		||||
            } else if (statusText.includes('Draft')) {
 | 
			
		||||
                draftCount++;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update progress bars
 | 
			
		||||
            const progressBar = row.querySelector('.progress-bar');
 | 
			
		||||
            if (progressBar) {
 | 
			
		||||
                const signersText = row.cells[3].textContent.trim();
 | 
			
		||||
                if (signersText !== 'No signers') {
 | 
			
		||||
                    const [signed, total] = signersText.split('/').map(n => parseInt(n));
 | 
			
		||||
                    const percentage = total > 0 ? Math.round((signed / total) * 100) : 0;
 | 
			
		||||
                    progressBar.style.width = percentage + '%';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Update statistics cards
 | 
			
		||||
        document.getElementById('pending-count').textContent = pendingCount;
 | 
			
		||||
        document.getElementById('signed-count').textContent = signedCount;
 | 
			
		||||
        document.getElementById('draft-count').textContent = draftCount;
 | 
			
		||||
 | 
			
		||||
        // Update total count badge
 | 
			
		||||
        const badge = document.querySelector('.badge.bg-primary');
 | 
			
		||||
        if (badge) {
 | 
			
		||||
            badge.textContent = totalContracts;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Calculate initial statistics
 | 
			
		||||
        updateStatistics();
 | 
			
		||||
 | 
			
		||||
        // Handle confirm delete button click
 | 
			
		||||
        document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
 | 
			
		||||
            console.log('User confirmed deletion, submitting form...');
 | 
			
		||||
 | 
			
		||||
            // Create and submit form
 | 
			
		||||
            const form = document.createElement('form');
 | 
			
		||||
            form.method = 'POST';
 | 
			
		||||
            form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
 | 
			
		||||
            form.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
            document.body.appendChild(form);
 | 
			
		||||
            form.submit();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log('My Contracts page scripts loaded successfully');
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,370 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ contract.title }} - Signed Contract{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Action Bar (hidden in print) -->
 | 
			
		||||
    <div class="row mb-4 no-print">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h1 class="h4 mb-1">
 | 
			
		||||
                        <i class="bi bi-file-earmark-check text-success me-2"></i>
 | 
			
		||||
                        Signed Contract Document
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    <p class="text-muted mb-0">Official digitally signed copy</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-end">
 | 
			
		||||
                    <a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary">
 | 
			
		||||
                        <i class="bi bi-arrow-left me-1"></i> Back to Contract
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <button class="btn btn-primary" onclick="window.print()">
 | 
			
		||||
                        <i class="bi bi-printer me-1"></i> Print Document
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button class="btn btn-outline-secondary" id="copyContentBtn"
 | 
			
		||||
                        title="Copy contract content to clipboard">
 | 
			
		||||
                        <i class="bi bi-clipboard" id="copyIcon"></i>
 | 
			
		||||
                        <div class="spinner-border spinner-border-sm d-none" id="copySpinner" role="status">
 | 
			
		||||
                            <span class="visually-hidden">Loading...</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Signature Verification Banner -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="alert alert-success border-success">
 | 
			
		||||
                <div class="row align-items-center">
 | 
			
		||||
                    <div class="col-md-1 text-center">
 | 
			
		||||
                        <i class="bi bi-shield-check text-success" style="font-size: 2rem;"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-11">
 | 
			
		||||
                        <h5 class="alert-heading mb-2">
 | 
			
		||||
                            <i class="bi bi-check-circle me-2"></i>Digitally Signed Document
 | 
			
		||||
                        </h5>
 | 
			
		||||
                        <p class="mb-1">
 | 
			
		||||
                            <strong>{{ signer.name }}</strong> ({{ signer.email }}) digitally signed this contract on
 | 
			
		||||
                            <strong>{{ signer.signed_at }}</strong>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        {% if signer.comments %}
 | 
			
		||||
                        <p class="mb-0">
 | 
			
		||||
                            <strong>Signer Comments:</strong> {{ signer.comments }}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Contract Information -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-info-circle me-2"></i>Contract Information
 | 
			
		||||
                    </h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <p><strong>Contract ID:</strong> {{ contract.contract_id }}</p>
 | 
			
		||||
                            <p><strong>Title:</strong> {{ contract.title }}</p>
 | 
			
		||||
                            <p><strong>Type:</strong> {{ contract.contract_type }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <p><strong>Status:</strong>
 | 
			
		||||
                                <span class="badge bg-success">{{ contract.status }}</span>
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p><strong>Created:</strong> {{ contract.created_at }}</p>
 | 
			
		||||
                            <p><strong>Version:</strong> {{ contract.current_version }}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% if contract.description %}
 | 
			
		||||
                    <p><strong>Description:</strong> {{ contract.description }}</p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-4">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-person-check me-2"></i>Signer Information
 | 
			
		||||
                    </h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <p><strong>Name:</strong> {{ signer.name }}</p>
 | 
			
		||||
                    <p><strong>Email:</strong> {{ signer.email }}</p>
 | 
			
		||||
                    <p><strong>Status:</strong>
 | 
			
		||||
                        <span class="badge bg-success">{{ signer.status }}</span>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p><strong>Signed At:</strong> {{ signer.signed_at }}</p>
 | 
			
		||||
                    {% if signer.comments %}
 | 
			
		||||
                    <p><strong>Comments:</strong></p>
 | 
			
		||||
                    <div class="bg-light p-2 rounded">
 | 
			
		||||
                        {{ signer.comments }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <!-- Display Saved Signature -->
 | 
			
		||||
                    {% if signer.signature_data %}
 | 
			
		||||
                    <div class="mt-3">
 | 
			
		||||
                        <p><strong>Digital Signature:</strong></p>
 | 
			
		||||
                        <div class="signature-display bg-white border rounded p-3 text-center">
 | 
			
		||||
                            <img src="{{ signer.signature_data }}" alt="Digital Signature" class="signature-image" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Contract Content -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">
 | 
			
		||||
                        <i class="bi bi-file-text me-2"></i>Contract Terms & Conditions
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <button class="btn btn-outline-secondary btn-sm" id="copyContentBtn"
 | 
			
		||||
                            title="Copy contract content to clipboard">
 | 
			
		||||
                            <i class="bi bi-clipboard" id="copyIcon"></i>
 | 
			
		||||
                            <div class="spinner-border spinner-border-sm d-none" id="copySpinner" role="status">
 | 
			
		||||
                                <span class="visually-hidden">Loading...</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if contract_content_html %}
 | 
			
		||||
                    <!-- Hidden element containing raw markdown content for copying -->
 | 
			
		||||
                    <div id="rawContractContent" class="d-none">{{ contract.terms_and_conditions }}</div>
 | 
			
		||||
                    <div class="contract-content bg-white p-4 border rounded">
 | 
			
		||||
                        {{ contract_content_html | safe }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="alert alert-info text-center py-5">
 | 
			
		||||
                        <i class="bi bi-file-text text-muted" style="font-size: 3rem;"></i>
 | 
			
		||||
                        <h5 class="mt-3">No Content Available</h5>
 | 
			
		||||
                        <p class="text-muted">This contract doesn't have any content.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Digital Signature Footer -->
 | 
			
		||||
    <div class="row mt-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card border-success">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h6 class="text-success mb-2">
 | 
			
		||||
                        <i class="bi bi-shield-check me-2"></i>Digital Signature Verification
 | 
			
		||||
                    </h6>
 | 
			
		||||
                    <p class="small text-muted mb-0">
 | 
			
		||||
                        This document has been digitally signed by {{ signer.name }} on {{ signer.signed_at }}.
 | 
			
		||||
                        The digital signature ensures the authenticity and integrity of this contract.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_css %}
 | 
			
		||||
<style>
 | 
			
		||||
    /* Print styles */
 | 
			
		||||
    @media print {
 | 
			
		||||
 | 
			
		||||
        .btn,
 | 
			
		||||
        .card-header .btn {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .alert {
 | 
			
		||||
            border: 2px solid #28a745 !important;
 | 
			
		||||
            background-color: #f8f9fa !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .card {
 | 
			
		||||
            border: 1px solid #dee2e6 !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .bg-light {
 | 
			
		||||
            background-color: #f8f9fa !important;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Markdown Content Styles */
 | 
			
		||||
    .contract-content h1,
 | 
			
		||||
    .contract-content h2,
 | 
			
		||||
    .contract-content h3,
 | 
			
		||||
    .contract-content h4,
 | 
			
		||||
    .contract-content h5,
 | 
			
		||||
    .contract-content h6 {
 | 
			
		||||
        margin-top: 1.5rem;
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content h1 {
 | 
			
		||||
        font-size: 2rem;
 | 
			
		||||
        border-bottom: 2px solid #e9ecef;
 | 
			
		||||
        padding-bottom: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content h2 {
 | 
			
		||||
        font-size: 1.5rem;
 | 
			
		||||
        border-bottom: 1px solid #e9ecef;
 | 
			
		||||
        padding-bottom: 0.3rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content p {
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        line-height: 1.6;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content ul,
 | 
			
		||||
    .contract-content ol {
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        padding-left: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content table {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        border-collapse: collapse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content table th,
 | 
			
		||||
    .contract-content table td {
 | 
			
		||||
        padding: 0.75rem;
 | 
			
		||||
        border: 1px solid #dee2e6;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contract-content table th {
 | 
			
		||||
        background-color: #f8f9fa;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Signature Display Styles */
 | 
			
		||||
    .signature-display {
 | 
			
		||||
        min-height: 80px;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .signature-image {
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        max-height: 60px;
 | 
			
		||||
        border: 1px solid #dee2e6;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        background: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Copy button styles */
 | 
			
		||||
    #copyContentBtn {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        min-width: 40px;
 | 
			
		||||
        min-height: 32px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #copyContentBtn:disabled {
 | 
			
		||||
        opacity: 0.7;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #copySpinner {
 | 
			
		||||
        width: 1rem;
 | 
			
		||||
        height: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    // Copy contract content functionality
 | 
			
		||||
    const copyContentBtn = document.getElementById('copyContentBtn');
 | 
			
		||||
    const copyIcon = document.getElementById('copyIcon');
 | 
			
		||||
    const copySpinner = document.getElementById('copySpinner');
 | 
			
		||||
 | 
			
		||||
    if (copyContentBtn) {
 | 
			
		||||
        copyContentBtn.addEventListener('click', async function () {
 | 
			
		||||
            const rawContent = document.getElementById('rawContractContent');
 | 
			
		||||
 | 
			
		||||
            if (!rawContent) {
 | 
			
		||||
                alert('No contract content available to copy.');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show loading state
 | 
			
		||||
            copyIcon.classList.add('d-none');
 | 
			
		||||
            copySpinner.classList.remove('d-none');
 | 
			
		||||
            copyContentBtn.disabled = true;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                // Copy to clipboard
 | 
			
		||||
                await navigator.clipboard.writeText(rawContent.textContent);
 | 
			
		||||
 | 
			
		||||
                // Show success state
 | 
			
		||||
                copySpinner.classList.add('d-none');
 | 
			
		||||
                copyIcon.classList.remove('d-none');
 | 
			
		||||
                copyIcon.className = 'bi bi-check-circle text-success';
 | 
			
		||||
 | 
			
		||||
                // Initialize tooltip
 | 
			
		||||
                const tooltip = new bootstrap.Tooltip(copyContentBtn, {
 | 
			
		||||
                    title: 'Contract content copied to clipboard!',
 | 
			
		||||
                    placement: 'top',
 | 
			
		||||
                    trigger: 'manual'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Show tooltip
 | 
			
		||||
                tooltip.show();
 | 
			
		||||
 | 
			
		||||
                // Hide tooltip and reset icon after 2 seconds
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    tooltip.hide();
 | 
			
		||||
                    copyIcon.className = 'bi bi-clipboard';
 | 
			
		||||
                    copyContentBtn.disabled = false;
 | 
			
		||||
 | 
			
		||||
                    // Dispose tooltip to prevent memory leaks
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        tooltip.dispose();
 | 
			
		||||
                    }, 300);
 | 
			
		||||
                }, 2000);
 | 
			
		||||
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
                console.error('Failed to copy content: ', err);
 | 
			
		||||
 | 
			
		||||
                // Show error state
 | 
			
		||||
                copySpinner.classList.add('d-none');
 | 
			
		||||
                copyIcon.classList.remove('d-none');
 | 
			
		||||
                copyIcon.className = 'bi bi-x-circle text-danger';
 | 
			
		||||
 | 
			
		||||
                alert('Failed to copy content to clipboard. Please try again.');
 | 
			
		||||
 | 
			
		||||
                // Reset icon after 2 seconds
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    copyIcon.className = 'bi bi-clipboard';
 | 
			
		||||
                    copyContentBtn.disabled = false;
 | 
			
		||||
                }, 2000);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,125 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Page Not Found - Zanzibar Digital Freezone</title>
 | 
			
		||||
    <link rel="stylesheet" href="/static/css/bootstrap.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body class="bg-light">
 | 
			
		||||
    <div class="container-fluid min-vh-100 d-flex align-items-center justify-content-center">
 | 
			
		||||
        <div class="container-fluid">
 | 
			
		||||
            <div class="row justify-content-center">
 | 
			
		||||
                <div class="col-md-8 col-lg-6">
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <!-- 404 Icon -->
 | 
			
		||||
                        <div class="mb-4">
 | 
			
		||||
                            <i class="bi bi-exclamation-triangle display-1 text-warning"></i>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- Error Code -->
 | 
			
		||||
                        <h1 class="display-1 fw-bold text-muted">404</h1>
 | 
			
		||||
 | 
			
		||||
                        <!-- Error Message -->
 | 
			
		||||
                        <h2 class="mb-3">{% if error_title %}{{ error_title }}{% else %}Page Not Found{% endif %}</h2>
 | 
			
		||||
                        <p class="lead text-muted mb-4">
 | 
			
		||||
                            {% if error_message %}{{ error_message }}{% else %}The page you're looking for doesn't exist
 | 
			
		||||
                            or has
 | 
			
		||||
                            been moved.{% endif %}
 | 
			
		||||
                        </p>
 | 
			
		||||
 | 
			
		||||
                        <!-- Suggestions -->
 | 
			
		||||
                        <div class="card bg-light border-0 mb-4">
 | 
			
		||||
                            <div class="card-body">
 | 
			
		||||
                                <h6 class="card-title">What can you do?</h6>
 | 
			
		||||
                                <ul class="list-unstyled mb-0">
 | 
			
		||||
                                    <li class="mb-2">
 | 
			
		||||
                                        <i class="bi bi-arrow-left text-primary me-2"></i>
 | 
			
		||||
                                        Go back to the previous page
 | 
			
		||||
                                    </li>
 | 
			
		||||
                                    <li class="mb-2">
 | 
			
		||||
                                        <i class="bi bi-house text-primary me-2"></i>
 | 
			
		||||
                                        Visit our homepage
 | 
			
		||||
                                    </li>
 | 
			
		||||
                                    <li class="mb-2">
 | 
			
		||||
                                        <i class="bi bi-search text-primary me-2"></i>
 | 
			
		||||
                                        Check the URL for typos
 | 
			
		||||
                                    </li>
 | 
			
		||||
                                    <li class="mb-2">
 | 
			
		||||
                                        <i class="bi bi-arrow-clockwise text-primary me-2"></i>
 | 
			
		||||
                                        Try refreshing the page
 | 
			
		||||
                                    </li>
 | 
			
		||||
                                </ul>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- Action Buttons -->
 | 
			
		||||
                        <div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
 | 
			
		||||
                            <button onclick="history.back()" class="btn btn-outline-primary">
 | 
			
		||||
                                <i class="bi bi-arrow-left me-1"></i> Go Back
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <a href="/" class="btn btn-primary">
 | 
			
		||||
                                <i class="bi bi-house me-1"></i> Go Home
 | 
			
		||||
                            </a>
 | 
			
		||||
                            {% if return_url %}
 | 
			
		||||
                            <a href="{{ return_url }}" class="btn btn-outline-secondary">
 | 
			
		||||
                                <i class="bi bi-arrow-return-left me-1"></i> {% if return_text %}{{ return_text }}{%
 | 
			
		||||
                                else
 | 
			
		||||
                                %}Return{% endif %}
 | 
			
		||||
                            </a>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        <!-- Contact Support -->
 | 
			
		||||
                        <div class="mt-5 pt-4 border-top">
 | 
			
		||||
                            <p class="text-muted small">
 | 
			
		||||
                                Still having trouble?
 | 
			
		||||
                                <a href="/support" class="text-decoration-none">Contact Support</a>
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script src="/static/js/bootstrap.bundle.min.js"></script>
 | 
			
		||||
    <script>
 | 
			
		||||
        // Auto-redirect after 10 seconds if no user interaction
 | 
			
		||||
        let redirectTimer;
 | 
			
		||||
        let countdown = 10;
 | 
			
		||||
 | 
			
		||||
        function startAutoRedirect() {
 | 
			
		||||
            redirectTimer = setInterval(() => {
 | 
			
		||||
                countdown--;
 | 
			
		||||
                if (countdown <= 0) {
 | 
			
		||||
                    clearInterval(redirectTimer);
 | 
			
		||||
                    window.location.href = '/';
 | 
			
		||||
                }
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Cancel auto-redirect on any user interaction
 | 
			
		||||
        function cancelAutoRedirect() {
 | 
			
		||||
            if (redirectTimer) {
 | 
			
		||||
                clearInterval(redirectTimer);
 | 
			
		||||
                redirectTimer = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Start auto-redirect after 5 seconds of no interaction
 | 
			
		||||
        setTimeout(startAutoRedirect, 5000);
 | 
			
		||||
 | 
			
		||||
        // Cancel auto-redirect on mouse movement, clicks, or key presses
 | 
			
		||||
        document.addEventListener('mousemove', cancelAutoRedirect);
 | 
			
		||||
        document.addEventListener('click', cancelAutoRedirect);
 | 
			
		||||
        document.addEventListener('keydown', cancelAutoRedirect);
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										411
									
								
								actix_mvc_app/src/views/flows/index copy.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								actix_mvc_app/src/views/flows/index copy.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,411 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Flows Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/flows">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/list">All Workflows</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/my-flows">My Workflows</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/create">Create Workflow</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Info Alert -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                <h5><i class="bi bi-info-circle"></i> About Workflows</h5>
 | 
			
		||||
                <p>The workflow system helps you track and manage business processes across your organization. Create new workflows, monitor progress, and collaborate with team members to ensure smooth operations.</p>
 | 
			
		||||
                <div class="mt-2">
 | 
			
		||||
                    <a href="/flows/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Dashboard Main Content -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <!-- Workflows with Pending Actions -->
 | 
			
		||||
        <div class="col-lg-9 mb-4">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Workflows with Pending Actions</h5>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <a href="/flows/pending" class="btn btn-sm btn-outline-primary">View All Pending</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if flows and flows|length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover align-middle">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Workflow</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Current Step</th>
 | 
			
		||||
                                    <th>Last Updated</th>
 | 
			
		||||
                                    <th>Owner</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for flow in flows %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            <div class="flex-shrink-0 me-2">
 | 
			
		||||
                                                <div class="avatar bg-light text-primary rounded p-2">
 | 
			
		||||
                                                    <i class="bi bi-diagram-3"></i>
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div>
 | 
			
		||||
                                                <a href="/flows/{{ flow.id }}" class="text-decoration-none fw-medium">{{ flow.name }}</a>
 | 
			
		||||
                                                <div class="small text-muted">ID: {{ flow.id }}</div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td><span class="badge bg-info">{{ flow.flow_type }}</span></td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if flow.current_step %}
 | 
			
		||||
                                            <span class="text-warning fw-medium">{{ flow.current_step.name }}</span>
 | 
			
		||||
                                            <div class="small text-muted">{{ flow.current_step.description }}</div>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <span class="text-muted">No pending step</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span>{{ flow.updated_at | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                                        {% if flow.status == 'Stuck' %}
 | 
			
		||||
                                            <div class="small text-danger">May need attention</div>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <div class="small text-muted">Last updated</div>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            <div class="flex-shrink-0 me-2">
 | 
			
		||||
                                                <div class="avatar avatar-sm">
 | 
			
		||||
                                                    <img src="{{ flow.owner_avatar or '/static/img/avatar-placeholder.png' }}" alt="{{ flow.owner_name }}" class="rounded-circle" onerror="this.src='/static/img/avatar-placeholder.png'; this.onerror='';">
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div>{{ flow.owner_name }}</div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex gap-2">
 | 
			
		||||
                                            <a href="/flows/{{ flow.id }}#take-action" class="btn btn-sm btn-primary">Take Action</a>
 | 
			
		||||
                                            <div class="dropdown">
 | 
			
		||||
                                                <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
			
		||||
                                                    <i class="bi bi-three-dots"></i>
 | 
			
		||||
                                                </button>
 | 
			
		||||
                                                <ul class="dropdown-menu">
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}">View Details</a></li>
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}/reassign">Reassign</a></li>
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}/extend">Extend Deadline</a></li>
 | 
			
		||||
                                                </ul>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <i class="bi bi-check-circle-fill fs-1 text-success mb-3"></i>
 | 
			
		||||
                        <h5>No Pending Actions</h5>
 | 
			
		||||
                        <p class="text-muted">There are no workflows that require your immediate attention.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Timeline of Recent Flow Steps -->
 | 
			
		||||
        <div class="col-lg-3 mb-4">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Recent Activity</h5>
 | 
			
		||||
                    <a href="/flows/activity" class="btn btn-sm btn-outline-primary">View All</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    {% if flows and flows|length > 0 %}
 | 
			
		||||
                        <div class="list-group list-group-flush">
 | 
			
		||||
                            {% set count = 0 %}
 | 
			
		||||
                            {% for flow in flows %}
 | 
			
		||||
                                {% if count < 8 %}
 | 
			
		||||
                                {% set count = count + 1 %}
 | 
			
		||||
                                <div class="list-group-item border-start-0 border-end-0 py-3">
 | 
			
		||||
                                    <div class="d-flex">
 | 
			
		||||
                                        <div class="me-3">
 | 
			
		||||
                                            <div class="timeline-icon bg-light text-{% if flow.status == 'Completed' %}success{% elif flow.status == 'Stuck' %}danger{% else %}primary{% endif %} rounded-circle p-2">
 | 
			
		||||
                                                <i class="bi bi-{% if flow.status == 'Completed' %}check-circle{% elif flow.status == 'Stuck' %}exclamation-triangle{% else %}arrow-right-circle{% endif %} fs-5"></i>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <div class="flex-fill">
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <div class="fw-medium">
 | 
			
		||||
                                                    {% if flow.status == 'In Progress' %}Working on{% elif flow.status == 'Completed' %}Completed{% elif flow.status == 'Stuck' %}Stuck at{% else %}Updated{% endif %}
 | 
			
		||||
                                                    {% if flow.current_step %} {{ flow.current_step.name }}{% endif %}
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                                <div class="text-muted small">{{ flow.updated_at | date(format="%H:%M") }}</div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div>in <a href="/flows/{{ flow.id }}" class="text-decoration-none">{{ flow.name }}</a></div>
 | 
			
		||||
                                            <div class="text-muted small mt-1">by {{ flow.owner_name }}</div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="card-footer text-center">
 | 
			
		||||
                            <a href="/flows/activity" class="btn btn-sm btn-outline-secondary">View Full Activity Log</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <div class="list-group list-group-flush">
 | 
			
		||||
                            <div class="list-group-item border-start-0 border-end-0 py-5 text-center">
 | 
			
		||||
                                <i class="bi bi-hourglass fs-1 text-muted mb-3"></i>
 | 
			
		||||
                                <h6>No Recent Activity</h6>
 | 
			
		||||
                                <p class="text-muted small mb-0">Activity will appear here as workflows progress.</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Compact Filter Controls -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Workflow Filters</h5>
 | 
			
		||||
                    <button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="false" aria-controls="filterCollapse">
 | 
			
		||||
                        <i class="bi bi-funnel"></i> Show/Hide Filters
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="collapse show" id="filterCollapse">
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <form class="row g-3" action="/flows" method="get">
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="status" class="form-label">Status</label>
 | 
			
		||||
                                <select class="form-select" id="status" name="status">
 | 
			
		||||
                                    <option value="all" selected>All</option>
 | 
			
		||||
                                    <option value="in_progress">In Progress</option>
 | 
			
		||||
                                    <option value="completed">Completed</option>
 | 
			
		||||
                                    <option value="stuck">Stuck</option>
 | 
			
		||||
                                    <option value="cancelled">Cancelled</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <!-- Freezone filter - for UI demonstration only -->
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="freezone" class="form-label">Freezone</label>
 | 
			
		||||
                                <select class="form-select" id="freezone" name="freezone" disabled>
 | 
			
		||||
                                    <option value="all" selected>All Freezones</option>
 | 
			
		||||
                                    <option value="dubai_multi_commodities_centre">DMCC</option>
 | 
			
		||||
                                    <option value="dubai_international_financial_centre">DIFC</option>
 | 
			
		||||
                                    <option value="jebel_ali_free_zone">JAFZA</option>
 | 
			
		||||
                                    <option value="dubai_silicon_oasis">DSO</option>
 | 
			
		||||
                                    <option value="dubai_internet_city">DIC</option>
 | 
			
		||||
                                    <option value="dubai_media_city">DMC</option>
 | 
			
		||||
                                    <option value="abu_dhabi_global_market">ADGM</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                                <div class="form-text">Coming soon</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="type" class="form-label">Workflow Type</label>
 | 
			
		||||
                                <select class="form-select" id="type" name="type">
 | 
			
		||||
                                    <option value="all" selected>All</option>
 | 
			
		||||
                                    <option value="company_registration">Company Incorporation</option>
 | 
			
		||||
                                    <option value="user_onboarding">KYC Verification</option>
 | 
			
		||||
                                    <option value="service_activation">License Activation</option>
 | 
			
		||||
                                    <option value="payment_processing">Payment Processing</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-3">
 | 
			
		||||
                                <label for="search" class="form-label">Search</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="search" name="search" placeholder="Search workflows...">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-12 text-end">
 | 
			
		||||
                                <button type="submit" class="btn btn-primary">
 | 
			
		||||
                                    <i class="bi bi-filter me-1"></i> Apply Filters
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <a href="/flows" class="btn btn-outline-secondary">
 | 
			
		||||
                                    <i class="bi bi-x-circle me-1"></i> Clear Filters
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Recent Active Workflows Section -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Active Workflows (Recent Updates)</h5>
 | 
			
		||||
                    <a href="/flows/list" class="btn btn-sm btn-outline-primary">View All</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        {% set count = 0 %}
 | 
			
		||||
                        {% for flow in flows %}
 | 
			
		||||
                            {% if count < 3 and flow.status == 'In Progress' %}
 | 
			
		||||
                                <div class="col-md-4 mb-3">
 | 
			
		||||
                                    <div class="card h-100">
 | 
			
		||||
                                        <div class="card-body">
 | 
			
		||||
                                            <h5 class="card-title">{{ flow.name }}</h5>
 | 
			
		||||
                                            <h6 class="card-subtitle mb-2 text-muted">Owner: {{ flow.owner_name }}</h6>
 | 
			
		||||
                                            <div class="mb-3">
 | 
			
		||||
                                                <span class="badge bg-primary">{{ flow.flow_type }}</span>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <p class="mb-2">Current stage: 
 | 
			
		||||
                                                {% set current = flow.current_step %}
 | 
			
		||||
                                                {% if current %}
 | 
			
		||||
                                                {{ current.name }}
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                <span class="text-muted">No active stage</span>
 | 
			
		||||
                                                {% endif %}
 | 
			
		||||
                                            </p>
 | 
			
		||||
                                            <div class="progress mb-2" style="height: 20px;">
 | 
			
		||||
                                                <div class="progress-bar bg-primary" role="progressbar" 
 | 
			
		||||
                                                    style="width: {{ flow.progress_percentage }}%;" 
 | 
			
		||||
                                                    aria-valuenow="{{ flow.progress_percentage }}" 
 | 
			
		||||
                                                    aria-valuemin="0" aria-valuemax="100">{{ flow.progress_percentage }}%</div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center mt-3">
 | 
			
		||||
                                                <small class="text-muted">Updated: {{ flow.updated_at | date(format="%Y-%m-%d") }}</small>
 | 
			
		||||
                                                <a href="/flows/{{ flow.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% set count = count + 1 %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {% if count == 0 %}
 | 
			
		||||
                            <div class="col-12 text-center py-4">
 | 
			
		||||
                                <i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
 | 
			
		||||
                                <h5>No active workflows</h5>
 | 
			
		||||
                                <p class="text-muted">All workflows are either completed or not yet started.</p>
 | 
			
		||||
                                <a href="/flows/create" class="btn btn-primary mt-3">Create New Workflow</a>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Flows Table (Simplified) -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Recent Workflows</h5>
 | 
			
		||||
                    <a href="/flows/list" class="btn btn-sm btn-outline-primary">View All Workflows</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if flows|length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Workflow Name</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Status</th>
 | 
			
		||||
                                    <th>Assignee</th>
 | 
			
		||||
                                    <th>Progress</th>
 | 
			
		||||
                                    <th>Initiated</th>
 | 
			
		||||
                                    <th>Last Updated</th>
 | 
			
		||||
                                    <th>Current Stage</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for flow in flows %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <a href="/flows/{{ flow.id }}">{{ flow.name }}</a>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ flow.flow_type }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span
 | 
			
		||||
                                            class="badge {% if flow.status == 'In Progress' %}bg-primary{% elif flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                            {{ flow.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ flow.owner_name }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="progress mb-2" style="height: 20px;">
 | 
			
		||||
                                            <div class="progress-bar {% if flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-primary{% endif %}"
 | 
			
		||||
                                                role="progressbar" style="width: {{ flow.progress_percentage }}%;"
 | 
			
		||||
                                                aria-valuenow="{{ flow.progress_percentage }}" aria-valuemin="0"
 | 
			
		||||
                                                aria-valuemax="100">{{ flow.progress_percentage }}%</div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ flow.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>{{ flow.updated_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% set current = flow.current_step %}
 | 
			
		||||
                                        {% if current %}
 | 
			
		||||
                                        {{ current.name }}
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                        {% if flow.status == 'Completed' %}
 | 
			
		||||
                                        <span class="text-success">All stages completed</span>
 | 
			
		||||
                                        {% elif flow.status == 'Cancelled' %}
 | 
			
		||||
                                        <span class="text-secondary">Workflow cancelled</span>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                        <span class="text-muted">No active stage</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="btn-group">
 | 
			
		||||
                                            <a href="/flows/{{ flow.id }}" class="btn btn-sm btn-primary" title="View Details">
 | 
			
		||||
                                                <i class="bi bi-eye"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% if flow.status == 'In Progress' %}
 | 
			
		||||
                                            <a href="/flows/{{ flow.id }}#advance" class="btn btn-sm btn-success" title="Advance to Next Stage">
 | 
			
		||||
                                                <i class="bi bi-arrow-right"></i>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-4">
 | 
			
		||||
                        <i class="bi bi-search display-1 text-muted"></i>
 | 
			
		||||
                        <p class="lead mt-3">No workflows found matching your criteria.</p>
 | 
			
		||||
                        <p class="text-muted">Try adjusting your filters or create a new workflow.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -3,122 +3,188 @@
 | 
			
		||||
{% block title %}Flows Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <h1 class="display-5 mb-4">Flows Dashboard</h1>
 | 
			
		||||
            <p class="lead">Track and manage workflow processes across the organization.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-4 text-md-end">
 | 
			
		||||
            <a href="/flows/create" class="btn btn-primary">
 | 
			
		||||
                <i class="bi bi-plus-circle me-1"></i> Create New Workflow
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Statistics Cards -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-3 mb-3">
 | 
			
		||||
            <div class="card text-white bg-primary h-100">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Total Flows</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.total_flows }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3 mb-3">
 | 
			
		||||
            <div class="card text-white bg-success h-100">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">In Progress</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.in_progress_flows }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3 mb-3">
 | 
			
		||||
            <div class="card text-white bg-danger h-100">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Stuck</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.stuck_flows }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-3 mb-3">
 | 
			
		||||
            <div class="card text-white bg-info h-100">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h5 class="card-title">Completed</h5>
 | 
			
		||||
                    <p class="display-4">{{ stats.completed_flows }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Filter Controls -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Filter Workflows</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form class="row g-3" action="/flows" method="get">
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="status" class="form-label">Status</label>
 | 
			
		||||
                            <select class="form-select" id="status" name="status">
 | 
			
		||||
                                <option value="all" selected>All</option>
 | 
			
		||||
                                <option value="in_progress">In Progress</option>
 | 
			
		||||
                                <option value="completed">Completed</option>
 | 
			
		||||
                                <option value="stuck">Stuck</option>
 | 
			
		||||
                                <option value="cancelled">Cancelled</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <!-- Freezone filter - for UI demonstration only -->
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="freezone" class="form-label">Freezone</label>
 | 
			
		||||
                            <select class="form-select" id="freezone" name="freezone" disabled>
 | 
			
		||||
                                <option value="all" selected>All Freezones</option>
 | 
			
		||||
                                <option value="dubai_multi_commodities_centre">DMCC</option>
 | 
			
		||||
                                <option value="dubai_international_financial_centre">DIFC</option>
 | 
			
		||||
                                <option value="jebel_ali_free_zone">JAFZA</option>
 | 
			
		||||
                                <option value="dubai_silicon_oasis">DSO</option>
 | 
			
		||||
                                <option value="dubai_internet_city">DIC</option>
 | 
			
		||||
                                <option value="dubai_media_city">DMC</option>
 | 
			
		||||
                                <option value="abu_dhabi_global_market">ADGM</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                            <div class="form-text">Coming soon</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="type" class="form-label">Workflow Type</label>
 | 
			
		||||
                            <select class="form-select" id="type" name="type">
 | 
			
		||||
                                <option value="all" selected>All</option>
 | 
			
		||||
                                <option value="company_registration">Company Incorporation</option>
 | 
			
		||||
                                <option value="user_onboarding">KYC Verification</option>
 | 
			
		||||
                                <option value="service_activation">License Activation</option>
 | 
			
		||||
                                <option value="payment_processing">Payment Processing</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-3">
 | 
			
		||||
                            <label for="search" class="form-label">Search</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="search" name="search" placeholder="Search workflows...">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-12 text-end">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">
 | 
			
		||||
                                <i class="bi bi-filter me-1"></i> Apply Filters
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <a href="/flows" class="btn btn-outline-secondary">
 | 
			
		||||
                                <i class="bi bi-x-circle me-1"></i> Clear Filters
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/flows">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/list">All Workflows</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/my-flows">My Workflows</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/flows/create">Create Workflow</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Info Alert -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                <h5><i class="bi bi-info-circle"></i> About Workflows</h5>
 | 
			
		||||
                <p>The workflow system helps you track and manage business processes across your organization. Create new workflows, monitor progress, and collaborate with team members to ensure smooth operations.</p>
 | 
			
		||||
                <div class="mt-2">
 | 
			
		||||
                    <a href="/flows/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Flows Table -->
 | 
			
		||||
    <!-- Dashboard Main Content -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <!-- Workflows with Pending Actions -->
 | 
			
		||||
        <div class="col-lg-9 mb-4">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Workflows with Pending Actions</h5>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <a href="/flows/pending" class="btn btn-sm btn-outline-primary">View All Pending</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if flows and flows|length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover align-middle">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Workflow</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Current Step</th>
 | 
			
		||||
                                    <th>Last Updated</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for flow in flows %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex align-items-center">
 | 
			
		||||
                                            <div class="flex-shrink-0 me-2">
 | 
			
		||||
                                                <div class="avatar bg-light text-primary rounded p-2">
 | 
			
		||||
                                                    <i class="bi bi-diagram-3"></i>
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div>
 | 
			
		||||
                                                <a href="/flows/{{ flow.id }}" class="text-decoration-none fw-medium">{{ flow.name }}</a>
 | 
			
		||||
                                                <div class="small text-muted">ID: {{ flow.id }}</div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td><span class="badge bg-info">{{ flow.flow_type }}</span></td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if flow.current_step %}
 | 
			
		||||
                                            <span class="text-warning fw-medium">{{ flow.current_step.name }}</span>
 | 
			
		||||
                                            <div class="small text-muted">{{ flow.current_step.description }}</div>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <span class="text-muted">No pending step</span>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span>{{ flow.updated_at | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                                        {% if flow.status == 'Stuck' %}
 | 
			
		||||
                                            <div class="small text-danger">May need attention</div>
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            <div class="small text-muted">Last updated</div>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="d-flex gap-2">
 | 
			
		||||
                                            <a href="/flows/{{ flow.id }}#take-action" class="btn btn-sm btn-primary">Take Action</a>
 | 
			
		||||
                                            <div class="dropdown">
 | 
			
		||||
                                                <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
			
		||||
                                                    <i class="bi bi-three-dots"></i>
 | 
			
		||||
                                                </button>
 | 
			
		||||
                                                <ul class="dropdown-menu">
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}">View Details</a></li>
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}/reassign">Reassign</a></li>
 | 
			
		||||
                                                    <li><a class="dropdown-item" href="/flows/{{ flow.id }}/extend">Extend Deadline</a></li>
 | 
			
		||||
                                                </ul>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <i class="bi bi-check-circle-fill fs-1 text-success mb-3"></i>
 | 
			
		||||
                        <h5>No Pending Actions</h5>
 | 
			
		||||
                        <p class="text-muted">There are no workflows that require your immediate attention.</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Timeline of Recent Flow Steps -->
 | 
			
		||||
        <div class="col-lg-3 mb-4">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Recent Activity</h5>
 | 
			
		||||
                    <a href="/flows/activity" class="btn btn-sm btn-outline-primary">View All</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    {% if flows and flows|length > 0 %}
 | 
			
		||||
                        <div class="list-group list-group-flush">
 | 
			
		||||
                            {% set count = 0 %}
 | 
			
		||||
                            {% for flow in flows %}
 | 
			
		||||
                                {% if count < 8 %}
 | 
			
		||||
                                {% set count = count + 1 %}
 | 
			
		||||
                                <div class="list-group-item border-start-0 border-end-0 py-3">
 | 
			
		||||
                                    <div class="d-flex">
 | 
			
		||||
                                        <div class="me-3">
 | 
			
		||||
                                            <div class="timeline-icon bg-light text-{% if flow.status == 'Completed' %}success{% elif flow.status == 'Stuck' %}danger{% else %}primary{% endif %} rounded-circle p-2">
 | 
			
		||||
                                                <i class="bi bi-{% if flow.status == 'Completed' %}check-circle{% elif flow.status == 'Stuck' %}exclamation-triangle{% else %}arrow-right-circle{% endif %} fs-5"></i>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <div class="flex-fill">
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <div class="fw-medium">
 | 
			
		||||
                                                    {% if flow.status == 'In Progress' %}Working on{% elif flow.status == 'Completed' %}Completed{% elif flow.status == 'Stuck' %}Stuck at{% else %}Updated{% endif %}
 | 
			
		||||
                                                    {% if flow.current_step %} {{ flow.current_step.name }}{% endif %}
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                                <div class="text-muted small">{{ flow.updated_at | date(format="%H:%M") }}</div>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                            <div>in <a href="/flows/{{ flow.id }}" class="text-decoration-none">{{ flow.name }}</a></div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="card-footer text-center">
 | 
			
		||||
                            <a href="/flows/activity" class="btn btn-sm btn-outline-secondary">View Full Activity Log</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <div class="list-group list-group-flush">
 | 
			
		||||
                            <div class="list-group-item border-start-0 border-end-0 py-5 text-center">
 | 
			
		||||
                                <i class="bi bi-hourglass fs-1 text-muted mb-3"></i>
 | 
			
		||||
                                <h6>No Recent Activity</h6>
 | 
			
		||||
                                <p class="text-muted small mb-0">Activity will appear here as workflows progress.</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Flows Table (Simplified) -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">All Workflows</h5>
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Recent Workflows</h5>
 | 
			
		||||
                    <a href="/flows/list" class="btn btn-sm btn-outline-primary">View All Workflows</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if flows|length > 0 %}
 | 
			
		||||
@@ -129,7 +195,6 @@
 | 
			
		||||
                                    <th>Workflow Name</th>
 | 
			
		||||
                                    <th>Type</th>
 | 
			
		||||
                                    <th>Status</th>
 | 
			
		||||
                                    <th>Assignee</th>
 | 
			
		||||
                                    <th>Progress</th>
 | 
			
		||||
                                    <th>Initiated</th>
 | 
			
		||||
                                    <th>Last Updated</th>
 | 
			
		||||
@@ -150,7 +215,6 @@
 | 
			
		||||
                                            {{ flow.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ flow.owner_name }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <div class="progress mb-2" style="height: 20px;">
 | 
			
		||||
                                            <div class="progress-bar {% if flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-primary{% endif %}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<!-- Governance Page Header -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
            <div>
 | 
			
		||||
                <h1 class="h3 mb-1">{{ page_title }}</h1>
 | 
			
		||||
                <p class="text-muted mb-0">{{ page_description }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if show_create_button %}
 | 
			
		||||
            <div>
 | 
			
		||||
                <a href="/governance/create" class="btn btn-primary">
 | 
			
		||||
                    <i class="bi bi-plus-circle"></i> Create Proposal
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
<!-- Governance Navigation Tabs -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <ul class="nav nav-tabs">
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link {% if active_tab == 'dashboard' %}active{% endif %}" href="/governance">
 | 
			
		||||
                    <i class="bi bi-house"></i> Dashboard
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link {% if active_tab == 'proposals' %}active{% endif %}" href="/governance/proposals">
 | 
			
		||||
                    <i class="bi bi-file-text"></i> All Proposals
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link {% if active_tab == 'create' %}active{% endif %}" href="/governance/create">
 | 
			
		||||
                    <i class="bi bi-plus-circle"></i> Create Proposal
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link {% if active_tab == 'my-votes' %}active{% endif %}" href="/governance/my-votes">
 | 
			
		||||
                    <i class="bi bi-check-circle"></i> My Votes
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link {% if active_tab == 'activities' %}active{% endif %}" href="/governance/activities">
 | 
			
		||||
                    <i class="bi bi-activity"></i> All Activities
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,118 +0,0 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}All Governance Activities{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <!-- Header -->
 | 
			
		||||
            {% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
            <!-- Navigation Tabs -->
 | 
			
		||||
            {% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
            <!-- Activities List -->
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="card-title mb-0">
 | 
			
		||||
                        <i class="bi bi-activity"></i> Governance Activity History
 | 
			
		||||
                    </h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if activities %}
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div class="col-12">
 | 
			
		||||
                            <div class="table-responsive">
 | 
			
		||||
                                <table class="table table-hover">
 | 
			
		||||
                                    <thead>
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <th width="50">Type</th>
 | 
			
		||||
                                            <th>User</th>
 | 
			
		||||
                                            <th>Action</th>
 | 
			
		||||
                                            <th>Proposal</th>
 | 
			
		||||
                                            <th width="150">Date</th>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    </thead>
 | 
			
		||||
                                    <tbody>
 | 
			
		||||
                                        {% for activity in activities %}
 | 
			
		||||
                                        <tr>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <i class="{{ activity.icon }}"></i>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <strong>{{ activity.user }}</strong>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                {{ activity.action }}
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <a href="/governance/proposals/{{ activity.proposal_id }}"
 | 
			
		||||
                                                    class="text-decoration-none">
 | 
			
		||||
                                                    {{ activity.proposal_title }}
 | 
			
		||||
                                                </a>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                            <td>
 | 
			
		||||
                                                <small class="text-muted">
 | 
			
		||||
                                                    {{ activity.created_at | date(format="%Y-%m-%d %H:%M") }}
 | 
			
		||||
                                                </small>
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                        {% endfor %}
 | 
			
		||||
                                    </tbody>
 | 
			
		||||
                                </table>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <i class="bi bi-activity display-1 text-muted"></i>
 | 
			
		||||
                        <h4 class="mt-3">No Activities Yet</h4>
 | 
			
		||||
                        <p class="text-muted">
 | 
			
		||||
                            Governance activities will appear here as users create proposals and cast votes.
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <a href="/governance/create" class="btn btn-primary">
 | 
			
		||||
                            <i class="bi bi-plus-circle"></i> Create First Proposal
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Activity Statistics -->
 | 
			
		||||
            {% if activities %}
 | 
			
		||||
            <div class="row mt-4">
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">{{ activities | length }}</h5>
 | 
			
		||||
                            <p class="card-text text-muted">Total Activities</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">
 | 
			
		||||
                                <i class="bi bi-activity text-primary"></i>
 | 
			
		||||
                            </h5>
 | 
			
		||||
                            <p class="card-text text-muted">Activity Timeline</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-4">
 | 
			
		||||
                    <div class="card text-center">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h5 class="card-title">
 | 
			
		||||
                                <i class="bi bi-people text-success"></i>
 | 
			
		||||
                            </h5>
 | 
			
		||||
                            <p class="card-text text-muted">Community Engagement</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -4,74 +4,69 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    {% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    {% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Info Alert -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                <h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
 | 
			
		||||
                <p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
 | 
			
		||||
                    clearly state the problem, solution, and implementation details. The community will review and vote
 | 
			
		||||
                    on your proposal, so be thorough and thoughtful in your submission.</p>
 | 
			
		||||
                <div class="mt-2">
 | 
			
		||||
                    <a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
 | 
			
		||||
                            class="bi bi-file-earmark-text"></i> Proposal Templates</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <h1 class="display-5 mb-4">Create Governance Proposal</h1>
 | 
			
		||||
            <p class="lead">Submit a new proposal for the community to vote on.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Proposal Form and Guidelines in Flex Layout -->
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <!-- Proposal Form Column -->
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/proposals">All Proposals</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/my-votes">My Votes</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/governance/create">Create Proposal</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Proposal Form -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-8 mx-auto">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">New Proposal</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/governance/create" method="post" id="proposalForm" novalidate>
 | 
			
		||||
                    <form action="/governance/create" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="title" class="form-label">Title</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="title" name="title" required minlength="5"
 | 
			
		||||
                                maxlength="100" placeholder="Enter a clear, concise title for your proposal">
 | 
			
		||||
                            <div class="invalid-feedback">Please provide a title (5-100 characters).</div>
 | 
			
		||||
                            <input type="text" class="form-control" id="title" name="title" required 
 | 
			
		||||
                                   placeholder="Enter a clear, concise title for your proposal">
 | 
			
		||||
                            <div class="form-text">Make it descriptive and specific</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="description" class="form-label">Description</label>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="8" required
 | 
			
		||||
                                minlength="50" maxlength="5000"
 | 
			
		||||
                                placeholder="Provide a detailed description of your proposal..."></textarea>
 | 
			
		||||
                            <div class="invalid-feedback">Please provide a detailed description (at least 50
 | 
			
		||||
                                characters).</div>
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="6" required
 | 
			
		||||
                                      placeholder="Provide a detailed description of your proposal..."></textarea>
 | 
			
		||||
                            <div class="form-text">Explain the purpose, benefits, and implementation details</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="row mb-3">
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="voting_start_date" class="form-label">Voting Start Date</label>
 | 
			
		||||
                                <input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
 | 
			
		||||
                                <div class="invalid-feedback" id="start_date_feedback">Please select a valid start date.
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-text">When should voting begin?</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="col-md-6">
 | 
			
		||||
                                <label for="voting_end_date" class="form-label">Voting End Date</label>
 | 
			
		||||
                                <input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
 | 
			
		||||
                                <div class="invalid-feedback" id="end_date_feedback">End date must be after start date.
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="form-text">When should voting end?</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <div class="form-check">
 | 
			
		||||
                                <input class="form-check-input" type="checkbox" id="draft" name="draft" value="true">
 | 
			
		||||
@@ -80,7 +75,7 @@
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="d-grid gap-2">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary">Submit Proposal</button>
 | 
			
		||||
                            <a href="/governance" class="btn btn-outline-secondary">Cancel</a>
 | 
			
		||||
@@ -89,10 +84,12 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Guidelines Column -->
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card bg-light h-100">
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Guidelines Card -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-8 mx-auto">
 | 
			
		||||
            <div class="card bg-light">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Proposal Guidelines</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -119,111 +116,4 @@
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        const form = document.getElementById('proposalForm');
 | 
			
		||||
        const startDateInput = document.getElementById('voting_start_date');
 | 
			
		||||
        const endDateInput = document.getElementById('voting_end_date');
 | 
			
		||||
        const startDateFeedback = document.getElementById('start_date_feedback');
 | 
			
		||||
        const endDateFeedback = document.getElementById('end_date_feedback');
 | 
			
		||||
 | 
			
		||||
        // Set default dates
 | 
			
		||||
        const today = new Date();
 | 
			
		||||
        const tomorrow = new Date(today);
 | 
			
		||||
        tomorrow.setDate(tomorrow.getDate() + 1);
 | 
			
		||||
        const nextWeek = new Date(today);
 | 
			
		||||
        nextWeek.setDate(nextWeek.getDate() + 7);
 | 
			
		||||
 | 
			
		||||
        // Format dates for input fields
 | 
			
		||||
        const formatDate = (date) => {
 | 
			
		||||
            const year = date.getFullYear();
 | 
			
		||||
            const month = String(date.getMonth() + 1).padStart(2, '0');
 | 
			
		||||
            const day = String(date.getDate()).padStart(2, '0');
 | 
			
		||||
            return `${year}-${month}-${day}`;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Set default values
 | 
			
		||||
        startDateInput.value = formatDate(tomorrow);
 | 
			
		||||
        endDateInput.value = formatDate(nextWeek);
 | 
			
		||||
 | 
			
		||||
        // Validate dates when they change
 | 
			
		||||
        function validateDates() {
 | 
			
		||||
            const startDate = new Date(startDateInput.value);
 | 
			
		||||
            const endDate = new Date(endDateInput.value);
 | 
			
		||||
            const currentDate = new Date();
 | 
			
		||||
            currentDate.setHours(0, 0, 0, 0); // Reset time to start of day
 | 
			
		||||
 | 
			
		||||
            let startValid = true;
 | 
			
		||||
            let endValid = true;
 | 
			
		||||
 | 
			
		||||
            // Validate start date is not in the past
 | 
			
		||||
            if (startDate < currentDate) {
 | 
			
		||||
                startDateInput.classList.add('is-invalid');
 | 
			
		||||
                startDateFeedback.textContent = 'Start date cannot be in the past.';
 | 
			
		||||
                startValid = false;
 | 
			
		||||
            } else {
 | 
			
		||||
                startDateInput.classList.remove('is-invalid');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Validate end date is after start date
 | 
			
		||||
            if (endDate < startDate) {
 | 
			
		||||
                endDateInput.classList.add('is-invalid');
 | 
			
		||||
                endDateFeedback.textContent = 'End date must be after start date.';
 | 
			
		||||
                endValid = false;
 | 
			
		||||
            } else {
 | 
			
		||||
                endDateInput.classList.remove('is-invalid');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return startValid && endValid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate on input
 | 
			
		||||
        startDateInput.addEventListener('change', validateDates);
 | 
			
		||||
        endDateInput.addEventListener('change', validateDates);
 | 
			
		||||
 | 
			
		||||
        // Form submission validation
 | 
			
		||||
        form.addEventListener('submit', function (event) {
 | 
			
		||||
            let formValid = true;
 | 
			
		||||
 | 
			
		||||
            // Validate required fields
 | 
			
		||||
            const requiredFields = form.querySelectorAll('[required]');
 | 
			
		||||
            requiredFields.forEach(field => {
 | 
			
		||||
                if (!field.value.trim()) {
 | 
			
		||||
                    field.classList.add('is-invalid');
 | 
			
		||||
                    formValid = false;
 | 
			
		||||
                } else {
 | 
			
		||||
                    field.classList.remove('is-invalid');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Check minlength if specified
 | 
			
		||||
                if (field.minLength && field.value.length < field.minLength) {
 | 
			
		||||
                    field.classList.add('is-invalid');
 | 
			
		||||
                    formValid = false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Validate dates
 | 
			
		||||
            const datesValid = validateDates();
 | 
			
		||||
            formValid = formValid && datesValid;
 | 
			
		||||
 | 
			
		||||
            // If form is not valid, prevent submission
 | 
			
		||||
            if (!formValid) {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                // Scroll to the first invalid element
 | 
			
		||||
                const firstInvalid = form.querySelector('.is-invalid');
 | 
			
		||||
                if (firstInvalid) {
 | 
			
		||||
                    firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
 | 
			
		||||
                    firstInvalid.focus();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Initial validation
 | 
			
		||||
        validateDates();
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -3,192 +3,170 @@
 | 
			
		||||
{% block title %}Governance Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Info Alert -->
 | 
			
		||||
<div class="row mb-2">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
            <h5><i class="bi bi-info-circle"></i> About Governance</h5>
 | 
			
		||||
            <p>The governance system allows token holders to participate in decision-making processes by voting on
 | 
			
		||||
                proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction
 | 
			
		||||
                of our decentralized ecosystem.</p>
 | 
			
		||||
            <div class="mt-2">
 | 
			
		||||
                <a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i>
 | 
			
		||||
                    Read Documentation</a>
 | 
			
		||||
            </div>
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/governance">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/proposals">All Proposals</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/my-votes">My Votes</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/create">Create Proposal</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Dashboard Main Content -->
 | 
			
		||||
<div class="row mb-3">
 | 
			
		||||
    <!-- Voting Pane for Nearest Deadline Proposal -->
 | 
			
		||||
    <div class="col-lg-8 mb-4 mb-lg-0">
 | 
			
		||||
        {% if nearest_proposal is defined %}
 | 
			
		||||
        <div class="card h-100">
 | 
			
		||||
            <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                <h5 class="mb-0">Urgent: Voting Closes Soon</h5>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.vote_end_date |
 | 
			
		||||
                        date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                    <a href="/governance/proposals/{{ nearest_proposal.base_data.id }}"
 | 
			
		||||
                        class="btn btn-sm btn-outline-primary">View Full Proposal</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <h4 class="card-title">{{ nearest_proposal.title }}</h4>
 | 
			
		||||
                <h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
 | 
			
		||||
 | 
			
		||||
                <div class="mb-4">
 | 
			
		||||
                    <p>{{ nearest_proposal.description }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {% set yes_percent = 0 %}
 | 
			
		||||
                {% set no_percent = 0 %}
 | 
			
		||||
                {% set abstain_percent = 0 %}
 | 
			
		||||
                {% set total_votes = 0 %}
 | 
			
		||||
 | 
			
		||||
                {% if nearest_proposal_results is defined %}
 | 
			
		||||
                {% if nearest_proposal_results.total_votes > 0 %}
 | 
			
		||||
                {% set yes_percent = (nearest_proposal_results.yes_count * 100 / nearest_proposal_results.total_votes) |
 | 
			
		||||
                int %}
 | 
			
		||||
                {% set no_percent = (nearest_proposal_results.no_count * 100 / nearest_proposal_results.total_votes) |
 | 
			
		||||
                int %}
 | 
			
		||||
                {% set abstain_percent = (nearest_proposal_results.abstain_count * 100 /
 | 
			
		||||
                nearest_proposal_results.total_votes) |
 | 
			
		||||
                int %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% set total_votes = nearest_proposal_results.total_votes %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                <div class="progress mb-3" style="height: 25px;">
 | 
			
		||||
                    <div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
 | 
			
		||||
                        aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100">{{ yes_percent }}% Yes
 | 
			
		||||
        <!-- Info Alert -->
 | 
			
		||||
        <div class="row mb-2">
 | 
			
		||||
            <div class="col-12">
 | 
			
		||||
                <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
                    <h5><i class="bi bi-info-circle"></i> About Governance</h5>
 | 
			
		||||
                    <p>The governance system allows token holders to participate in decision-making processes by voting on proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction of our decentralized ecosystem.</p>
 | 
			
		||||
                    <div class="mt-2">
 | 
			
		||||
                        <a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
 | 
			
		||||
                        aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100">{{ no_percent }}% No
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"
 | 
			
		||||
                        aria-valuenow="{{ abstain_percent }}" aria-valuemin="0" aria-valuemax="100">{{ abstain_percent
 | 
			
		||||
                        }}% Abstain
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="d-flex justify-content-between text-muted small mb-4">
 | 
			
		||||
                    <span>{{ total_votes }} votes cast</span>
 | 
			
		||||
                    <span>Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="mb-4">
 | 
			
		||||
                    <h5 class="mb-3">Cast Your Vote</h5>
 | 
			
		||||
                    <form action="/governance/proposals/{{ nearest_proposal.base_data.id }}/vote" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <input type="text" class="form-control" name="comment"
 | 
			
		||||
                                placeholder="Optional comment on your vote" aria-label="Vote comment">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="d-flex justify-content-between">
 | 
			
		||||
                            <button type="submit" name="vote_type" value="Yes" class="btn btn-success">Vote Yes</button>
 | 
			
		||||
                            <button type="submit" name="vote_type" value="No" class="btn btn-danger">Vote No</button>
 | 
			
		||||
                            <button type="submit" name="vote_type" value="Abstain"
 | 
			
		||||
                                class="btn btn-secondary">Abstain</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <div class="card h-100">
 | 
			
		||||
            <div class="card-body text-center py-5">
 | 
			
		||||
                <i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
 | 
			
		||||
                <h5>No active proposals requiring votes</h5>
 | 
			
		||||
                <p class="text-muted">When new proposals are created, they will appear here for voting.</p>
 | 
			
		||||
                <a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Recent Activity Timeline -->
 | 
			
		||||
    <div class="col-lg-4">
 | 
			
		||||
        <div class="card h-100">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <h5 class="mb-0">Recent Activity</h5>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body p-0">
 | 
			
		||||
                <div class="list-group list-group-flush">
 | 
			
		||||
                    {% for activity in recent_activity %}
 | 
			
		||||
                    <div class="list-group-item border-start-0 border-end-0 py-3">
 | 
			
		||||
                        <div class="d-flex">
 | 
			
		||||
                            <div class="me-3">
 | 
			
		||||
                                <i class="bi {{ activity.icon }} fs-4"></i>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Dashboard Main Content -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <!-- Voting Pane for Nearest Deadline Proposal -->
 | 
			
		||||
        <div class="col-lg-8 mb-4 mb-lg-0">
 | 
			
		||||
            {% if nearest_proposal is defined %}
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">Urgent: Voting Closes Soon</h5>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                        <a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <h4 class="card-title">{{ nearest_proposal.title }}</h4>
 | 
			
		||||
                    <h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <p>{{ nearest_proposal.description }}</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="progress mb-3" style="height: 25px;">
 | 
			
		||||
                        <div class="progress-bar bg-success" role="progressbar" style="width: 65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">65% Yes</div>
 | 
			
		||||
                        <div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="d-flex justify-content-between text-muted small mb-4">
 | 
			
		||||
                        <span>26 votes cast</span>
 | 
			
		||||
                        <span>Quorum: 75% reached</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <h5 class="mb-3">Cast Your Vote</h5>
 | 
			
		||||
                        <form>
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                    <strong>{{ activity.user }}</strong>
 | 
			
		||||
                                    <small class="text-muted">{{ activity.created_at | date(format="%H:%M") }}</small>
 | 
			
		||||
                            <div class="d-flex justify-content-between">
 | 
			
		||||
                                <button type="submit" name="vote" value="yes" class="btn btn-success">Vote Yes</button>
 | 
			
		||||
                                <button type="submit" name="vote" value="no" class="btn btn-danger">Vote No</button>
 | 
			
		||||
                                <button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-body text-center py-5">
 | 
			
		||||
                    <i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
 | 
			
		||||
                    <h5>No active proposals requiring votes</h5>
 | 
			
		||||
                    <p class="text-muted">When new proposals are created, they will appear here for voting.</p>
 | 
			
		||||
                    <a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <!-- Recent Activity Timeline -->
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card h-100">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Recent Activity</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body p-0">
 | 
			
		||||
                    <div class="list-group list-group-flush">
 | 
			
		||||
                        {% for activity in recent_activity %}
 | 
			
		||||
                        <div class="list-group-item border-start-0 border-end-0 py-3">
 | 
			
		||||
                            <div class="d-flex">
 | 
			
		||||
                                <div class="me-3">
 | 
			
		||||
                                    <i class="bi {{ activity.icon }} fs-4"></i>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <p class="mb-1">{{ activity.action }} on <a
 | 
			
		||||
                                        href="/governance/proposals/{{ activity.proposal_id }}">{{
 | 
			
		||||
                                        activity.proposal_title }}</a></p>
 | 
			
		||||
                                {% if activity.type == "comment" and activity.comment is defined %}
 | 
			
		||||
                                <p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-footer text-center">
 | 
			
		||||
                <a href="/governance/activities" class="btn btn-sm btn-outline-info">View All Activities</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Recent Proposals Section -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <h5 class="mb-0">Active Proposals (Ending Soon)</h5>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <div class="row">
 | 
			
		||||
                    {% set count = 0 %}
 | 
			
		||||
                    {% for proposal in proposals %}
 | 
			
		||||
                    {% if count < 3 %} <div class="col-md-4 mb-3">
 | 
			
		||||
                        <div class="card h-100">
 | 
			
		||||
                            <div class="card-body">
 | 
			
		||||
                                <h5 class="card-title">{{ proposal.title }}</h5>
 | 
			
		||||
                                <h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
 | 
			
		||||
                                <p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
 | 
			
		||||
                                <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                    <span
 | 
			
		||||
                                        class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                        {{ proposal.status }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <a href="/governance/proposals/{{ proposal.base_data.id }}"
 | 
			
		||||
                                        class="btn btn-sm btn-outline-primary">View Details</a>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                        <strong>{{ activity.user }}</strong>
 | 
			
		||||
                                        <small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p>
 | 
			
		||||
                                    {% if activity.type == "comment" and activity.comment is defined %}
 | 
			
		||||
                                    <p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="card-footer text-muted text-center">
 | 
			
		||||
                                <span>Voting ends: {{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-footer text-center">
 | 
			
		||||
                    <a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% set count = count + 1 %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
    <!-- Recent Proposals Section -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Active Proposals (Ending Soon)</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        {% set count = 0 %}
 | 
			
		||||
                        {% for proposal in proposals %}
 | 
			
		||||
                            {% if count < 3 %}
 | 
			
		||||
                                <div class="col-md-4 mb-3">
 | 
			
		||||
                                    <div class="card h-100">
 | 
			
		||||
                                        <div class="card-body">
 | 
			
		||||
                                            <h5 class="card-title">{{ proposal.title }}</h5>
 | 
			
		||||
                                            <h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
 | 
			
		||||
                                            <p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
 | 
			
		||||
                                            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                                                <span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                                    {{ proposal.status }}
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                                <a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                        <div class="card-footer text-muted text-center">
 | 
			
		||||
                                            <span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% set count = count + 1 %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,121 +3,133 @@
 | 
			
		||||
{% block title %}My Votes - Governance Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Info Alert -->
 | 
			
		||||
<div class="row">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
            <h5><i class="bi bi-info-circle"></i> About Votes</h5>
 | 
			
		||||
            <p>Voting is a fundamental right of all token holders in our governance system. Each vote carries weight
 | 
			
		||||
                proportional to your token holdings, ensuring fair representation. The voting statistics below show the
 | 
			
		||||
                community's collective decision-making across all proposals.</p>
 | 
			
		||||
            <div class="mt-2">
 | 
			
		||||
                <a href="/governance/voting-guide" class="btn btn-sm btn-outline-primary"><i
 | 
			
		||||
                        class="bi bi-check2-square"></i> Voting Guide</a>
 | 
			
		||||
            </div>
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/proposals">All Proposals</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/governance/my-votes">My Votes</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/create">Create Proposal</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Voting Stats -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-md-4 mb-3">
 | 
			
		||||
        <div class="card text-white bg-success h-100">
 | 
			
		||||
            <div class="card-body text-center">
 | 
			
		||||
                <h5 class="card-title">Yes Votes</h5>
 | 
			
		||||
                <p class="display-4">
 | 
			
		||||
                    {{ total_yes_votes }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-md-4 mb-3">
 | 
			
		||||
        <div class="card text-white bg-danger h-100">
 | 
			
		||||
            <div class="card-body text-center">
 | 
			
		||||
                <h5 class="card-title">No Votes</h5>
 | 
			
		||||
                <p class="display-4">
 | 
			
		||||
                    {{ total_no_votes }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-md-4 mb-3">
 | 
			
		||||
        <div class="card text-white bg-secondary h-100">
 | 
			
		||||
            <div class="card-body text-center">
 | 
			
		||||
                <h5 class="card-title">Abstain Votes</h5>
 | 
			
		||||
                <p class="display-4">
 | 
			
		||||
                    {{ total_abstain_votes }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- My Votes List -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <h5 class="mb-0">My Voting History</h5>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                {% if votes | length > 0 %}
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
                    <table class="table table-hover">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <th>Proposal</th>
 | 
			
		||||
                                <th>My Vote</th>
 | 
			
		||||
                                <th>Status</th>
 | 
			
		||||
                                <th>Voted On</th>
 | 
			
		||||
                                <th>Actions</th>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td>{{ proposal.title }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <span
 | 
			
		||||
                                        class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                        {{ vote.vote_type }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <span
 | 
			
		||||
                                        class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
 | 
			
		||||
                                        {{ proposal.status }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <a href="/governance/proposals/{{ proposal.base_data.id }}"
 | 
			
		||||
                                        class="btn btn-sm btn-primary">View Proposal</a>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
    <!-- My Votes List -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">My Voting History</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <div class="text-center py-5">
 | 
			
		||||
                    <i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
 | 
			
		||||
                    <h5>You haven't voted on any proposals yet</h5>
 | 
			
		||||
                    <p class="text-muted">When you vote on proposals, they will appear here.</p>
 | 
			
		||||
                    <a href="/governance/proposals" class="btn btn-primary mt-3">Browse Proposals</a>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if votes | length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Proposal</th>
 | 
			
		||||
                                    <th>My Vote</th>
 | 
			
		||||
                                    <th>Status</th>
 | 
			
		||||
                                    <th>Voted On</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>{{ proposal.title }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                            {{ vote.vote_type }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
 | 
			
		||||
                                            {{ proposal.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View Proposal</a>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="text-center py-5">
 | 
			
		||||
                        <i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
 | 
			
		||||
                        <h5>You haven't voted on any proposals yet</h5>
 | 
			
		||||
                        <p class="text-muted">When you vote on proposals, they will appear here.</p>
 | 
			
		||||
                        <a href="/governance/proposals" class="btn btn-primary mt-3">Browse Proposals</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
    <!-- Voting Stats -->
 | 
			
		||||
    {% if votes | length > 0 %}
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-md-4 mb-3">
 | 
			
		||||
            <div class="card text-white bg-success h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title">Yes Votes</h5>
 | 
			
		||||
                    <p class="display-4">
 | 
			
		||||
                        {% set yes_count = 0 %}
 | 
			
		||||
                        {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
 | 
			
		||||
                            {% if vote.vote_type == 'Yes' %}
 | 
			
		||||
                                {% set yes_count = yes_count + 1 %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {{ yes_count }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-4 mb-3">
 | 
			
		||||
            <div class="card text-white bg-danger h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title">No Votes</h5>
 | 
			
		||||
                    <p class="display-4">
 | 
			
		||||
                        {% set no_count = 0 %}
 | 
			
		||||
                        {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
 | 
			
		||||
                            {% if vote.vote_type == 'No' %}
 | 
			
		||||
                                {% set no_count = no_count + 1 %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {{ no_count }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-4 mb-3">
 | 
			
		||||
            <div class="card text-white bg-secondary h-100">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <h5 class="card-title">Abstain Votes</h5>
 | 
			
		||||
                    <p class="display-4">
 | 
			
		||||
                        {% set abstain_count = 0 %}
 | 
			
		||||
                        {% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
 | 
			
		||||
                            {% if vote.vote_type == 'Abstain' %}
 | 
			
		||||
                                {% set abstain_count = abstain_count + 1 %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {{ abstain_count }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,45 +2,8 @@
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block styles %}
 | 
			
		||||
<style>
 | 
			
		||||
    .avatar-circle {
 | 
			
		||||
        width: 32px;
 | 
			
		||||
        height: 32px;
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .comment-text {
 | 
			
		||||
        max-width: 300px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        text-overflow: ellipsis;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .comment-text:hover {
 | 
			
		||||
        white-space: normal;
 | 
			
		||||
        overflow: visible;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .progress {
 | 
			
		||||
        border-radius: 10px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    {% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    {% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <nav aria-label="breadcrumb">
 | 
			
		||||
@@ -67,549 +30,160 @@
 | 
			
		||||
 | 
			
		||||
    <!-- Proposal Details -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-lg-8">
 | 
			
		||||
            <div class="card h-100 shadow-sm">
 | 
			
		||||
                <div class="card-header bg-light">
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h4 class="mb-0">{{ proposal.title }}</h4>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body d-flex flex-column">
 | 
			
		||||
                    <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
                        <span
 | 
			
		||||
                            class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
 | 
			
		||||
                            <i
 | 
			
		||||
                                class="bi {% if proposal.status == 'Active' %}bi-check-circle{% elif proposal.status == 'Approved' %}bi-trophy{% elif proposal.status == 'Rejected' %}bi-x-circle{% elif proposal.status == 'Draft' %}bi-pencil{% else %}bi-exclamation-circle{% endif %} me-1"></i>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="d-flex justify-content-between mb-3">
 | 
			
		||||
                        <span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
 | 
			
		||||
                            {{ proposal.status }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <span class="text-muted"><i class="bi bi-person me-1"></i>Created by {{ proposal.creator_name
 | 
			
		||||
                            }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="flex-grow-1">
 | 
			
		||||
                        <h5><i class="bi bi-file-text me-2"></i>Description</h5>
 | 
			
		||||
                        <div class="p-3 bg-light rounded mb-4">{{ proposal.description }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mt-auto">
 | 
			
		||||
                        <h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
 | 
			
		||||
                            {% if proposal.vote_start_date and proposal.vote_end_date %}
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="text-muted mb-1">Start Date</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="text-center">
 | 
			
		||||
                                <i class="bi bi-arrow-right fs-4 text-muted"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="text-muted mb-1">End Date</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <div class="text-center w-100">Not set</div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <small class="text-muted">Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }}</small>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <h5>Description</h5>
 | 
			
		||||
                    <p class="mb-4">{{ proposal.description }}</p>
 | 
			
		||||
                    
 | 
			
		||||
                    <h5>Voting Period</h5>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        {% if proposal.voting_starts_at and proposal.voting_ends_at %}
 | 
			
		||||
                            <strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
 | 
			
		||||
                            <strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            Not set
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="col-lg-4">
 | 
			
		||||
            <div class="card mb-4 shadow-sm h-100">
 | 
			
		||||
                <div class="card-header bg-primary text-white">
 | 
			
		||||
                    <h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
 | 
			
		||||
        
 | 
			
		||||
        <div class="col-md-4">
 | 
			
		||||
            <div class="card mb-4">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Voting Results</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body d-flex flex-column">
 | 
			
		||||
                    <!-- Voting Results Section -->
 | 
			
		||||
                    <div class="mb-4">
 | 
			
		||||
                        <h6 class="border-bottom pb-2 mb-3">Results</h6>
 | 
			
		||||
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        {% set yes_percent = 0 %}
 | 
			
		||||
                        {% set no_percent = 0 %}
 | 
			
		||||
                        {% set abstain_percent = 0 %}
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
                        {% if results.total_votes > 0 %}
 | 
			
		||||
                        {% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                        {% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                        {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                            {% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                            {% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                            {% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
 | 
			
		||||
                        <!-- Yes votes -->
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center mb-1">
 | 
			
		||||
                            <span class="fw-bold text-success"><i class="bi bi-check-circle-fill me-1"></i> Yes</span>
 | 
			
		||||
                            <span class="badge bg-success rounded-pill">{{ results.yes_count }}</span>
 | 
			
		||||
                        
 | 
			
		||||
                        <p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
 | 
			
		||||
                        <div class="progress mb-3">
 | 
			
		||||
                            <div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="progress mb-3" style="height: 12px;">
 | 
			
		||||
                            <div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
 | 
			
		||||
                                aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100"
 | 
			
		||||
                                title="{{ yes_percent }}% of votes"></div>
 | 
			
		||||
                        
 | 
			
		||||
                        <p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
 | 
			
		||||
                        <div class="progress mb-3">
 | 
			
		||||
                            <div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- No votes -->
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center mb-1">
 | 
			
		||||
                            <span class="fw-bold text-danger"><i class="bi bi-x-circle-fill me-1"></i> No</span>
 | 
			
		||||
                            <span class="badge bg-danger rounded-pill">{{ results.no_count }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="progress mb-3" style="height: 12px;">
 | 
			
		||||
                            <div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
 | 
			
		||||
                                aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100"
 | 
			
		||||
                                title="{{ no_percent }}% of votes"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- Abstain votes -->
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center mb-1">
 | 
			
		||||
                            <span class="fw-bold text-secondary"><i class="bi bi-dash-circle-fill me-1"></i>
 | 
			
		||||
                                Abstain</span>
 | 
			
		||||
                            <span class="badge bg-secondary rounded-pill">{{ results.abstain_count }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="progress mb-3" style="height: 12px;">
 | 
			
		||||
                            <div class="progress-bar bg-secondary" role="progressbar"
 | 
			
		||||
                                style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
 | 
			
		||||
                                aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
 | 
			
		||||
                        
 | 
			
		||||
                        <p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
 | 
			
		||||
                        <div class="progress mb-3">
 | 
			
		||||
                            <div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mt-auto">
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
 | 
			
		||||
                            <div class="text-center">
 | 
			
		||||
                                <h4 class="mb-0">{{ results.total_votes }}</h4>
 | 
			
		||||
                                <small class="text-muted">Total Votes</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            {% if proposal.status == "Active" %}
 | 
			
		||||
                            <div class="text-center">
 | 
			
		||||
                                <div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
 | 
			
		||||
                                    <svg width="60" height="60">
 | 
			
		||||
                                        <circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
 | 
			
		||||
                                        </circle>
 | 
			
		||||
                                        <circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
 | 
			
		||||
                                            stroke-dasharray="157"
 | 
			
		||||
                                            stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
 | 
			
		||||
                                            transform="rotate(-90 30 30)"></circle>
 | 
			
		||||
                                    </svg>
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
 | 
			
		||||
                                        {{ yes_percent }}%</div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <small class="text-muted">Approval Rate</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <!-- Vote Form Section -->
 | 
			
		||||
                    {% if proposal.status == "Active" and user and user.id %}
 | 
			
		||||
                    <div class="mt-auto">
 | 
			
		||||
                        <h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
 | 
			
		||||
                        <form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post"
 | 
			
		||||
                            id="voteForm">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <div class="d-flex gap-2 mb-2">
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="radio" name="vote_type" id="voteYes"
 | 
			
		||||
                                            value="Yes" required>
 | 
			
		||||
                                        <label class="form-check-label text-success" for="voteYes"><i
 | 
			
		||||
                                                class="bi bi-check-circle-fill me-1"></i>Yes</label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="radio" name="vote_type" id="voteNo"
 | 
			
		||||
                                            value="No">
 | 
			
		||||
                                        <label class="form-check-label text-danger" for="voteNo"><i
 | 
			
		||||
                                                class="bi bi-x-circle-fill me-1"></i>No</label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
                                        <input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
 | 
			
		||||
                                            value="Abstain">
 | 
			
		||||
                                        <label class="form-check-label text-secondary" for="voteAbstain"><i
 | 
			
		||||
                                                class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <textarea class="form-control" id="comment" name="comment" rows="2"
 | 
			
		||||
                                    placeholder="Add your thoughts about this proposal (optional)..."></textarea>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <button type="submit" class="btn btn-primary w-100"><i class="bi bi-send me-2"></i>Submit
 | 
			
		||||
                                Vote</button>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% elif proposal.status != "Active" %}
 | 
			
		||||
                    <div class="mt-auto text-center p-3 bg-light rounded">
 | 
			
		||||
                        <i class="bi bi-info-circle fs-4 text-muted"></i>
 | 
			
		||||
                        <p class="mb-0 mt-2">Voting is {{ proposal.status | lower }} for this proposal</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% elif not user or not user.id %}
 | 
			
		||||
                    <div class="mt-auto text-center p-3 bg-light rounded">
 | 
			
		||||
                        <i class="bi bi-person-lock fs-4 text-muted"></i>
 | 
			
		||||
                        <p class="mb-0 mt-2">You must be logged in to vote</p>
 | 
			
		||||
                        <a href="/login" class="btn btn-primary btn-sm mt-2">Login to Vote</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <!-- Vote Form -->
 | 
			
		||||
            {% if proposal.status == "Active" and user and user.id %}
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Cast Your Vote</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/governance/proposals/{{ proposal.id }}/vote" method="post">
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label class="form-label">Vote Type</label>
 | 
			
		||||
                            <div class="form-check">
 | 
			
		||||
                                <input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked>
 | 
			
		||||
                                <label class="form-check-label" for="voteYes">
 | 
			
		||||
                                    Yes - I support this proposal
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-check">
 | 
			
		||||
                                <input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
 | 
			
		||||
                                <label class="form-check-label" for="voteNo">
 | 
			
		||||
                                    No - I oppose this proposal
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-check">
 | 
			
		||||
                                <input class="form-check-input" type="radio" name="vote_type" id="voteAbstain" value="Abstain">
 | 
			
		||||
                                <label class="form-check-label" for="voteAbstain">
 | 
			
		||||
                                    Abstain - I choose not to vote
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="mb-3">
 | 
			
		||||
                            <label for="comment" class="form-label">Comment (Optional)</label>
 | 
			
		||||
                            <textarea class="form-control" id="comment" name="comment" rows="3" placeholder="Explain your vote..."></textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <button type="submit" class="btn btn-primary w-100">Submit Vote</button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% elif not user or not user.id %}
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-body text-center">
 | 
			
		||||
                    <p>You must be logged in to vote.</p>
 | 
			
		||||
                    <a href="/login" class="btn btn-primary">Login to Vote</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
        <!-- Votes List -->
 | 
			
		||||
        <div class="row mt-4">
 | 
			
		||||
            <div class="col-12">
 | 
			
		||||
                <div class="card shadow-sm">
 | 
			
		||||
                    <div class="card-header bg-light">
 | 
			
		||||
                        <h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="card-body p-0">
 | 
			
		||||
                        <div class="table-responsive">
 | 
			
		||||
                            <table class="table table-hover align-middle mb-0">
 | 
			
		||||
                                <thead class="table-light">
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <th class="ps-3">Voter</th>
 | 
			
		||||
                                        <th>Vote</th>
 | 
			
		||||
                                        <th>Comment</th>
 | 
			
		||||
                                        <th class="text-end pe-3">Date</th>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </thead>
 | 
			
		||||
                                <tbody id="votesTableBody">
 | 
			
		||||
                                    {% if votes | length == 0 %}
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <td colspan="4" class="text-center py-4">
 | 
			
		||||
                                            <div class="py-3">
 | 
			
		||||
                                                <i class="bi bi-inbox fs-1 text-muted"></i>
 | 
			
		||||
                                                <p class="mt-2 mb-0">No votes have been cast yet</p>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                    {% for vote in votes %}
 | 
			
		||||
                                    <tr class="vote-row" data-vote-type="{{ vote.vote_type | lower }}">
 | 
			
		||||
                                        <td class="ps-3">
 | 
			
		||||
                                            <div class="d-flex align-items-center">
 | 
			
		||||
                                                <div class="avatar-circle me-2 bg-primary text-white">
 | 
			
		||||
                                                    U
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                                <span>{{ vote.voter_name }}</span>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            <span
 | 
			
		||||
                                                class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %} rounded-pill px-3 py-2">
 | 
			
		||||
                                                {% if vote.vote_type == 'Yes' %}
 | 
			
		||||
                                                <i class="bi bi-check-circle-fill me-1"></i>
 | 
			
		||||
                                                {% elif vote.vote_type == 'No' %}
 | 
			
		||||
                                                <i class="bi bi-x-circle-fill me-1"></i>
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                <i class="bi bi-dash-circle-fill me-1"></i>
 | 
			
		||||
                                                {% endif %}
 | 
			
		||||
                                                {{ vote.vote_type }}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td>
 | 
			
		||||
                                            {% if vote.comment %}
 | 
			
		||||
                                            <div class="comment-text">{{ vote.comment }}</div>
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                            <span class="text-muted fst-italic">No comment provided</span>
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                        <td class="text-end pe-3">
 | 
			
		||||
                                            <div class="d-flex flex-column align-items-end">
 | 
			
		||||
                                                <span>{{ vote.created_at | date(format="%Y-%m-%d") }}</span>
 | 
			
		||||
                                                <small class="text-muted">{{ vote.created_at | date(format="%H:%M")
 | 
			
		||||
                                                    }}</small>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        </td>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </tbody>
 | 
			
		||||
                            </table>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <!-- Pagination Controls -->
 | 
			
		||||
                        {% if votes | length > 10 %}
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center p-3 border-top">
 | 
			
		||||
                            <div class="d-flex align-items-center">
 | 
			
		||||
                                <label class="me-2 text-muted small">Rows per page:</label>
 | 
			
		||||
                                <select id="rowsPerPage" class="form-select form-select-sm" style="width: auto;">
 | 
			
		||||
                                    <option value="10">10</option>
 | 
			
		||||
                                    <option value="25">25</option>
 | 
			
		||||
                                    <option value="50">50</option>
 | 
			
		||||
                                    <option value="100">100</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <nav aria-label="Votes pagination">
 | 
			
		||||
                                    <ul class="pagination pagination-sm mb-0" id="paginationControls">
 | 
			
		||||
                                        <li class="page-item disabled" id="prevPage">
 | 
			
		||||
                                            <a class="page-link" href="#" aria-label="Previous">
 | 
			
		||||
                                                <span aria-hidden="true">«</span>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li class="page-item active"><a class="page-link" href="#">1</a></li>
 | 
			
		||||
                                        <li class="page-item"><a class="page-link" href="#">2</a></li>
 | 
			
		||||
                                        <li class="page-item"><a class="page-link" href="#">3</a></li>
 | 
			
		||||
                                        <li class="page-item" id="nextPage">
 | 
			
		||||
                                            <a class="page-link" href="#" aria-label="Next">
 | 
			
		||||
                                                <span aria-hidden="true">»</span>
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                    </ul>
 | 
			
		||||
                                </nav>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="text-muted small" id="paginationInfo">
 | 
			
		||||
                                Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span
 | 
			
		||||
                                    id="totalRows">{{ votes | length }}</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
    <!-- Votes List -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h5 class="mb-0">Votes ({{ votes | length }})</h5>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if votes | length > 0 %}
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Voter</th>
 | 
			
		||||
                                    <th>Vote</th>
 | 
			
		||||
                                    <th>Comment</th>
 | 
			
		||||
                                    <th>Date</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for vote in votes %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>{{ vote.voter_name }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
 | 
			
		||||
                                            {{ vote.vote_type }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
 | 
			
		||||
                                    <td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <p class="text-center">No votes have been cast yet.</p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Remove query parameters from URL without refreshing the page
 | 
			
		||||
        if (window.location.search.includes('vote_success=true')) {
 | 
			
		||||
            const newUrl = window.location.pathname;
 | 
			
		||||
            window.history.replaceState({}, document.title, newUrl);
 | 
			
		||||
 | 
			
		||||
            // Auto-hide the success alert after 5 seconds
 | 
			
		||||
            const successAlert = document.querySelector('.alert-success');
 | 
			
		||||
            if (successAlert) {
 | 
			
		||||
                setTimeout(function () {
 | 
			
		||||
                    successAlert.classList.remove('show');
 | 
			
		||||
                    setTimeout(function () {
 | 
			
		||||
                        successAlert.remove();
 | 
			
		||||
                    }, 500);
 | 
			
		||||
                }, 5000);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Pagination functionality
 | 
			
		||||
        const rowsPerPageSelect = document.getElementById('rowsPerPage');
 | 
			
		||||
        const paginationControls = document.getElementById('paginationControls');
 | 
			
		||||
        const votesTableBody = document.getElementById('votesTableBody');
 | 
			
		||||
        const startRowElement = document.getElementById('startRow');
 | 
			
		||||
        const endRowElement = document.getElementById('endRow');
 | 
			
		||||
        const totalRowsElement = document.getElementById('totalRows');
 | 
			
		||||
        const prevPageBtn = document.getElementById('prevPage');
 | 
			
		||||
        const nextPageBtn = document.getElementById('nextPage');
 | 
			
		||||
 | 
			
		||||
        let currentPage = 1;
 | 
			
		||||
        let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
 | 
			
		||||
 | 
			
		||||
        // Function to update pagination display
 | 
			
		||||
        function updatePagination() {
 | 
			
		||||
            if (!paginationControls) return;
 | 
			
		||||
 | 
			
		||||
            // Get all rows that match the current filter
 | 
			
		||||
            const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
            const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
            // Get rows that match the current filter and search term
 | 
			
		||||
            let filteredRows = Array.from(voteRows);
 | 
			
		||||
            if (filterType !== 'all') {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Apply search filter if there's a search term
 | 
			
		||||
            const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
            if (searchTerm) {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                    const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                    const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                    return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const totalRows = filteredRows.length;
 | 
			
		||||
 | 
			
		||||
            // Calculate total pages
 | 
			
		||||
            const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
            // Ensure current page is valid
 | 
			
		||||
            if (currentPage > totalPages) {
 | 
			
		||||
                currentPage = totalPages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update pagination controls
 | 
			
		||||
            if (paginationControls) {
 | 
			
		||||
                // Clear existing page links (except prev/next)
 | 
			
		||||
                const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
 | 
			
		||||
                pageLinks.forEach(link => link.remove());
 | 
			
		||||
 | 
			
		||||
                // Add new page links
 | 
			
		||||
                const maxVisiblePages = 5;
 | 
			
		||||
                let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
 | 
			
		||||
                let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
 | 
			
		||||
 | 
			
		||||
                // Adjust if we're near the end
 | 
			
		||||
                if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
 | 
			
		||||
                    startPage = Math.max(1, endPage - maxVisiblePages + 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Insert page links before the next button
 | 
			
		||||
                const nextPageElement = document.getElementById('nextPage');
 | 
			
		||||
                for (let i = startPage; i <= endPage; i++) {
 | 
			
		||||
                    const li = document.createElement('li');
 | 
			
		||||
                    li.className = `page-item ${i === currentPage ? 'active' : ''}`;
 | 
			
		||||
 | 
			
		||||
                    const a = document.createElement('a');
 | 
			
		||||
                    a.className = 'page-link';
 | 
			
		||||
                    a.href = '#';
 | 
			
		||||
                    a.textContent = i;
 | 
			
		||||
                    a.addEventListener('click', function (e) {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        currentPage = i;
 | 
			
		||||
                        updatePagination();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    li.appendChild(a);
 | 
			
		||||
                    paginationControls.insertBefore(li, nextPageElement);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Update prev/next buttons
 | 
			
		||||
                prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
 | 
			
		||||
                nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show current page
 | 
			
		||||
            showCurrentPage();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Function to show current page
 | 
			
		||||
        function showCurrentPage() {
 | 
			
		||||
            if (!votesTableBody) return;
 | 
			
		||||
 | 
			
		||||
            // Get all rows that match the current filter
 | 
			
		||||
            const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
            const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
            // Get rows that match the current filter and search term
 | 
			
		||||
            let filteredRows = Array.from(voteRows);
 | 
			
		||||
            if (filterType !== 'all') {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Apply search filter if there's a search term
 | 
			
		||||
            const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
            if (searchTerm) {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                    const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                    const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                    return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Hide all rows first
 | 
			
		||||
            voteRows.forEach(row => row.style.display = 'none');
 | 
			
		||||
 | 
			
		||||
            // Calculate pagination
 | 
			
		||||
            const totalRows = filteredRows.length;
 | 
			
		||||
            const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
            // Ensure current page is valid
 | 
			
		||||
            if (currentPage > totalPages) {
 | 
			
		||||
                currentPage = totalPages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show only rows for current page
 | 
			
		||||
            const start = (currentPage - 1) * rowsPerPage;
 | 
			
		||||
            const end = start + rowsPerPage;
 | 
			
		||||
 | 
			
		||||
            filteredRows.slice(start, end).forEach(row => row.style.display = '');
 | 
			
		||||
 | 
			
		||||
            // Update pagination info
 | 
			
		||||
            if (startRowElement && endRowElement && totalRowsElement) {
 | 
			
		||||
                startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
 | 
			
		||||
                endRowElement.textContent = Math.min(end, totalRows);
 | 
			
		||||
                totalRowsElement.textContent = totalRows;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Event listeners for pagination
 | 
			
		||||
        if (prevPageBtn) {
 | 
			
		||||
            prevPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                if (currentPage > 1) {
 | 
			
		||||
                    currentPage--;
 | 
			
		||||
                    updatePagination();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (nextPageBtn) {
 | 
			
		||||
            nextPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                // Get all rows that match the current filter
 | 
			
		||||
                const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
                const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
                // Get rows that match the current filter and search term
 | 
			
		||||
                let filteredRows = Array.from(voteRows);
 | 
			
		||||
                if (filterType !== 'all') {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Apply search filter if there's a search term
 | 
			
		||||
                const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
                if (searchTerm) {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                        const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                        const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                        return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const totalRows = filteredRows.length;
 | 
			
		||||
                const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
                if (currentPage < totalPages) {
 | 
			
		||||
                    currentPage++;
 | 
			
		||||
                    updatePagination();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (rowsPerPageSelect) {
 | 
			
		||||
            rowsPerPageSelect.addEventListener('change', function () {
 | 
			
		||||
                rowsPerPage = parseInt(this.value);
 | 
			
		||||
                currentPage = 1; // Reset to first page
 | 
			
		||||
                updatePagination();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize pagination (but don't interfere with filtering)
 | 
			
		||||
        if (paginationControls) {
 | 
			
		||||
            // Only initialize pagination if there are many votes
 | 
			
		||||
            // The filtering will handle showing/hiding rows
 | 
			
		||||
            console.log('Pagination controls available but not interfering with filtering');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize tooltips for all elements with title attributes
 | 
			
		||||
        const tooltipElements = document.querySelectorAll('[title]');
 | 
			
		||||
        if (tooltipElements.length > 0) {
 | 
			
		||||
            [].slice.call(tooltipElements).map(function (el) {
 | 
			
		||||
                return new bootstrap.Tooltip(el);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add debugging for vote form
 | 
			
		||||
        const voteForm = document.getElementById('voteForm');
 | 
			
		||||
        if (voteForm) {
 | 
			
		||||
            console.log('Vote form found:', voteForm);
 | 
			
		||||
            voteForm.addEventListener('submit', function (e) {
 | 
			
		||||
                console.log('Vote form submitted');
 | 
			
		||||
                const formData = new FormData(voteForm);
 | 
			
		||||
                console.log('Form data:', Object.fromEntries(formData));
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            console.log('Vote form not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Debug logging
 | 
			
		||||
        console.log('Filter buttons found:', filterButtons.length);
 | 
			
		||||
        console.log('Vote rows found:', voteRows.length);
 | 
			
		||||
        console.log('Search input found:', searchInput ? 'Yes' : 'No');
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -3,140 +3,128 @@
 | 
			
		||||
{% block title %}Proposals - Governance Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
    <!-- Success message if present -->
 | 
			
		||||
    {% if success %}
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
                {{ success }}
 | 
			
		||||
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-3">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ul class="nav nav-tabs">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance">Dashboard</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link active" href="/governance/proposals">All Proposals</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/my-votes">My Votes</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <a class="nav-link" href="/governance/create">Create Proposal</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<!-- Success message if present -->
 | 
			
		||||
{% if success %}
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="alert alert-success alert-dismissible fade show" role="alert">
 | 
			
		||||
            {{ success }}
 | 
			
		||||
        <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<div class="col-12">
 | 
			
		||||
    <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
        <h5><i class="bi bi-info-circle"></i> About Proposals</h5>
 | 
			
		||||
        <p>Proposals are formal requests for changes to the platform that require community approval. Each proposal
 | 
			
		||||
            includes a detailed description, implementation plan, and voting period. Browse the list below to see all
 | 
			
		||||
            active and past proposals.</p>
 | 
			
		||||
        <div class="mt-2">
 | 
			
		||||
            <a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i
 | 
			
		||||
                    class="bi bi-file-text"></i> Proposal Guidelines</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Filter Controls -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <form action="/governance/proposals" method="get" class="row g-3">
 | 
			
		||||
                    <div class="col-md-4">
 | 
			
		||||
                        <label for="status" class="form-label">Status</label>
 | 
			
		||||
                        <select class="form-select" id="status" name="status">
 | 
			
		||||
                            <option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
 | 
			
		||||
                                Statuses</option>
 | 
			
		||||
                            <option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
 | 
			
		||||
                            <option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
 | 
			
		||||
                            <option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
 | 
			
		||||
                            </option>
 | 
			
		||||
                            <option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
 | 
			
		||||
                            </option>
 | 
			
		||||
                            <option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
 | 
			
		||||
                            </option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <label for="search" class="form-label">Search</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="search" name="search"
 | 
			
		||||
                            placeholder="Search by title or description"
 | 
			
		||||
                            value="{% if search_filter %}{{ search_filter }}{% endif %}">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-2 d-flex align-items-end">
 | 
			
		||||
                        <button type="submit" class="btn btn-primary w-100">Filter</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </form>
 | 
			
		||||
            <h5><i class="bi bi-info-circle"></i> About Proposals</h5>
 | 
			
		||||
            <p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
 | 
			
		||||
            <div class="mt-2">
 | 
			
		||||
                <a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Proposals List -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
            <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                <h5 class="mb-0">All Proposals</h5>
 | 
			
		||||
                <a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                {% if proposals and proposals|length > 0 %}
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
                    <table class="table table-hover">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <th>Title</th>
 | 
			
		||||
                                <th>Creator</th>
 | 
			
		||||
                                <th>Status</th>
 | 
			
		||||
                                <th>Created</th>
 | 
			
		||||
                                <th>Voting Period</th>
 | 
			
		||||
                                <th>Actions</th>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% for proposal in proposals %}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td>{{ proposal.title }}</td>
 | 
			
		||||
                                <td>{{ proposal.creator_name }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <span
 | 
			
		||||
                                        class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
 | 
			
		||||
                                        {{ proposal.status }}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    {% if proposal.vote_start_date and proposal.vote_end_date %}
 | 
			
		||||
                                    {{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
 | 
			
		||||
                                    proposal.vote_end_date | date(format="%Y-%m-%d") }}
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                    Not set
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    <a href="/governance/proposals/{{ proposal.base_data.id }}"
 | 
			
		||||
                                        class="btn btn-sm btn-primary">View</a>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="alert alert-info text-center py-5">
 | 
			
		||||
                        <i class="bi bi-info-circle fs-1 mb-3"></i>
 | 
			
		||||
                        <h5>No proposals found</h5>
 | 
			
		||||
                        {% if status_filter or search_filter %}
 | 
			
		||||
                        <p>No proposals match your current filter criteria. Try adjusting your filters or <a
 | 
			
		||||
                                href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <p>There are no proposals in the system yet.</p>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        <a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
    <!-- Filter Controls -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form action="/governance/proposals" method="get" class="row g-3">
 | 
			
		||||
                        <div class="col-md-4">
 | 
			
		||||
                            <label for="status" class="form-label">Status</label>
 | 
			
		||||
                            <select class="form-select" id="status" name="status">
 | 
			
		||||
                                <option value="">All Statuses</option>
 | 
			
		||||
                                <option value="Draft">Draft</option>
 | 
			
		||||
                                <option value="Active">Active</option>
 | 
			
		||||
                                <option value="Approved">Approved</option>
 | 
			
		||||
                                <option value="Rejected">Rejected</option>
 | 
			
		||||
                                <option value="Cancelled">Cancelled</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <label for="search" class="form-label">Search</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-2 d-flex align-items-end">
 | 
			
		||||
                            <button type="submit" class="btn btn-primary w-100">Filter</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
    <!-- Proposals List -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header d-flex justify-content-between align-items-center">
 | 
			
		||||
                    <h5 class="mb-0">All Proposals</h5>
 | 
			
		||||
                    <a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <div class="table-responsive">
 | 
			
		||||
                        <table class="table table-hover">
 | 
			
		||||
                            <thead>
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <th>Title</th>
 | 
			
		||||
                                    <th>Creator</th>
 | 
			
		||||
                                    <th>Status</th>
 | 
			
		||||
                                    <th>Created</th>
 | 
			
		||||
                                    <th>Voting Period</th>
 | 
			
		||||
                                    <th>Actions</th>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            </thead>
 | 
			
		||||
                            <tbody>
 | 
			
		||||
                                {% for proposal in proposals %}
 | 
			
		||||
                                <tr>
 | 
			
		||||
                                    <td>{{ proposal.title }}</td>
 | 
			
		||||
                                    <td>{{ proposal.creator_name }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
 | 
			
		||||
                                            {{ proposal.status }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        {% if proposal.voting_starts_at and proposal.voting_ends_at %}
 | 
			
		||||
                                            {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
 | 
			
		||||
                                        {% else %}
 | 
			
		||||
                                            Not set
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                    <td>
 | 
			
		||||
                                        <a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
 | 
			
		||||
                                    </td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </tbody>
 | 
			
		||||
                        </table>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,362 +0,0 @@
 | 
			
		||||
use actix_mvc_app::controllers::payment::CompanyRegistrationData;
 | 
			
		||||
use actix_mvc_app::db::payment as payment_db;
 | 
			
		||||
use actix_mvc_app::db::registration as registration_db;
 | 
			
		||||
use actix_mvc_app::utils::stripe_security::StripeWebhookVerifier;
 | 
			
		||||
use actix_mvc_app::validators::CompanyRegistrationValidator;
 | 
			
		||||
use heromodels::models::biz::PaymentStatus;
 | 
			
		||||
use hmac::{Hmac, Mac};
 | 
			
		||||
use sha2::Sha256;
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod payment_flow_tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    fn create_valid_registration_data() -> CompanyRegistrationData {
 | 
			
		||||
        CompanyRegistrationData {
 | 
			
		||||
            company_name: "Test Company Ltd".to_string(),
 | 
			
		||||
            company_type: "Single FZC".to_string(),
 | 
			
		||||
            company_email: Some("test@example.com".to_string()),
 | 
			
		||||
            company_phone: Some("+1234567890".to_string()),
 | 
			
		||||
            company_website: Some("https://example.com".to_string()),
 | 
			
		||||
            company_address: Some("123 Test Street, Test City".to_string()),
 | 
			
		||||
            company_industry: Some("Technology".to_string()),
 | 
			
		||||
            company_purpose: Some("Software development".to_string()),
 | 
			
		||||
            fiscal_year_end: Some("December".to_string()),
 | 
			
		||||
            shareholders: r#"[{"name": "John Doe", "percentage": 100}]"#.to_string(),
 | 
			
		||||
            payment_plan: "monthly".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_registration_data_validation_success() {
 | 
			
		||||
        let data = create_valid_registration_data();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
 | 
			
		||||
        assert!(
 | 
			
		||||
            result.is_valid,
 | 
			
		||||
            "Valid registration data should pass validation"
 | 
			
		||||
        );
 | 
			
		||||
        assert!(result.errors.is_empty(), "Valid data should have no errors");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_registration_data_validation_failures() {
 | 
			
		||||
        // Test empty company name
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_name = "".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_name"));
 | 
			
		||||
 | 
			
		||||
        // Test invalid email
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_email = Some("invalid-email".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_email"));
 | 
			
		||||
 | 
			
		||||
        // Test invalid phone
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_phone = Some("123".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_phone"));
 | 
			
		||||
 | 
			
		||||
        // Test invalid website
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.company_website = Some("not-a-url".to_string());
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "company_website"));
 | 
			
		||||
 | 
			
		||||
        // Test invalid shareholders JSON
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.shareholders = "invalid json".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "shareholders"));
 | 
			
		||||
 | 
			
		||||
        // Test invalid payment plan
 | 
			
		||||
        let mut data = create_valid_registration_data();
 | 
			
		||||
        data.payment_plan = "invalid_plan".to_string();
 | 
			
		||||
        let result = CompanyRegistrationValidator::validate(&data);
 | 
			
		||||
        assert!(!result.is_valid);
 | 
			
		||||
        assert!(result.errors.iter().any(|e| e.field == "payment_plan"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_registration_data_storage_and_retrieval() {
 | 
			
		||||
        let payment_intent_id = "pi_test_123456".to_string();
 | 
			
		||||
        let data = create_valid_registration_data();
 | 
			
		||||
 | 
			
		||||
        // Store registration data
 | 
			
		||||
        let store_result =
 | 
			
		||||
            registration_db::store_registration_data(payment_intent_id.clone(), data.clone());
 | 
			
		||||
        assert!(
 | 
			
		||||
            store_result.is_ok(),
 | 
			
		||||
            "Should successfully store registration data"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Retrieve registration data
 | 
			
		||||
        let retrieve_result = registration_db::get_registration_data(&payment_intent_id);
 | 
			
		||||
        assert!(
 | 
			
		||||
            retrieve_result.is_ok(),
 | 
			
		||||
            "Should successfully retrieve registration data"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let retrieved_data = retrieve_result.unwrap();
 | 
			
		||||
        assert!(
 | 
			
		||||
            retrieved_data.is_some(),
 | 
			
		||||
            "Should find stored registration data"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let stored_data = retrieved_data.unwrap();
 | 
			
		||||
        assert_eq!(stored_data.company_name, data.company_name);
 | 
			
		||||
        assert_eq!(stored_data.company_email, data.company_email.unwrap());
 | 
			
		||||
        assert_eq!(stored_data.payment_plan, data.payment_plan);
 | 
			
		||||
 | 
			
		||||
        // Clean up
 | 
			
		||||
        let _ = registration_db::delete_registration_data(&payment_intent_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_payment_creation_and_status_updates() {
 | 
			
		||||
        let payment_intent_id = "pi_test_payment_123".to_string();
 | 
			
		||||
 | 
			
		||||
        // Create a payment
 | 
			
		||||
        let create_result = payment_db::create_new_payment(
 | 
			
		||||
            payment_intent_id.clone(),
 | 
			
		||||
            0, // Temporary company_id
 | 
			
		||||
            "monthly".to_string(),
 | 
			
		||||
            20.0, // setup_fee
 | 
			
		||||
            20.0, // monthly_fee
 | 
			
		||||
            40.0, // total_amount
 | 
			
		||||
        );
 | 
			
		||||
        assert!(create_result.is_ok(), "Should successfully create payment");
 | 
			
		||||
 | 
			
		||||
        let (payment_id, payment) = create_result.unwrap();
 | 
			
		||||
        assert_eq!(payment.payment_intent_id, payment_intent_id);
 | 
			
		||||
        assert_eq!(payment.status, PaymentStatus::Pending);
 | 
			
		||||
 | 
			
		||||
        // Update payment status to completed
 | 
			
		||||
        let update_result =
 | 
			
		||||
            payment_db::update_payment_status(&payment_intent_id, PaymentStatus::Completed);
 | 
			
		||||
        assert!(
 | 
			
		||||
            update_result.is_ok(),
 | 
			
		||||
            "Should successfully update payment status"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let updated_payment = update_result.unwrap();
 | 
			
		||||
        assert!(updated_payment.is_some(), "Should return updated payment");
 | 
			
		||||
        assert_eq!(updated_payment.unwrap().status, PaymentStatus::Completed);
 | 
			
		||||
 | 
			
		||||
        // Test updating company ID
 | 
			
		||||
        let company_id = 123u32;
 | 
			
		||||
        let link_result = payment_db::update_payment_company_id(&payment_intent_id, company_id);
 | 
			
		||||
        assert!(
 | 
			
		||||
            link_result.is_ok(),
 | 
			
		||||
            "Should successfully link payment to company"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let linked_payment = link_result.unwrap();
 | 
			
		||||
        assert!(linked_payment.is_some(), "Should return linked payment");
 | 
			
		||||
        assert_eq!(linked_payment.unwrap().company_id, company_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_payment_queries() {
 | 
			
		||||
        // Test getting pending payments
 | 
			
		||||
        let pending_result = payment_db::get_pending_payments();
 | 
			
		||||
        assert!(
 | 
			
		||||
            pending_result.is_ok(),
 | 
			
		||||
            "Should successfully get pending payments"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Test getting failed payments
 | 
			
		||||
        let failed_result = payment_db::get_failed_payments();
 | 
			
		||||
        assert!(
 | 
			
		||||
            failed_result.is_ok(),
 | 
			
		||||
            "Should successfully get failed payments"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Test getting payment by intent ID
 | 
			
		||||
        let get_result = payment_db::get_payment_by_intent_id("nonexistent_payment");
 | 
			
		||||
        assert!(
 | 
			
		||||
            get_result.is_ok(),
 | 
			
		||||
            "Should handle nonexistent payment gracefully"
 | 
			
		||||
        );
 | 
			
		||||
        assert!(
 | 
			
		||||
            get_result.unwrap().is_none(),
 | 
			
		||||
            "Should return None for nonexistent payment"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_pricing_calculations() {
 | 
			
		||||
        // Test pricing calculation logic
 | 
			
		||||
        fn calculate_total_amount(setup_fee: f64, monthly_fee: f64, payment_plan: &str) -> f64 {
 | 
			
		||||
            match payment_plan {
 | 
			
		||||
                "monthly" => setup_fee + monthly_fee,
 | 
			
		||||
                "yearly" => setup_fee + (monthly_fee * 12.0 * 0.8), // 20% discount
 | 
			
		||||
                "two_year" => setup_fee + (monthly_fee * 24.0 * 0.6), // 40% discount
 | 
			
		||||
                _ => setup_fee + monthly_fee,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Test monthly pricing
 | 
			
		||||
        let monthly_total = calculate_total_amount(20.0, 20.0, "monthly");
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            monthly_total, 40.0,
 | 
			
		||||
            "Monthly total should be setup + monthly fee"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Test yearly pricing (20% discount)
 | 
			
		||||
        let yearly_total = calculate_total_amount(20.0, 20.0, "yearly");
 | 
			
		||||
        let expected_yearly = 20.0 + (20.0 * 12.0 * 0.8); // Setup + discounted yearly
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            yearly_total, expected_yearly,
 | 
			
		||||
            "Yearly total should include 20% discount"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Test two-year pricing (40% discount)
 | 
			
		||||
        let two_year_total = calculate_total_amount(20.0, 20.0, "two_year");
 | 
			
		||||
        let expected_two_year = 20.0 + (20.0 * 24.0 * 0.6); // Setup + discounted two-year
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            two_year_total, expected_two_year,
 | 
			
		||||
            "Two-year total should include 40% discount"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_company_type_mapping() {
 | 
			
		||||
        let test_cases = vec![
 | 
			
		||||
            ("Single FZC", "Single"),
 | 
			
		||||
            ("Startup FZC", "Starter"),
 | 
			
		||||
            ("Growth FZC", "Global"),
 | 
			
		||||
            ("Global FZC", "Global"),
 | 
			
		||||
            ("Cooperative FZC", "Coop"),
 | 
			
		||||
            ("Twin FZC", "Twin"),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        for (input, expected) in test_cases {
 | 
			
		||||
            // This would test the business type mapping in create_company_from_form_data
 | 
			
		||||
            // We'll need to expose this logic or test it indirectly
 | 
			
		||||
            assert!(true, "Company type mapping test placeholder for {}", input);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod webhook_security_tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use std::time::{SystemTime, UNIX_EPOCH};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_webhook_signature_verification_valid() {
 | 
			
		||||
        let payload = b"test payload";
 | 
			
		||||
        let webhook_secret = "whsec_test_secret";
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        // Create a valid signature
 | 
			
		||||
        let signed_payload = format!("{}.{}", current_time, std::str::from_utf8(payload).unwrap());
 | 
			
		||||
        let mut mac = Hmac::<Sha256>::new_from_slice(webhook_secret.as_bytes()).unwrap();
 | 
			
		||||
        mac.update(signed_payload.as_bytes());
 | 
			
		||||
        let signature = hex::encode(mac.finalize().into_bytes());
 | 
			
		||||
 | 
			
		||||
        let signature_header = format!("t={},v1={}", current_time, signature);
 | 
			
		||||
 | 
			
		||||
        let result = StripeWebhookVerifier::verify_signature(
 | 
			
		||||
            payload,
 | 
			
		||||
            &signature_header,
 | 
			
		||||
            webhook_secret,
 | 
			
		||||
            Some(300),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert!(result.is_ok(), "Valid signature should verify successfully");
 | 
			
		||||
        assert!(result.unwrap(), "Valid signature should return true");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_webhook_signature_verification_invalid() {
 | 
			
		||||
        let payload = b"test payload";
 | 
			
		||||
        let webhook_secret = "whsec_test_secret";
 | 
			
		||||
        let current_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs();
 | 
			
		||||
 | 
			
		||||
        // Create an invalid signature
 | 
			
		||||
        let signature_header = format!("t={},v1=invalid_signature", current_time);
 | 
			
		||||
 | 
			
		||||
        let result = StripeWebhookVerifier::verify_signature(
 | 
			
		||||
            payload,
 | 
			
		||||
            &signature_header,
 | 
			
		||||
            webhook_secret,
 | 
			
		||||
            Some(300),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert!(result.is_ok(), "Invalid signature should not cause error");
 | 
			
		||||
        assert!(!result.unwrap(), "Invalid signature should return false");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_webhook_signature_verification_expired() {
 | 
			
		||||
        let payload = b"test payload";
 | 
			
		||||
        let webhook_secret = "whsec_test_secret";
 | 
			
		||||
        let old_time = SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .as_secs()
 | 
			
		||||
            - 400; // 400 seconds ago (beyond 300s tolerance)
 | 
			
		||||
 | 
			
		||||
        // Create a signature with old timestamp
 | 
			
		||||
        let signed_payload = format!("{}.{}", old_time, std::str::from_utf8(payload).unwrap());
 | 
			
		||||
        let mut mac = Hmac::<Sha256>::new_from_slice(webhook_secret.as_bytes()).unwrap();
 | 
			
		||||
        mac.update(signed_payload.as_bytes());
 | 
			
		||||
        let signature = hex::encode(mac.finalize().into_bytes());
 | 
			
		||||
 | 
			
		||||
        let signature_header = format!("t={},v1={}", old_time, signature);
 | 
			
		||||
 | 
			
		||||
        let result = StripeWebhookVerifier::verify_signature(
 | 
			
		||||
            payload,
 | 
			
		||||
            &signature_header,
 | 
			
		||||
            webhook_secret,
 | 
			
		||||
            Some(300),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assert!(result.is_err(), "Expired signature should return error");
 | 
			
		||||
        assert!(
 | 
			
		||||
            result.unwrap_err().contains("too old"),
 | 
			
		||||
            "Error should mention timestamp age"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_webhook_signature_verification_malformed_header() {
 | 
			
		||||
        let payload = b"test payload";
 | 
			
		||||
        let webhook_secret = "whsec_test_secret";
 | 
			
		||||
 | 
			
		||||
        // Test various malformed headers
 | 
			
		||||
        let malformed_headers = vec![
 | 
			
		||||
            "invalid_header",
 | 
			
		||||
            "t=123",
 | 
			
		||||
            "v1=signature",
 | 
			
		||||
            "t=invalid_timestamp,v1=signature",
 | 
			
		||||
            "",
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        for header in malformed_headers {
 | 
			
		||||
            let result =
 | 
			
		||||
                StripeWebhookVerifier::verify_signature(payload, header, webhook_secret, Some(300));
 | 
			
		||||
 | 
			
		||||
            assert!(
 | 
			
		||||
                result.is_err(),
 | 
			
		||||
                "Malformed header '{}' should return error",
 | 
			
		||||
                header
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2809
									
								
								flowbroker/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2809
									
								
								flowbroker/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								flowbroker/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								flowbroker/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "flowbroker"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
sigsocket = { path = "../sigsocket" } # Path relative to flowbroker directory
 | 
			
		||||
actix-web = "4.3.1"
 | 
			
		||||
actix-rt = "2.8.0"
 | 
			
		||||
actix-files = "0.6.2"
 | 
			
		||||
actix-web-actors = "4.2.0"
 | 
			
		||||
serde = { version = "1.0", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
env_logger = "0.10.0"
 | 
			
		||||
log = "0.4.0"
 | 
			
		||||
tera = "1.19.0"
 | 
			
		||||
tokio = { version = "1.28.0", features = ["full"] }
 | 
			
		||||
dotenv = "0.15.0"
 | 
			
		||||
hex = "0.4.3"
 | 
			
		||||
uuid = { version = "1.4", features = ["v4", "serde"] }
 | 
			
		||||
chrono = { version = "0.4", features = ["serde"] } # For timestamps
 | 
			
		||||
rhai = "1.18.0"
 | 
			
		||||
serde_urlencoded = "0.7"
 | 
			
		||||
 | 
			
		||||
# Database models and ORM-like functionality
 | 
			
		||||
heromodels = { path = "../../db/heromodels" }
 | 
			
		||||
# Note: heromodels pulls in 'ourdb', 'heromodels_core', 'heromodels_derive'
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/data/0.db
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/data/0.db
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/data/lookup/data
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/data/lookup/data
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/index/0.db
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/index/0.db
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								flowbroker/flowbroker_db/index/lookup/.inc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								flowbroker/flowbroker_db/index/lookup/.inc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
148
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/index/lookup/data
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								flowbroker/flowbroker_db/index/lookup/data
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										690
									
								
								flowbroker/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										690
									
								
								flowbroker/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,690 @@
 | 
			
		||||
use actix_files as fs;
 | 
			
		||||
use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result as ActixResult};
 | 
			
		||||
use std::fs as std_fs;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_urlencoded; // Added for from_str
 | 
			
		||||
use tera::{Tera, Context};
 | 
			
		||||
use std::sync::{Arc, Mutex, RwLock};
 | 
			
		||||
use sigsocket::service::SigSocketService;
 | 
			
		||||
use sigsocket::registry::ConnectionRegistry;
 | 
			
		||||
use log::{info, error};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
use rhai::{Engine, EvalAltResult, Position};
 | 
			
		||||
// use std::collections::HashMap; // Removed as no longer used
 | 
			
		||||
use heromodels; // Added for database models
 | 
			
		||||
use heromodels::db::hero::OurDB;
 | 
			
		||||
use heromodels::db::{Db, Collection}; // Import Db trait for .collection() and Collection trait for .set()/.get_all()
 | 
			
		||||
use heromodels::models::flowbroker_models::{Flow, FlowStep, SignatureRequirement}; // Import the models
 | 
			
		||||
use dotenv::dotenv;
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
// --- Flowbroker Specific Enums (to be used by application logic) ---
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
 | 
			
		||||
pub enum FlowStepStatus {
 | 
			
		||||
    Pending,    // Step created, not yet processed
 | 
			
		||||
    InProgress, // Step is actively being processed (e.g., waiting for signatures)
 | 
			
		||||
    Completed,  // All requirements for this step are met
 | 
			
		||||
    Failed,     // Step failed (e.g., a signature requirement failed or timed out)
 | 
			
		||||
    Skipped,    // Step was skipped (e.g., due to conditional logic not yet implemented)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FlowStepStatus {
 | 
			
		||||
    pub fn to_db_string(&self) -> String {
 | 
			
		||||
        format!("{:?}", self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_db_string(s: &str) -> Result<Self, String> {
 | 
			
		||||
        match s {
 | 
			
		||||
            "Pending" => Ok(FlowStepStatus::Pending),
 | 
			
		||||
            "InProgress" => Ok(FlowStepStatus::InProgress),
 | 
			
		||||
            "Completed" => Ok(FlowStepStatus::Completed),
 | 
			
		||||
            "Failed" => Ok(FlowStepStatus::Failed),
 | 
			
		||||
            "Skipped" => Ok(FlowStepStatus::Skipped),
 | 
			
		||||
            _ => Err(format!("Invalid FlowStepStatus string: {}", s)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
 | 
			
		||||
pub enum SignatureRequirementStatus {
 | 
			
		||||
    Pending,        // Not yet processed or sent for signing
 | 
			
		||||
    SentToClient,   // Sent to client via SigSocket, awaiting signature
 | 
			
		||||
    Signed,         // Successfully signed
 | 
			
		||||
    Failed,         // Signing failed (e.g., client rejected, timeout, error)
 | 
			
		||||
    Error,          // An internal error occurred processing this requirement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SignatureRequirementStatus {
 | 
			
		||||
    pub fn to_db_string(&self) -> String {
 | 
			
		||||
        format!("{:?}", self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_db_string(s: &str) -> Result<Self, String> {
 | 
			
		||||
        match s {
 | 
			
		||||
            "Pending" => Ok(SignatureRequirementStatus::Pending),
 | 
			
		||||
            "SentToClient" => Ok(SignatureRequirementStatus::SentToClient),
 | 
			
		||||
            "Signed" => Ok(SignatureRequirementStatus::Signed),
 | 
			
		||||
            "Failed" => Ok(SignatureRequirementStatus::Failed),
 | 
			
		||||
            "Error" => Ok(SignatureRequirementStatus::Error),
 | 
			
		||||
            _ => Err(format!("Invalid SignatureRequirementStatus string: {}", s)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
 | 
			
		||||
pub enum FlowStatus {
 | 
			
		||||
    Pending,    // Flow created, no steps initiated
 | 
			
		||||
    InProgress, // Flow started, steps are being processed
 | 
			
		||||
    Completed,  // All steps successfully signed
 | 
			
		||||
    Failed,     // A step failed or timed out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FlowStatus {
 | 
			
		||||
    pub fn to_db_string(&self) -> String {
 | 
			
		||||
        format!("{:?}", self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_db_string(s: &str) -> Result<Self, String> {
 | 
			
		||||
        match s {
 | 
			
		||||
            "Pending" => Ok(FlowStatus::Pending),
 | 
			
		||||
            "InProgress" => Ok(FlowStatus::InProgress),
 | 
			
		||||
            "Completed" => Ok(FlowStatus::Completed),
 | 
			
		||||
            "Failed" => Ok(FlowStatus::Failed),
 | 
			
		||||
            _ => Err(format!("Invalid FlowStatus string: {}", s)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE: The old Flow, FlowStep, and SignatureRequirement structs previously here
 | 
			
		||||
// have been removed. Their definitions are now in the heromodels crate.
 | 
			
		||||
 | 
			
		||||
// --- AppState ---
 | 
			
		||||
pub struct AppState {
 | 
			
		||||
    templates: Tera,
 | 
			
		||||
    sigsocket_service: Arc<SigSocketService>,
 | 
			
		||||
    db: Arc<OurDB>, // Using OurDB from heromodels
 | 
			
		||||
    next_id_counter: Arc<Mutex<u32>>, // For generating temporary primary keys
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Form Deserialization (for new dynamic form) ---
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct RequirementRealFormData {
 | 
			
		||||
    // The name attributes in HTML are like: steps[0][requirements][0][message]
 | 
			
		||||
    pub message: String,      // Made fields public for external construction in tests
 | 
			
		||||
    pub public_key: String,   // Made fields public
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct FlowStepFormData {
 | 
			
		||||
    description: Option<String>, // If description field is optional and might not be present
 | 
			
		||||
    requirements: Vec<RequirementRealFormData>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct CreateFlowRealFormData { // Renamed to avoid confusion with heromodels::Flow
 | 
			
		||||
    flow_name: String,
 | 
			
		||||
    steps: Vec<FlowStepFormData>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, Debug)]
 | 
			
		||||
pub struct RhaiScriptFormData {
 | 
			
		||||
    rhai_script: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// --- Context Structs for Templates ---
 | 
			
		||||
#[derive(Serialize, Clone)]
 | 
			
		||||
struct RhaiExampleDisplay {
 | 
			
		||||
    name: String,
 | 
			
		||||
    content: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
struct ListFlowsContext {
 | 
			
		||||
    flows: Vec<Flow>, // Using heromodels::models::flowbroker_models::Flow
 | 
			
		||||
    example_scripts: Vec<RhaiExampleDisplay>,
 | 
			
		||||
    error_message: Option<String>,
 | 
			
		||||
    success_message: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Handlers ---
 | 
			
		||||
 | 
			
		||||
// Display list of flows
 | 
			
		||||
async fn list_flows(data: web::Data<AppState>) -> ActixResult<HttpResponse> {
 | 
			
		||||
    let tera = &data.templates;
 | 
			
		||||
 | 
			
		||||
    // Fetch actual flows from the database
 | 
			
		||||
    let flows_collection = data.db.collection::<Flow>()
 | 
			
		||||
        .map_err(|e| actix_web::error::ErrorInternalServerError(format!("DB Error: Failed to get flows collection: {}", e)))?;
 | 
			
		||||
    let (mut flows, flow_error_message) = match flows_collection.get_all() {
 | 
			
		||||
        Ok(mut flows_vec) => {
 | 
			
		||||
            flows_vec.sort_by(|a, b| b.base_data.created_at.cmp(&a.base_data.created_at)); // Sort by newest
 | 
			
		||||
            (flows_vec, None)
 | 
			
		||||
        },
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to fetch flows: {:?}", e);
 | 
			
		||||
            (Vec::new(), Some(format!("Error fetching flows: {:?}", e)))
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Load Rhai example scripts
 | 
			
		||||
    let examples_path = PathBuf::from("templates/rhai_examples");
 | 
			
		||||
    let mut example_scripts_display = Vec::new();
 | 
			
		||||
    if examples_path.is_dir() {
 | 
			
		||||
        match std_fs::read_dir(examples_path) {
 | 
			
		||||
            Ok(entries) => {
 | 
			
		||||
                for entry in entries {
 | 
			
		||||
                    if let Ok(entry) = entry {
 | 
			
		||||
                        let path = entry.path();
 | 
			
		||||
                        if path.is_file() && path.extension().and_then(std::ffi::OsStr::to_str) == Some("rhai") {
 | 
			
		||||
                            let file_stem = path.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Unknown Script");
 | 
			
		||||
                            // Convert filename (e.g., simple_two_step) to a nicer name (e.g., Simple Two Step)
 | 
			
		||||
                            let script_name = file_stem.replace("_", " ")
 | 
			
		||||
                                .split_whitespace()
 | 
			
		||||
                                .map(|word| {
 | 
			
		||||
                                    let mut c = word.chars();
 | 
			
		||||
                                    match c.next() {
 | 
			
		||||
                                        None => String::new(),
 | 
			
		||||
                                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
 | 
			
		||||
                                    }
 | 
			
		||||
                                })
 | 
			
		||||
                                .collect::<Vec<String>>().join(" ");
 | 
			
		||||
 | 
			
		||||
                            match std_fs::read_to_string(&path) {
 | 
			
		||||
                                Ok(content) => {
 | 
			
		||||
                                    example_scripts_display.push(RhaiExampleDisplay { name: script_name, content });
 | 
			
		||||
                                }
 | 
			
		||||
                                Err(e) => {
 | 
			
		||||
                                    error!("Failed to read Rhai example script {:?}: {}", path, e);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                error!("Failed to read Rhai examples directory: {}", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    example_scripts_display.sort_by(|a, b| a.name.cmp(&b.name));
 | 
			
		||||
 | 
			
		||||
    let list_context = ListFlowsContext {
 | 
			
		||||
        flows,
 | 
			
		||||
        example_scripts: example_scripts_display,
 | 
			
		||||
        error_message: flow_error_message,
 | 
			
		||||
        success_message: None, // TODO: Populate from query params or session later if needed
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let tera_ctx = Context::from_serialize(&list_context).unwrap_or_else(|e| {
 | 
			
		||||
        error!("Failed to serialize ListFlowsContext: {}", e);
 | 
			
		||||
        // Fallback to a minimal context or an error state if serialization fails
 | 
			
		||||
        let mut err_ctx = Context::new();
 | 
			
		||||
        err_ctx.insert("error_message", &"Critical error preparing page data.".to_string());
 | 
			
		||||
        err_ctx
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Still rendering to index.html, which will be the revamped list_flows.html
 | 
			
		||||
    let rendered = tera.render("index.html", &tera_ctx)
 | 
			
		||||
        .map_err(|e| actix_web::error::ErrorInternalServerError(format!("Template error (index.html): {}", e)))?;
 | 
			
		||||
    Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handle creation of a new flow
 | 
			
		||||
async fn create_flow(
 | 
			
		||||
    data: web::Data<AppState>,
 | 
			
		||||
    raw_form_data: String, // Changed to accept raw String
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    info!("Received raw form data for create_flow: {}", raw_form_data);
 | 
			
		||||
 | 
			
		||||
    // Attempt to parse the raw form data
 | 
			
		||||
    let form_parse_result: Result<CreateFlowRealFormData, serde_urlencoded::de::Error> = serde_urlencoded::from_str(&raw_form_data);
 | 
			
		||||
 | 
			
		||||
    let form = match form_parse_result {
 | 
			
		||||
        Ok(parsed_form_data) => {
 | 
			
		||||
            info!("Successfully parsed form data: {:?}", parsed_form_data);
 | 
			
		||||
            parsed_form_data // Use the successfully parsed data
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to parse form data from string: {}. Raw data: {}", e, raw_form_data);
 | 
			
		||||
            return HttpResponse::BadRequest().body(format!("Form parsing error: {}. Please check input and logs.", e));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // --- Logic starts here, using `form` which is now CreateFlowRealFormData ---
 | 
			
		||||
    info!("Processing create_flow request for: {}", form.flow_name);
 | 
			
		||||
 | 
			
		||||
    let db = &data.db;
 | 
			
		||||
    let mut id_counter = match data.next_id_counter.lock() {
 | 
			
		||||
        Ok(guard) => guard,
 | 
			
		||||
        Err(poisoned) => {
 | 
			
		||||
            error!("Mutex for next_id_counter was poisoned: {}. Recovering.", poisoned);
 | 
			
		||||
            poisoned.into_inner() // Attempt to recover
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 1. Create and save the main Flow object
 | 
			
		||||
    *id_counter += 1;
 | 
			
		||||
    let flow_db_id = *id_counter;
 | 
			
		||||
    let flow_uuid = Uuid::new_v4().to_string();
 | 
			
		||||
    
 | 
			
		||||
    let flow_instance = Flow::new(
 | 
			
		||||
        flow_db_id, 
 | 
			
		||||
        &flow_uuid, 
 | 
			
		||||
        &form.flow_name, 
 | 
			
		||||
        FlowStatus::Pending.to_db_string() // Use local enum's string representation
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    match db.collection::<Flow>() {
 | 
			
		||||
        Ok(flow_collection) => {
 | 
			
		||||
            if let Err(e) = flow_collection.set(&flow_instance) {
 | 
			
		||||
                error!("Failed to save Flow (name: {}): {:?}. Aborting flow creation.", form.flow_name, e);
 | 
			
		||||
                return HttpResponse::InternalServerError().body(format!("Failed to save main flow data: {:?}", e));
 | 
			
		||||
            }
 | 
			
		||||
            info!("Saved Flow object for '{}', UUID: {}, DB_ID: {}", flow_instance.name, flow_instance.flow_uuid, flow_instance.base_data.id);
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to get Flow collection: {:?}. Aborting flow creation.", e);
 | 
			
		||||
            return HttpResponse::InternalServerError().body(format!("Database error getting flow collection: {:?}", e));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 2. Create and save FlowStep and SignatureRequirement objects
 | 
			
		||||
    for (step_idx, step_form_data) in form.steps.into_iter().enumerate() {
 | 
			
		||||
        *id_counter += 1;
 | 
			
		||||
        let flow_step_db_id = *id_counter;
 | 
			
		||||
        
 | 
			
		||||
        let mut flow_step_instance = FlowStep::new(
 | 
			
		||||
            flow_step_db_id, 
 | 
			
		||||
            flow_instance.base_data.id, // Use ID from the saved Flow instance
 | 
			
		||||
            step_idx as u32, // step_order
 | 
			
		||||
            FlowStepStatus::Pending.to_db_string() // Use local enum's string representation
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if let Some(desc) = step_form_data.description {
 | 
			
		||||
            if !desc.is_empty() { // Only set if description is not empty
 | 
			
		||||
                flow_step_instance = flow_step_instance.description(desc);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match db.collection::<FlowStep>() {
 | 
			
		||||
            Ok(step_collection) => {
 | 
			
		||||
                if let Err(e) = step_collection.set(&flow_step_instance) {
 | 
			
		||||
                    error!("Failed to save FlowStep (flow: {}, step_idx: {}): {:?}", flow_instance.name, step_idx, e);
 | 
			
		||||
                    return HttpResponse::InternalServerError().body(format!("Failed to save flow step: {:?}", e));
 | 
			
		||||
                }
 | 
			
		||||
                info!("Saved FlowStep {} for flow '{}', DB_ID: {}", step_idx + 1, flow_instance.name, flow_step_instance.base_data.id);
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                error!("Failed to get FlowStep collection: {:?}. Aborting.", e);
 | 
			
		||||
                return HttpResponse::InternalServerError().body(format!("Database error getting step collection: {:?}", e));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (req_idx, req_form_data) in step_form_data.requirements.into_iter().enumerate() {
 | 
			
		||||
            *id_counter += 1;
 | 
			
		||||
            let sig_req_db_id = *id_counter;
 | 
			
		||||
 | 
			
		||||
            let sig_req_instance = SignatureRequirement::new(
 | 
			
		||||
                sig_req_db_id, 
 | 
			
		||||
                flow_step_instance.base_data.id, // Use ID from the saved FlowStep instance
 | 
			
		||||
                &req_form_data.public_key, 
 | 
			
		||||
                &req_form_data.message, 
 | 
			
		||||
                SignatureRequirementStatus::Pending.to_db_string() // Use local enum's string representation
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            match db.collection::<SignatureRequirement>() {
 | 
			
		||||
                Ok(req_collection) => {
 | 
			
		||||
                    if let Err(e) = req_collection.set(&sig_req_instance) {
 | 
			
		||||
                        error!("Failed to save SignatureRequirement (flow: {}, step: {}, req_idx: {}): {:?}", flow_instance.name, step_idx, req_idx, e);
 | 
			
		||||
                        return HttpResponse::InternalServerError().body(format!("Failed to save signature requirement: {:?}", e));
 | 
			
		||||
                    }
 | 
			
		||||
                    info!(
 | 
			
		||||
                        "Saved SignatureRequirement {} for step {} of flow '{}', DB_ID: {}",
 | 
			
		||||
                        req_idx + 1, step_idx + 1, flow_instance.name, sig_req_instance.base_data.id
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    error!("Failed to get SignatureRequirement collection: {:?}. Aborting.", e);
 | 
			
		||||
                    return HttpResponse::InternalServerError().body(format!("Database error getting requirement collection: {:?}", e));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    info!("Finished processing all steps for flow '{}', UUID: {}", flow_instance.name, flow_instance.flow_uuid);
 | 
			
		||||
 | 
			
		||||
    HttpResponse::SeeOther()
 | 
			
		||||
        .append_header((actix_web::http::header::LOCATION, "/"))
 | 
			
		||||
        .finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Rhai-Callable Helper Functions ---
 | 
			
		||||
 | 
			
		||||
fn rhai_create_flow_entry(
 | 
			
		||||
    db_arc: Arc<OurDB>,
 | 
			
		||||
    id_counter_arc: Arc<Mutex<u32>>,
 | 
			
		||||
    name: String,
 | 
			
		||||
) -> Result<u32, Box<rhai::EvalAltResult>> {
 | 
			
		||||
    info!("Rhai: Attempting to create flow entry with name: {}", name);
 | 
			
		||||
 | 
			
		||||
    let mut id_counter = match id_counter_arc.lock() {
 | 
			
		||||
        Ok(guard) => guard,
 | 
			
		||||
        Err(poisoned) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    *id_counter += 1;
 | 
			
		||||
    let flow_db_id = *id_counter;
 | 
			
		||||
    let flow_uuid = Uuid::new_v4().to_string();
 | 
			
		||||
 | 
			
		||||
    let flow_instance = Flow::new(
 | 
			
		||||
        flow_db_id,
 | 
			
		||||
        &flow_uuid,
 | 
			
		||||
        &name,
 | 
			
		||||
        FlowStatus::Pending.to_db_string(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    match db_arc.collection::<Flow>() {
 | 
			
		||||
        Ok(flow_collection) => {
 | 
			
		||||
            if let Err(e) = flow_collection.set(&flow_instance) {
 | 
			
		||||
                let err_msg = format!("Rhai: Failed to save Flow (name: {}): {:?}", name, e);
 | 
			
		||||
                error!("{}", err_msg);
 | 
			
		||||
                return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
            }
 | 
			
		||||
            info!("Rhai: Saved Flow object for '{}', UUID: {}, DB_ID: {}", flow_instance.name, flow_instance.flow_uuid, flow_instance.base_data.id);
 | 
			
		||||
            Ok(flow_instance.base_data.id)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Failed to get Flow collection: {:?}", e);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn rhai_add_step_entry(
 | 
			
		||||
    db_arc: Arc<OurDB>,
 | 
			
		||||
    id_counter_arc: Arc<Mutex<u32>>,
 | 
			
		||||
    flow_db_id: u32, // ID of the parent flow
 | 
			
		||||
    description: String,
 | 
			
		||||
    order: u32,
 | 
			
		||||
) -> Result<u32, Box<rhai::EvalAltResult>> {
 | 
			
		||||
    info!(
 | 
			
		||||
        "Rhai: Adding step to flow ID {}, order {}, description: '{}'",
 | 
			
		||||
        flow_db_id, order, description
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut id_counter = match id_counter_arc.lock() {
 | 
			
		||||
        Ok(guard) => guard,
 | 
			
		||||
        Err(poisoned) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    *id_counter += 1;
 | 
			
		||||
    let flow_step_db_id = *id_counter;
 | 
			
		||||
 | 
			
		||||
    let mut flow_step_instance = FlowStep::new(
 | 
			
		||||
        flow_step_db_id,
 | 
			
		||||
        flow_db_id,
 | 
			
		||||
        order,
 | 
			
		||||
        FlowStepStatus::Pending.to_db_string(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if !description.is_empty() {
 | 
			
		||||
        flow_step_instance = flow_step_instance.description(description);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match db_arc.collection::<FlowStep>() {
 | 
			
		||||
        Ok(step_collection) => {
 | 
			
		||||
            if let Err(e) = step_collection.set(&flow_step_instance) {
 | 
			
		||||
                let err_msg = format!(
 | 
			
		||||
                    "Rhai: Failed to save FlowStep (flow_id: {}, order: {}): {:?}",
 | 
			
		||||
                    flow_db_id, order, e
 | 
			
		||||
                );
 | 
			
		||||
                error!("{}", err_msg);
 | 
			
		||||
                return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
            }
 | 
			
		||||
            info!(
 | 
			
		||||
                "Rhai: Saved FlowStep for flow_id {}, order {}, DB_ID: {}",
 | 
			
		||||
                flow_db_id, order, flow_step_instance.base_data.id
 | 
			
		||||
            );
 | 
			
		||||
            Ok(flow_step_instance.base_data.id)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Failed to get FlowStep collection: {:?}", e);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn rhai_add_requirement_entry(
 | 
			
		||||
    db_arc: Arc<OurDB>,
 | 
			
		||||
    id_counter_arc: Arc<Mutex<u32>>,
 | 
			
		||||
    step_db_id: u32, // ID of the parent step
 | 
			
		||||
    public_key: String,
 | 
			
		||||
    message: String,
 | 
			
		||||
) -> Result<u32, Box<rhai::EvalAltResult>> {
 | 
			
		||||
    info!(
 | 
			
		||||
        "Rhai: Adding requirement to step ID {}, pk: '{}', msg: '{}'",
 | 
			
		||||
        step_db_id, public_key, message
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut id_counter = match id_counter_arc.lock() {
 | 
			
		||||
        Ok(guard) => guard,
 | 
			
		||||
        Err(poisoned) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    *id_counter += 1;
 | 
			
		||||
    let sig_req_db_id = *id_counter;
 | 
			
		||||
 | 
			
		||||
    let sig_req_instance = SignatureRequirement::new(
 | 
			
		||||
        sig_req_db_id,
 | 
			
		||||
        step_db_id,
 | 
			
		||||
        &public_key,
 | 
			
		||||
        &message,
 | 
			
		||||
        SignatureRequirementStatus::Pending.to_db_string(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    match db_arc.collection::<SignatureRequirement>() {
 | 
			
		||||
        Ok(req_collection) => {
 | 
			
		||||
            if let Err(e) = req_collection.set(&sig_req_instance) {
 | 
			
		||||
                let err_msg = format!(
 | 
			
		||||
                    "Rhai: Failed to save SigRequirement (step_id: {}): {:?}",
 | 
			
		||||
                    step_db_id, e
 | 
			
		||||
                );
 | 
			
		||||
                error!("{}", err_msg);
 | 
			
		||||
                return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
            }
 | 
			
		||||
            info!(
 | 
			
		||||
                "Rhai: Saved SigRequirement for step_id {}, DB_ID: {}",
 | 
			
		||||
                step_db_id, sig_req_instance.base_data.id
 | 
			
		||||
            );
 | 
			
		||||
            Ok(sig_req_instance.base_data.id)
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            let err_msg = format!("Rhai: Failed to get SigRequirement collection: {:?}", e);
 | 
			
		||||
            error!("{}", err_msg);
 | 
			
		||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handle creation of a new flow from a Rhai script
 | 
			
		||||
async fn create_flow_from_script(
 | 
			
		||||
    data: web::Data<AppState>,
 | 
			
		||||
    form: web::Form<RhaiScriptFormData>,
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    info!("Received Rhai script for flow creation:\n{}", form.rhai_script);
 | 
			
		||||
 | 
			
		||||
    let mut engine = Engine::new();
 | 
			
		||||
 | 
			
		||||
    // Clone Arcs for capturing in closures
 | 
			
		||||
    let db_clone_for_flow = data.db.clone();
 | 
			
		||||
    let id_clone_for_flow = data.next_id_counter.clone();
 | 
			
		||||
    let db_clone_for_step = data.db.clone();
 | 
			
		||||
    let id_clone_for_step = data.next_id_counter.clone();
 | 
			
		||||
    let db_clone_for_req = data.db.clone();
 | 
			
		||||
    let id_clone_for_req = data.next_id_counter.clone();
 | 
			
		||||
 | 
			
		||||
    engine
 | 
			
		||||
        .register_fn("create_flow", move |name: String| {
 | 
			
		||||
            crate::rhai_create_flow_entry(db_clone_for_flow.clone(), id_clone_for_flow.clone(), name)
 | 
			
		||||
        })
 | 
			
		||||
        .register_fn("add_step", move |flow_id: u32, desc: String, order: i64| {
 | 
			
		||||
            if order < 0 || order > u32::MAX as i64 {
 | 
			
		||||
                return Err(Box::new(EvalAltResult::ErrorRuntime(format!("Order {} is out of range for u32", order).into(), Position::NONE)));
 | 
			
		||||
            }
 | 
			
		||||
            crate::rhai_add_step_entry(db_clone_for_step.clone(), id_clone_for_step.clone(), flow_id, desc, order as u32)
 | 
			
		||||
        })
 | 
			
		||||
        .register_fn("add_requirement", move |step_id: u32, pk: String, msg: String| {
 | 
			
		||||
            crate::rhai_add_requirement_entry(db_clone_for_req.clone(), id_clone_for_req.clone(), step_id, pk, msg)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    match engine.eval::<()>(&form.rhai_script) { // Expecting () as successful script execution doesn't need to return a value to Rust here.
 | 
			
		||||
        Ok(_) => {
 | 
			
		||||
            info!("Rhai script executed successfully.");
 | 
			
		||||
            HttpResponse::SeeOther()
 | 
			
		||||
                .append_header((actix_web::http::header::LOCATION, "/"))
 | 
			
		||||
                .finish()
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Rhai script execution failed: {}", e.to_string());
 | 
			
		||||
            HttpResponse::BadRequest().body(format!("Rhai script error: {}\n\nYour script was:\n{}", e.to_string(), form.rhai_script))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Placeholder for SigSocket WebSocket handler
 | 
			
		||||
async fn websocket_handler(
 | 
			
		||||
    req: actix_web::HttpRequest,
 | 
			
		||||
    stream: actix_web::web::Payload,
 | 
			
		||||
    service: web::Data<Arc<SigSocketService>>,
 | 
			
		||||
) -> ActixResult<HttpResponse> {
 | 
			
		||||
    info!("WebSocket connection attempt");
 | 
			
		||||
    let handler = service.create_websocket_handler();
 | 
			
		||||
    ws::start(handler, &req, stream)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// --- Extracted Helper Functions for App Setup and Configuration ---
 | 
			
		||||
 | 
			
		||||
/// Sets up the shared application data (AppState).
 | 
			
		||||
/// Allows overriding the database path for testing purposes.
 | 
			
		||||
pub async fn setup_app_data(db_path_override: Option<String>) -> Result<web::Data<AppState>, std::io::Error> {
 | 
			
		||||
    // Initialize templates
 | 
			
		||||
    let tera = match Tera::new("templates/**/*") {
 | 
			
		||||
        Ok(t) => t,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Critical: Tera template parsing error(s): {}", e);
 | 
			
		||||
            // Convert tera::Error to std::io::Error
 | 
			
		||||
            return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Tera init error: {}", e)));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Initialize SigSocket registry and service
 | 
			
		||||
    let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
 | 
			
		||||
    let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
 | 
			
		||||
 | 
			
		||||
    // Load environment variables from .env file
 | 
			
		||||
    dotenv().ok();
 | 
			
		||||
 | 
			
		||||
    // Initialize Database
 | 
			
		||||
    let database_path = db_path_override.unwrap_or_else(|| 
 | 
			
		||||
        env::var("DATABASE_PATH").unwrap_or_else(|_|
 | 
			
		||||
        {
 | 
			
		||||
            info!("DATABASE_PATH not set, defaulting to ./flowbroker_db");
 | 
			
		||||
            "./flowbroker_db".to_string()
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
    let db = match OurDB::new(&database_path, true) { // true for create_if_missing
 | 
			
		||||
        Ok(db_instance) => Arc::new(db_instance),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to initialize database at '{}': {}. Please ensure the path is writable.", database_path, e);
 | 
			
		||||
             // Convert heromodels::Error to std::io::Error (assuming Error impls std::error::Error)
 | 
			
		||||
            return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("DB init error: {}", e)));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    info!("Database initialized at: {}", database_path);
 | 
			
		||||
 | 
			
		||||
    // Initialize ID counter for temporary primary keys
 | 
			
		||||
    let next_id_counter = Arc::new(Mutex::new(0_u32)); 
 | 
			
		||||
    // TODO: Replace this with a robust primary key generation strategy from the database itself if possible.
 | 
			
		||||
 | 
			
		||||
    // Create shared application state
 | 
			
		||||
    Ok(web::Data::new(AppState {
 | 
			
		||||
        templates: tera,
 | 
			
		||||
        sigsocket_service: sigsocket_service.clone(), // Clone for AppState
 | 
			
		||||
        db,
 | 
			
		||||
        next_id_counter,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Configures the application routes.
 | 
			
		||||
pub fn configure_app_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
    // Note: AppState should be added via .app_data() before calling this configure function.
 | 
			
		||||
    // The websocket_handler specifically needs web::Data<Arc<SigSocketService>>.
 | 
			
		||||
    // The main HttpServer setup will add AppState (which includes an Arc<SigSocketService>)
 | 
			
		||||
    // and also the specific web::Data<Arc<SigSocketService>> for handlers like websocket_handler that expect it directly.
 | 
			
		||||
 | 
			
		||||
    cfg.route("/", web::get().to(list_flows))
 | 
			
		||||
       .service(
 | 
			
		||||
            web::scope("/flows") // Group flow-related routes under /flows
 | 
			
		||||
                // .route("", web::get().to(list_flows)) // If you want /flows to also list flows
 | 
			
		||||
                // .route("/new", web::get().to(new_flow_form)) // Deprecated, functionality merged into root list_flows
 | 
			
		||||
                .route("/create", web::post().to(create_flow))
 | 
			
		||||
                .route("/create_script", web::post().to(create_flow_from_script)) // Moved inside /flows scope
 | 
			
		||||
        )
 | 
			
		||||
       .service(web::resource("/ws/").route(web::get().to(websocket_handler)))
 | 
			
		||||
       .service(fs::Files::new("/static", "./static").show_files_listing()); // Static files
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Main Function ---
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
async fn main() -> std::io::Result<()> {
 | 
			
		||||
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
 | 
			
		||||
 | 
			
		||||
    let app_data = match setup_app_data(None).await {
 | 
			
		||||
        Ok(data) => data,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            error!("Failed to setup application data: {}", e);
 | 
			
		||||
            std::process::exit(1);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // The AppState (app_data) already contains an Arc<SigSocketService>.
 | 
			
		||||
    // Handlers like websocket_handler that take web::Data<Arc<SigSocketService>> directly
 | 
			
		||||
    // will be able to access it if AppState is correctly registered and the handler signature matches.
 | 
			
		||||
    // Alternatively, if a handler needs *only* the SigSocketService, it can be added separately.
 | 
			
		||||
    // For the websocket_handler as defined (taking web::Data<Arc<SigSocketService>>),
 | 
			
		||||
    // it needs this specific type registered with app_data.
 | 
			
		||||
    let sigsocket_service_for_ws_handler_data = web::Data::new(app_data.sigsocket_service.clone());
 | 
			
		||||
 | 
			
		||||
    info!("Flowbroker server starting on http://127.0.0.1:8081");
 | 
			
		||||
    info!("SigSocket WebSocket endpoint available at ws://127.0.0.1:8081/ws");
 | 
			
		||||
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        App::new()
 | 
			
		||||
            .app_data(app_data.clone()) // Main app state (includes SigSocketService)
 | 
			
		||||
            .app_data(sigsocket_service_for_ws_handler_data.clone()) // Specifically for handlers expecting web::Data<Arc<SigSocketService>>
 | 
			
		||||
            .configure(configure_app_routes)
 | 
			
		||||
    })
 | 
			
		||||
    .bind("127.0.0.1:8081")? // Using a different port for now
 | 
			
		||||
    .run()
 | 
			
		||||
    .await
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								flowbroker/start.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										34
									
								
								flowbroker/start.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
#!/bin/zsh
 | 
			
		||||
 | 
			
		||||
FORCE_KILL=false
 | 
			
		||||
 | 
			
		||||
# Parse command line options
 | 
			
		||||
while getopts ":f" opt; do
 | 
			
		||||
  case ${opt} in
 | 
			
		||||
    f )
 | 
			
		||||
      FORCE_KILL=true
 | 
			
		||||
      ;;
 | 
			
		||||
    \? )
 | 
			
		||||
      echo "Usage: cmd [-f]"
 | 
			
		||||
      exit 1
 | 
			
		||||
      ;;
 | 
			
		||||
  esac
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
if [ "$FORCE_KILL" = true ] ; then
 | 
			
		||||
    echo "Attempting to kill process on port 8081..."
 | 
			
		||||
    # Get PID of process using port 8081 and kill it
 | 
			
		||||
    # -t option for lsof outputs only the PID
 | 
			
		||||
    # xargs -r ensures kill is only run if lsof finds a PID
 | 
			
		||||
    lsof -t -i:8081 | xargs -r kill -9
 | 
			
		||||
    if [ $? -eq 0 ]; then
 | 
			
		||||
        echo "Process(es) on port 8081 killed."
 | 
			
		||||
    else
 | 
			
		||||
        echo "No process found on port 8081 or failed to kill."
 | 
			
		||||
    fi
 | 
			
		||||
    # Give a moment for the port to be released
 | 
			
		||||
    sleep 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
echo "Starting Flowbroker server..."
 | 
			
		||||
cargo run
 | 
			
		||||
							
								
								
									
										127
									
								
								flowbroker/static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								flowbroker/static/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
body {
 | 
			
		||||
    font-family: sans-serif;
 | 
			
		||||
    margin: 20px;
 | 
			
		||||
    line-height: 1.6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2 {
 | 
			
		||||
    color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
    color: #007bff;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form div {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input[type="text"], textarea {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
    background-color: #007bff;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 10px 15px;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button:hover {
 | 
			
		||||
    background-color: #0056b3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
hr {
 | 
			
		||||
    margin: 20px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#flows-list ul {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#flows-list li {
 | 
			
		||||
    border: 1px solid #eee;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Styles for dynamic form elements from create_flow.html */
 | 
			
		||||
.step, .requirement {
 | 
			
		||||
    border: 1px solid #ddd;
 | 
			
		||||
    padding: 15px; /* Increased padding */
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: #f9f9f9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.step h3, .step h4, .requirement h5 {
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    color: #555; /* Slightly softer color */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.step .requirementsContainer {
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    border-left: 3px solid #007bff; /* Thicker border */
 | 
			
		||||
    padding-left: 20px; /* Increased padding */
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.removeStepBtn, button.removeRequirementBtn {
 | 
			
		||||
    background-color: #dc3545;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 5px 10px; /* Adjusted padding */
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    margin-top: 10px; /* Increased margin */
 | 
			
		||||
    float: right; /* Align to the right */
 | 
			
		||||
}
 | 
			
		||||
button.removeStepBtn:hover, button.removeRequirementBtn:hover {
 | 
			
		||||
    background-color: #c82333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Clearfix for floated remove buttons */
 | 
			
		||||
.step::after, .requirement::after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    clear: both;
 | 
			
		||||
    display: table;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.addBtn { /* Style for Add Step / Add Requirement buttons */
 | 
			
		||||
    background-color: #28a745;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 8px 12px;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
.addBtn:hover {
 | 
			
		||||
    background-color: #218838;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* General styling for form elements within steps/requirements for consistency */
 | 
			
		||||
.step input[type="text"], .step textarea,
 | 
			
		||||
.requirement input[type="text"], .requirement textarea {
 | 
			
		||||
    margin-bottom: 8px; /* Add some space below inputs */
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										187
									
								
								flowbroker/templates/create_flow.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								flowbroker/templates/create_flow.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <title>Flowbroker - Create Flow</title>
 | 
			
		||||
    <link rel="stylesheet" href="/static/style.css">
 | 
			
		||||
    <style>
 | 
			
		||||
        .step, .requirement {
 | 
			
		||||
            border: 1px solid #ddd;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            background-color: #f9f9f9;
 | 
			
		||||
        }
 | 
			
		||||
        .step h3, .step h4, .requirement h5 {
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
        }
 | 
			
		||||
        .step .requirementsContainer {
 | 
			
		||||
            margin-left: 20px;
 | 
			
		||||
            border-left: 2px solid #007bff;
 | 
			
		||||
            padding-left: 15px;
 | 
			
		||||
        }
 | 
			
		||||
        button.removeStepBtn, button.removeRequirementBtn {
 | 
			
		||||
            background-color: #dc3545;
 | 
			
		||||
            margin-top: 5px;
 | 
			
		||||
        }
 | 
			
		||||
        button.removeStepBtn:hover, button.removeRequirementBtn:hover {
 | 
			
		||||
            background-color: #c82333;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <h1>Create New Flow</h1>
 | 
			
		||||
    <form id="createFlowForm" action="/flows" method="post">
 | 
			
		||||
        <div>
 | 
			
		||||
            <label for="flow_name">Flow Name:</label>
 | 
			
		||||
            <input type="text" id="flow_name" name="flow_name" required>
 | 
			
		||||
        </div>
 | 
			
		||||
        <hr>
 | 
			
		||||
 | 
			
		||||
        <div id="stepsContainer">
 | 
			
		||||
            <!-- Steps will be added here by JavaScript -->
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <button type="button" id="addStepBtn" class="addBtn">Add Step</button>
 | 
			
		||||
        <hr>
 | 
			
		||||
        <button type="submit">Create Flow</button>
 | 
			
		||||
    </form>
 | 
			
		||||
    <p><a href="/">Back to Flows List</a></p>
 | 
			
		||||
 | 
			
		||||
    <!-- Template for a new step -->
 | 
			
		||||
    <template id="stepTemplate">
 | 
			
		||||
        <div class="step" data-step-index="">
 | 
			
		||||
            <h3>Step <span class="step-number"></span></h3>
 | 
			
		||||
            <button type="button" class="removeStepBtn">Remove This Step</button>
 | 
			
		||||
            <div>
 | 
			
		||||
                <label>Step Description (Optional):</label>
 | 
			
		||||
                <input type="text" name="steps[X].description" class="step-description">
 | 
			
		||||
            </div>
 | 
			
		||||
            <h4>Signature Requirements for Step <span class="step-number"></span></h4>
 | 
			
		||||
            <div class="requirementsContainer" data-step-index="">
 | 
			
		||||
                <!-- Requirements will be added here -->
 | 
			
		||||
            </div>
 | 
			
		||||
            <button type="button" class="addRequirementBtn addBtn" data-step-index="">Add Signature Requirement</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <!-- Template for a new signature requirement -->
 | 
			
		||||
    <template id="requirementTemplate">
 | 
			
		||||
        <div class="requirement" data-req-index="">
 | 
			
		||||
            <h5>Requirement <span class="req-number"></span></h5>
 | 
			
		||||
            <button type="button" class="removeRequirementBtn">Remove Requirement</button>
 | 
			
		||||
            <div>
 | 
			
		||||
                <label>Message to Sign:</label>
 | 
			
		||||
                <textarea name="steps[X].requirements[Y].message" rows="2" required class="req-message"></textarea>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <label>Required Public Key:</label>
 | 
			
		||||
                <input type="text" name="steps[X].requirements[Y].public_key" required class="req-pubkey">
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const stepsContainer = document.getElementById('stepsContainer');
 | 
			
		||||
    const addStepBtn = document.getElementById('addStepBtn');
 | 
			
		||||
    const stepTemplate = document.getElementById('stepTemplate');
 | 
			
		||||
    const requirementTemplate = document.getElementById('requirementTemplate');
 | 
			
		||||
    const form = document.getElementById('createFlowForm');
 | 
			
		||||
 | 
			
		||||
    const updateIndices = () => {
 | 
			
		||||
        const steps = stepsContainer.querySelectorAll('.step');
 | 
			
		||||
        steps.forEach((step, stepIdx) => {
 | 
			
		||||
            // Update step-level attributes and text
 | 
			
		||||
            step.dataset.stepIndex = stepIdx;
 | 
			
		||||
            step.querySelector('.step-number').textContent = stepIdx + 1;
 | 
			
		||||
            step.querySelector('.step-description').name = `steps[${stepIdx}].description`;
 | 
			
		||||
            
 | 
			
		||||
            const addReqBtn = step.querySelector('.addRequirementBtn');
 | 
			
		||||
            if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
 | 
			
		||||
 | 
			
		||||
            const requirements = step.querySelectorAll('.requirementsContainer .requirement');
 | 
			
		||||
            requirements.forEach((req, reqIdx) => {
 | 
			
		||||
                // Update requirement-level attributes and text
 | 
			
		||||
                req.dataset.reqIndex = reqIdx;
 | 
			
		||||
                req.querySelector('.req-number').textContent = reqIdx + 1;
 | 
			
		||||
                req.querySelector('.req-message').name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
 | 
			
		||||
                req.querySelector('.req-pubkey').name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const addRequirement = (currentStepElement, stepIndex) => {
 | 
			
		||||
        const requirementsContainer = currentStepElement.querySelector('.requirementsContainer');
 | 
			
		||||
        const reqFragment = requirementTemplate.content.cloneNode(true);
 | 
			
		||||
        const newRequirement = reqFragment.querySelector('.requirement');
 | 
			
		||||
        
 | 
			
		||||
        requirementsContainer.appendChild(newRequirement);
 | 
			
		||||
        updateIndices(); // Update all indices after adding
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const addStep = () => {
 | 
			
		||||
        const stepFragment = stepTemplate.content.cloneNode(true);
 | 
			
		||||
        const newStep = stepFragment.querySelector('.step');
 | 
			
		||||
        stepsContainer.appendChild(newStep);
 | 
			
		||||
        
 | 
			
		||||
        // Add at least one requirement to the new step automatically
 | 
			
		||||
        const currentStepIndex = stepsContainer.querySelectorAll('.step').length - 1;
 | 
			
		||||
        addRequirement(newStep, currentStepIndex);
 | 
			
		||||
        
 | 
			
		||||
        updateIndices(); // Update all indices after adding
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Event delegation for remove buttons and add requirement button
 | 
			
		||||
    stepsContainer.addEventListener('click', (event) => {
 | 
			
		||||
        if (event.target.classList.contains('removeStepBtn')) {
 | 
			
		||||
            event.target.closest('.step').remove();
 | 
			
		||||
            if (stepsContainer.querySelectorAll('.step').length === 0) { // Ensure at least one step
 | 
			
		||||
                addStep();
 | 
			
		||||
            }
 | 
			
		||||
            updateIndices();
 | 
			
		||||
        } else if (event.target.classList.contains('addRequirementBtn')) {
 | 
			
		||||
            const stepElement = event.target.closest('.step');
 | 
			
		||||
            const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
 | 
			
		||||
            addRequirement(stepElement, stepIndex);
 | 
			
		||||
        } else if (event.target.classList.contains('removeRequirementBtn')) {
 | 
			
		||||
            const requirementElement = event.target.closest('.requirement');
 | 
			
		||||
            const stepElement = event.target.closest('.step');
 | 
			
		||||
            const requirementsContainer = stepElement.querySelector('.requirementsContainer');
 | 
			
		||||
            requirementElement.remove();
 | 
			
		||||
            // Ensure at least one requirement per step
 | 
			
		||||
            if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
 | 
			
		||||
                 const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
 | 
			
		||||
                 addRequirement(stepElement, stepIndex);
 | 
			
		||||
            }
 | 
			
		||||
            updateIndices();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    addStepBtn.addEventListener('click', addStep);
 | 
			
		||||
 | 
			
		||||
    // Add one step by default when the page loads
 | 
			
		||||
    if (stepsContainer.children.length === 0) {
 | 
			
		||||
        addStep();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Optional: Validate that there's at least one step and one requirement before submit
 | 
			
		||||
    form.addEventListener('submit', (event) => {
 | 
			
		||||
        if (stepsContainer.querySelectorAll('.step').length === 0) {
 | 
			
		||||
            alert('Please add at least one step to the flow.');
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const steps = stepsContainer.querySelectorAll('.step');
 | 
			
		||||
        for (let i = 0; i < steps.length; i++) {
 | 
			
		||||
            if (steps[i].querySelectorAll('.requirementsContainer .requirement').length === 0) {
 | 
			
		||||
                alert(`Step ${i + 1} must have at least one signature requirement.`);
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user