Compare commits

52 Commits

Author SHA1 Message Date
Mahmoud-Emad
d3a66d4fc8 feat: Add initial production deployment support
- Add .env.example file for environment variable setup
- Add .gitignore to manage sensitive files and directories
- Add Dockerfile.prod for production-ready Docker image
- Add PRODUCTION_CHECKLIST.md for pre/post deployment steps
- Add PRODUCTION_DEPLOYMENT.md for deployment instructions
- Add STRIPE_SETUP.md for Stripe payment configuration
- Add config/default.toml for default configuration settings
- Add config/local.toml.example for local configuration template
2025-06-25 18:32:20 +03:00
Mahmoud-Emad
464e253739 feat: Enhance contract management with new features
- Implement comprehensive contract listing with filtering by
  status and type, and search functionality.
- Add contract cloning, sharing, and cancellation features.
- Improve contract details view with enhanced UI and activity
  timeline.
- Implement signer management with add/update/delete and status
  updates, including signature data handling and rejection.
- Introduce contract creation and editing functionalities with
  markdown support.
- Add error handling for contract not found scenarios.
- Implement reminder system for pending signatures with rate
  limiting and status tracking.
- Add API endpoint for retrieving contract statistics.
- Improve logging with more descriptive messages.
- Refactor code for better structure and maintainability.
2025-06-12 13:53:33 +03:00
Mahmoud-Emad
7e95391a9c feat: Refactor governance models and views
- Moved governance models (`Vote`, `VoteType`, `VotingResults`) from
  `models/governance.rs` to `controllers/governance.rs` for better
  organization and to avoid circular dependencies.  This improves
  maintainability and reduces complexity.
- Updated governance views to use the new model locations.
- Added a limit to the number of recent activities displayed on the
  dashboard for performance optimization.
2025-06-03 15:31:50 +03:00
Mahmoud-Emad
9802d51acc feat: Migrate to hero-models for calendar event data
- Replaced custom `CalendarEvent` model with `heromodels`' `Event` model.
- Updated database interactions and controller logic to use the new model.
- Removed unnecessary `CalendarEvent` model and related code.
- Updated views to reflect changes in event data structure.
2025-06-03 15:12:53 +03:00
Mahmoud-Emad
2299b61e79 feat: Enhance calendar display of all-day events
- Improve display of all-day events by adding a message
  indicating when there are no all-day events scheduled.
- Add visual improvements to all-day event display using
  bootstrap classes.
- Clarify messaging when there are no events scheduled for a
  given day.
2025-05-29 14:17:48 +03:00
Mahmoud-Emad
b8928379de feat: Fix timezone issues in event creation
- Correctly handle timezones when creating events, ensuring that
  start and end times are accurately represented regardless of the
  user's timezone.
- Add 1-day compensation to event times to handle timezone shifts
  during conversion to UTC.
- Improve default time setting for date-specific events.
2025-05-29 14:07:03 +03:00
Mahmoud-Emad
45c4f4985e feat: Enhance calendar display and event management
- Improve event display: Show only the first two events for each day
  in the calendar, with a "+X more" link to show the rest.
- Add event details modal:  Allows viewing and deleting events.
- Enhance event creation modal: Improve user experience and add color
  selection for events.
- Improve year view: Show the number of events for each month.
- Improve day view: Display all day events separately.
- Improve styling and layout: Enhance the visual appeal and
  responsiveness of the calendar.
2025-05-28 16:59:24 +03:00
Mahmoud-Emad
58d1cde1ce feat: Migrate calendar functionality to a database
- Replaced Redis-based calendar with a database-backed solution
- Implemented database models for calendars and events
- Improved error handling and logging for database interactions
- Added new database functions for calendar management
- Updated calendar views to reflect the database changes
- Enhanced event creation and deletion processes
- Refined date/time handling for better consistency
2025-05-28 15:48:54 +03:00
Mahmoud-Emad
d815d9d365 feat: Add custom Tera filters for date/time formatting
- Add three new Tera filters: `format_hour`, `extract_hour`, and
  `format_time` for flexible date/time formatting in templates.
- Improve template flexibility and maintainability by allowing
  customizable date/time display.
- Enhance the user experience with more precise date/time rendering.
2025-05-28 10:43:02 +03:00
Mahmoud-Emad
2827cfebc9 refactor: Rename proposals module to governance
The `proposals` module has been renamed to `governance` to better
reflect its purpose and content.  This improves code clarity and
consistency.

- Renamed the `proposals` module to `governance` throughout the
  project to reflect the broader scope of governance features.
- Updated all related imports and function calls to use the new
  module name.
2025-05-28 09:29:19 +03:00
Mahmoud-Emad
7b15606da5 refactor: Remove unnecessary debug print statements
- Removed several `println!` statements from the `governance`
  controller and `proposals` database module to improve code
  cleanliness and reduce unnecessary console output.
- Updated the `all_activities.html` template to use the
  `created_at` field instead of `timestamp` for activity dates.
- Updated the `index.html` template to use the `created_at`
  field instead of `timestamp` for activity timestamps.
- Added `#[allow(unused_assignments)]` attribute to the
  `create_activity` function in `proposals.rs` to suppress a
  potentially unnecessary warning.
2025-05-28 09:24:56 +03:00
Mahmoud-Emad
11d7ae37b6 feat: Enhance governance module with activity tracking and DB refactor
- Refactor database interaction for proposals and activities.
- Add activity tracking for proposal creation and voting.
- Improve logging for better debugging and monitoring.
- Update governance views to display recent activities.
- Add strum and strum_macros crates for enum handling.
- Update Cargo.lock file with new dependencies.
2025-05-27 20:45:30 +03:00
Mahmoud-Emad
70ca9f1605 feat: Enhance governance dashboard with activity tracking
- Add governance activity tracker to record user actions.
- Display recent activities on the governance dashboard.
- Add a dedicated page to view all governance activities.
- Improve header information and styling across governance pages.
- Track proposal creation and voting activities.
2025-05-25 16:02:34 +03:00
Mahmoud-Emad
d12a082ca1 feat: Enhance Governance Controller and Proposal Handling
- Improve proposal search to include description field: This
  allows for more comprehensive search results.
- Fix redirect after voting: The redirect now correctly handles
  the success message.
- Handle potential invalid timestamps in ballots: The code now
  gracefully handles ballots with invalid timestamps, preventing
  crashes and using the current time as a fallback.
- Add local time formatting function:  This provides a way to
  display dates and times in the user's local timezone.
- Update database path: This simplifies the database setup.
- Improve proposal vote handling: Addresses issues with vote
  submission and timestamping.
- Add client-side pagination and filtering to proposal details:
  Improves user experience for viewing large vote lists.
2025-05-25 10:48:02 +03:00
Mahmoud-Emad
97e7a04827 feat: Add pagination and filtering improvements to proposal votes
- Added pagination to the proposal votes table to improve usability
  with large datasets.
- Implemented client-side filtering of votes by type and search
  terms, enhancing the user experience.
- Improved the responsiveness and efficiency of the vote filtering
  and pagination.
2025-05-22 17:13:52 +03:00
Mahmoud-Emad
3d8aca19cc feat: Improve user experience after voting on proposals
- Redirect users to the proposal detail page with a success
  message after a successful vote, improving feedback.
- Automatically remove the success message from the URL after a
  short time to avoid URL clutter and maintain a clean browsing
  experience.
- Add a success alert message on the proposal detail page to
  provide immediate visual confirmation of a successful vote.
- Improve the visual presentation of the votes list on the
  proposal detail page by adding top margin for better spacing.
2025-05-22 17:05:26 +03:00
Mahmoud-Emad
52fbc77e3e feat: Enhance proposal creation and display
- Improve proposal creation form with input validation and
  default date settings for a better user experience.
- Add context variables to the proposals template for
  consistent display across governance pages.
- Enhance proposal detail page with visual improvements,
  voting results display, and user voting functionality.
- Add styles for better visual presentation of proposal details
  and voting information.
2025-05-22 16:31:11 +03:00
Mahmoud-Emad
fad288f67d feat: Add total vote counts to governance views
- Add functionality to calculate total yes, no, and abstain votes
  across all proposals. This provides a summary of community
  voting patterns on the governance page.
- Improve the user experience by displaying total vote counts
  prominently on the "My Votes" page. This gives users a quick
  overview of the overall voting results.
- Enhance the "Create Proposal" page with informative guidelines
  and a helpful alert to guide users through the proposal creation
  process.  This improves clarity and ensures proposals are well-
  structured.
2025-05-22 16:08:12 +03:00
Mahmoud-Emad
4659697ae2 feat: Add filtering and searching to governance proposals page
- Added filtering of proposals by status (Draft, Active, Approved, Rejected, Cancelled).
- Added searching of proposals by title and description.
- Improved UI to persist filter and search values.
- Added a "No proposals found" message for better UX.
2025-05-22 15:47:11 +03:00
Mahmoud-Emad
67b80f237d feat: Enahnced the dashboard 2025-05-21 18:01:22 +03:00
Mahmoud-Emad
b606923102 feat: Finish the proposal dashboard 2025-05-21 15:56:47 +03:00
Mahmoud-Emad
8f1438dc01 feat: Remove mock proposals 2025-05-21 15:43:17 +03:00
Mahmoud-Emad
916f435dbc feat: Load voting 2025-05-21 15:04:45 +03:00
Mahmoud-Emad
5d9eaac1f8 feat: Implemented submit vote 2025-05-21 13:49:20 +03:00
Mahmoud-Emad
9c71c63ec5 feat: Working on the propsal page:
- Integerated the view proposal detail db call
- Use real data instead of mock data
2025-05-21 12:21:38 +03:00
Mahmoud-Emad
4a2f1c7282 feat: Implement Proposals page:
- Added the create new proposal functionality
- Added the list all proposals functionnality
2025-05-21 11:44:06 +03:00
Mahmoud Emad
60198dc2d4 fix: Remove warnings 2025-05-18 09:48:28 +03:00
Mahmoud Emad
e4e403e231 feat: Integerated the DB:
- Added an initialization with the db
- Implemented 'add_new_proposal' function to be used in the form
2025-05-18 09:07:59 +03:00
timurgordon
2fd74defab update governance ui 2025-05-16 14:07:20 +03:00
timurgordon
9468595395 Add company management module with registration and entity switching 2025-05-05 13:58:51 +03:00
timurgordon
2760f00a30 Vocabulary fixes 2025-05-05 11:32:09 +03:00
timurgordon
a7c0772d9b fix images 2025-05-05 11:12:15 +03:00
timurgordon
54762cb63f vocabulary change 2025-05-05 10:49:33 +03:00
timurgordon
bafb63e0b1 Merge branch 'development_timur' of https://git.ourworld.tf/herocode/hostbasket into development_timur 2025-05-01 12:15:14 +03:00
timurgordon
c05803ff58 add contract md folder support 2025-05-01 03:56:55 +03:00
Timur Gordon
6b7b2542ab move all defi functionality to page and separate controller 2025-05-01 02:55:41 +02:00
457f3c8268 fixes 2025-04-29 06:27:28 +04:00
Timur Gordon
19f8700b78 feat: Implement comprehensive DeFi platform in Digital Assets dashboard
Add a complete DeFi platform with the following features:
- Tabbed interface for different DeFi functionalities
- Lending & Borrowing system with APY calculations
- Liquidity Pools with LP token rewards
- Staking options for tokens and digital assets
- Token Swap interface with real-time exchange rates
- Collateralization system for loans and synthetic assets
- Interactive JavaScript functionality for real-time calculations

This enhancement provides users with a complete suite of DeFi tools
directly integrated into the Digital Assets dashboard.
2025-04-29 01:11:51 +02:00
Timur Gordon
c22d6c953e implement marketplace feature wip 2025-04-26 03:44:36 +02:00
Timur Gordon
9445dea629 styling and minor content fixes 2025-04-23 04:58:38 +02:00
Timur Gordon
b56f1cbc30 updates to mock content and contract view implementation 2025-04-23 03:52:11 +02:00
Timur Gordon
6060831f61 rwda ui implementation 2025-04-22 15:36:40 +02:00
34594b95fa ... 2025-04-22 10:39:29 +04:00
15b05cb599 ... 2025-04-22 08:50:31 +04:00
b6dd04a6aa ... 2025-04-22 07:14:37 +04:00
310a5d956f ... 2025-04-22 06:59:24 +04:00
af4f09a67b ... 2025-04-22 06:44:29 +04:00
093aff3851 ... 2025-04-22 06:08:08 +04:00
4a87392194 ... 2025-04-22 06:07:50 +04:00
Timur Gordon
951af7dec7 implement contracts 2025-04-22 03:06:58 +02:00
Timur Gordon
36d605829f Merge branch 'main' of https://git.ourworld.tf/herocode/hostbasket 2025-04-22 02:16:19 +02:00
Timur Gordon
6ed6737c7e implement governance and flow functionality 2025-04-22 02:15:49 +02:00
145 changed files with 37019 additions and 1016 deletions

View File

@@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true

View File

@@ -0,0 +1,21 @@
# 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 Normal file
View File

@@ -0,0 +1,53 @@
# 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

1418
actix_mvc_app/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,17 @@ 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"
actix-web = "4.5.1"
actix-files = "0.6.5"
tera = "1.19.1"
@@ -13,6 +23,8 @@ 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"
@@ -23,3 +35,26 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] }
lazy_static = "1.4.0"
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" }

View File

@@ -0,0 +1,69 @@
# 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"]

View File

@@ -0,0 +1,180 @@
# 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! 🎉

View File

@@ -0,0 +1,410 @@
# 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.

View File

@@ -1,6 +1,6 @@
# Actix MVC App
# Zanzibar Digital Freezone
A Rust web application built with Actix Web, Tera templates, and Bootstrap 5.3.5, following the MVC (Model-View-Controller) architectural pattern.
Convenience, Safety and Privacy
## Features
@@ -42,8 +42,8 @@ actix_mvc_app/
1. Clone the repository:
```
git clone https://github.com/yourusername/actix_mvc_app.git
cd actix_mvc_app
git clone https://github.com/yourusername/zanzibar-digital-freezone.git
cd zanzibar-digital-freezone
```
2. Build the project:

View File

@@ -0,0 +1,100 @@
# 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

View File

@@ -0,0 +1,17 @@
# 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

View File

@@ -0,0 +1,18 @@
# 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"

View File

@@ -0,0 +1,170 @@
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

View File

@@ -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,10 +9,13 @@ 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,
@@ -29,6 +32,17 @@ 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> {
@@ -37,7 +51,10 @@ 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("templates.dir", "./src/views")?
.set_default("stripe.publishable_key", "")?
.set_default("stripe.secret_key", "")?
.set_default("stripe.webhook_secret", None::<String>)?;
// Load from config file if it exists
if let Ok(config_path) = env::var("APP_CONFIG") {
@@ -50,7 +67,8 @@ 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()?;
@@ -61,4 +79,4 @@ impl AppConfig {
/// Returns the application configuration
pub fn get_config() -> AppConfig {
AppConfig::new().expect("Failed to load configuration")
}
}

View File

@@ -0,0 +1,3 @@
## 1. Purpose
The purpose of this Agreement is to establish the terms and conditions for tokenizing real estate assets on the Zanzibar blockchain network.

View File

@@ -0,0 +1,3 @@
## 2. Tokenization Process
Tokenizer shall create digital tokens representing ownership interests in the properties listed in Appendix A according to the specifications in Appendix B.

View File

@@ -0,0 +1,3 @@
## 3. Revenue Sharing
Revenue generated from the tokenized properties shall be distributed according to the formula set forth in Appendix C.

View File

@@ -0,0 +1,3 @@
## 4. Governance
Decisions regarding the management of tokenized properties shall be made according to the governance framework outlined in Appendix D.

View File

@@ -0,0 +1,3 @@
### Appendix A: Properties
List of properties to be tokenized.

View File

@@ -0,0 +1,3 @@
### Appendix B: Specifications
Technical specifications for tokenization.

View File

@@ -0,0 +1,3 @@
### Appendix C: Revenue Formula
Formula for revenue distribution.

View File

@@ -0,0 +1,3 @@
### Appendix D: Governance Framework
Governance framework for tokenized properties.

View File

@@ -0,0 +1,3 @@
# Digital Asset Tokenization Agreement
This Digital Asset Tokenization Agreement (the "Agreement") is entered into between Zanzibar Property Consortium ("Tokenizer") and the property owners listed in Appendix A ("Owners").

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cook
use actix_session::Session;
use tera::Tera;
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
use crate::utils::render_template;
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
@@ -24,6 +25,7 @@ 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> {
@@ -91,13 +93,7 @@ impl AuthController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "login");
let rendered = tmpl.render("auth/login.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "auth/login.html", &ctx)
}
/// Handles user login
@@ -146,13 +142,7 @@ impl AuthController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "register");
let rendered = tmpl.render("auth/register.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "auth/register.html", &ctx)
}
/// Handles user registration

View File

@@ -1,12 +1,17 @@
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 tera::Tera;
use serde_json::Value;
use tera::Tera;
use crate::models::{CalendarEvent, CalendarViewMode};
use crate::utils::RedisCalendarService;
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;
/// Controller for handling calendar-related routes
pub struct CalendarController;
@@ -14,9 +19,11 @@ 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
@@ -27,113 +34,176 @@ 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
// Add user to context if available and ensure user has a calendar
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 Redis
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
Ok(events) => events,
// 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()
}
Err(e) => {
log::error!("Failed to get events from Redis: {}", e);
log::error!("Failed to get events from database: {}", 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 {
@@ -142,27 +212,34 @@ 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 {
@@ -172,165 +249,250 @@ 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())
);
}
}
let rendered = tmpl.render("calendar/index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
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
// Add user to context if available and ensure user has a calendar
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);
}
}
}
}
let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
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: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
log::error!("Failed to parse start time '{}': {}", form.start_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
}
};
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: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
log::error!("Failed to parse end time '{}': {}", form.end_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
}
};
// Create the event
let event = CalendarEvent::new(
form.title.clone(),
form.description.clone(),
// 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),
start_time,
end_time,
Some(form.color.clone()),
None, // location
Some(&form.color),
form.all_day,
None, // User ID would come from session in a real app
);
// Save the event to Redis
match RedisCalendarService::save_event(&event) {
Ok(_) => {
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);
}
}
}
// Redirect to the calendar page
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar"))
.finish())
},
}
Err(e) => {
log::error!("Failed to save event to Redis: {}", e);
log::error!("Failed to save event to database: {}", 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) = Self::get_user_from_session(&_session) {
if let Some(user) = user_info {
ctx.insert("user", &user);
}
let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::InternalServerError().content_type("text/html").body(rendered))
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
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();
// Delete the event from Redis
match RedisCalendarService::delete_event(&id) {
// 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) {
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 Redis: {}", e);
log::error!("Failed to delete event from database: {}", 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 {
@@ -342,11 +504,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 {
@@ -365,7 +527,7 @@ impl CalendarController {
_ => "",
}
}
/// Returns the name of the day
fn day_name(day: u32) -> &'static str {
match day {
@@ -403,7 +565,7 @@ pub struct EventForm {
#[derive(Debug, Serialize)]
struct CalendarDay {
day: u32,
events: Vec<CalendarEvent>,
events: Vec<Event>,
is_current_month: bool,
}
@@ -412,5 +574,5 @@ struct CalendarDay {
struct CalendarMonth {
month: u32,
name: String,
events: Vec<CalendarEvent>,
}
events: Vec<Event>,
}

View File

@@ -0,0 +1,671 @@
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::HttpRequest;
use actix_web::{HttpResponse, Result, web};
use heromodels::models::biz::{BusinessType, CompanyStatus};
use serde::Deserialize;
use std::fs;
use tera::{Context, Tera};
// Form structs for company operations
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CompanyRegistrationForm {
pub company_name: String,
pub company_type: String,
pub shareholders: String,
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();
// 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 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 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 entity_name = &query_string[start..end];
let decoded_name =
urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
context.insert("entity_name", &decoded_name);
}
}
render_template(&tmpl, "company/index.html", &context)
}
// 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();
let mut context = Context::new();
// 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);
}
render_template(&tmpl, "company/view.html", &context)
} else {
render_company_not_found(&tmpl, Some(&company_id_str)).await
}
}
// 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());
}
};
// 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)
),
))
.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;
use futures_util::stream::StreamExt as _;
use std::collections::HashMap;
let mut fields: HashMap<String, String> = HashMap::new();
let mut uploaded_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));
}
}
} 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
})))
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,480 @@
use actix_web::HttpRequest;
use actix_web::{HttpResponse, Result, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use tera::{Context, Tera};
use uuid::Uuid;
use crate::models::asset::Asset;
use crate::models::defi::{
DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition,
ReceivingPosition,
};
use crate::utils::render_template;
// Form structs for DeFi operations
#[derive(Debug, Deserialize)]
pub struct ProvidingForm {
pub asset_id: String,
pub amount: f64,
pub duration: i32,
}
#[derive(Debug, Deserialize)]
pub struct ReceivingForm {
pub collateral_asset_id: String,
pub collateral_amount: f64,
pub amount: f64,
pub duration: i32,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LiquidityForm {
pub first_token: String,
pub first_amount: f64,
pub second_token: String,
pub second_amount: f64,
pub pool_fee: f64,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StakingForm {
pub asset_id: String,
pub amount: f64,
pub staking_period: i32,
}
#[derive(Debug, Deserialize)]
pub struct SwapForm {
pub from_token: String,
pub from_amount: f64,
pub to_token: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CollateralForm {
pub asset_id: String,
pub amount: f64,
pub purpose: String,
pub funds_amount: Option<f64>,
pub funds_term: Option<i32>,
}
pub struct DefiController;
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");
let providing_positions_json: Vec<serde_json::Value> = providing_positions
.iter()
.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
.iter()
.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> {
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 {
7 => 2.5,
30 => 4.2,
90 => 6.8,
180 => 8.5,
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));
// Create a new providing position
let providing_position = ProvidingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Providing,
status: DefiPositionStatus::Active,
asset_id: form.asset_id.clone(),
asset_name: asset.name.clone(),
asset_symbol: asset.asset_type.as_str().to_string(),
amount: form.amount,
value_usd: form.amount * asset.current_valuation.unwrap_or(0.0),
expected_return: profit_share,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::days(form.duration as i64)),
user_id: "user123".to_string(), // Hardcoded user ID for now
},
duration_days: form.duration,
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
);
Ok(HttpResponse::SeeOther()
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
} else {
// Asset not found, redirect with error
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/defi?error=Asset not found"))
.finish())
}
}
// Process receiving request
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 {
7 => 3.5,
30 => 5.0,
90 => 6.5,
180 => 8.0,
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 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_ratio = (collateral_value / form.amount) * 100.0;
// Create a new receiving position
let receiving_position = ReceivingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Receiving,
status: DefiPositionStatus::Active,
asset_id: "ZDFZ".to_string(), // Hardcoded for now, in a real app this would be a parameter
asset_name: "Zanzibar Token".to_string(),
asset_symbol: "ZDFZ".to_string(),
amount: form.amount,
value_usd: form.amount * 0.5, // Assuming 0.5 USD per ZDFZ
expected_return: profit_share_rate,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::days(form.duration as i64)),
user_id: "user123".to_string(), // Hardcoded user ID for now
},
collateral_asset_id: collateral_asset.id.clone(),
collateral_asset_name: collateral_asset.name.clone(),
collateral_asset_symbol: collateral_asset.asset_type.as_str().to_string(),
collateral_amount: form.collateral_amount,
collateral_value_usd: collateral_value,
duration_days: form.duration,
profit_share_rate,
profit_share_owed: profit_share,
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
);
Ok(HttpResponse::SeeOther()
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
} else {
// Asset not found, redirect with error
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/defi?error=Collateral asset not found"))
.finish())
}
}
// Process liquidity provision
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
);
Ok(HttpResponse::SeeOther()
.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> {
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)),
))
.finish())
}
// Process token swap
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
);
Ok(HttpResponse::SeeOther()
.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> {
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
);
Ok(HttpResponse::SeeOther()
.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
}
// 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()),
);
// 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),
);
} 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(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
}
// Generate mock assets for testing
fn get_mock_assets() -> Vec<Asset> {
// Reuse the asset controller's mock data function
crate::controllers::asset::AssetController::get_mock_assets()
}
}

View File

@@ -0,0 +1,382 @@
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())
}
}
}
}

View File

@@ -0,0 +1,125 @@
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
}

View File

@@ -0,0 +1,636 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use chrono::{Utc, Duration};
use serde::Deserialize;
use tera::Tera;
use crate::models::flow::{Flow, FlowStatus, FlowType, FlowStatistics, FlowStep, StepStatus, FlowLog};
use crate::controllers::auth::Claims;
use crate::utils::render_template;
pub struct FlowController;
impl FlowController {
/// Renders the flows dashboard
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let flows = Self::get_mock_flows();
let stats = FlowStatistics::new(&flows);
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &flows);
ctx.insert("stats", &stats);
ctx.insert("active_flows", &flows.iter().filter(|f| f.status == FlowStatus::InProgress).collect::<Vec<_>>());
ctx.insert("stuck_flows", &flows.iter().filter(|f| f.status == FlowStatus::Stuck).collect::<Vec<_>>());
render_template(&tmpl, "flows/index.html", &ctx)
}
/// Renders the flows list page
pub async fn list_flows(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let flows = Self::get_mock_flows();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &flows);
render_template(&tmpl, "flows/flows.html", &ctx)
}
/// Renders the flow detail page
pub async fn flow_detail(
path: web::Path<String>,
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
let flow_id = path.into_inner();
let user = Self::get_user_from_session(&session);
// Find the flow with the given ID
let flows = Self::get_mock_flows();
let flow = flows.iter().find(|f| f.id == flow_id);
if let Some(flow) = flow {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flow", flow);
render_template(&tmpl, "flows/flow_detail.html", &ctx)
} else {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("error", "Flow not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)),
Err(e) => {
log::error!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e)))
}
}
}
}
/// Renders the create flow page
pub async fn create_flow_form(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
render_template(&tmpl, "flows/create_flow.html", &ctx)
}
/// Handles the create flow form submission
pub async fn create_flow(
_form: web::Form<FlowForm>,
_session: Session
) -> impl Responder {
// In a real application, we would create a new flow here
// For now, just redirect to the flows list
HttpResponse::Found()
.append_header(("Location", "/flows"))
.finish()
}
/// Renders the my flows page
pub async fn my_flows(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
if let Some(user) = &user {
let flows = Self::get_mock_flows();
let my_flows = flows.iter()
.filter(|f| f.owner_name == user.sub)
.collect::<Vec<_>>();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &my_flows);
render_template(&tmpl, "flows/my_flows.html", &ctx)
} else {
Ok(HttpResponse::Found()
.append_header(("Location", "/login"))
.finish())
}
}
/// Handles the advance flow step action
pub async fn advance_flow_step(
path: web::Path<String>,
_session: Session
) -> impl Responder {
let flow_id = path.into_inner();
// In a real application, we would advance the flow step here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Handles the mark flow step as stuck action
pub async fn mark_flow_step_stuck(
path: web::Path<String>,
_form: web::Form<StuckForm>,
_session: Session
) -> impl Responder {
let flow_id = path.into_inner();
// In a real application, we would mark the flow step as stuck here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Handles the add log to flow step action
pub async fn add_log_to_flow_step(
path: web::Path<(String, String)>,
_form: web::Form<LogForm>,
_session: Session
) -> impl Responder {
let (flow_id, _step_id) = path.into_inner();
// In a real application, we would add a log to the flow step here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Gets the user from the session
fn get_user_from_session(session: &Session) -> Option<Claims> {
if let Ok(Some(user)) = session.get::<Claims>("user") {
Some(user)
} else {
None
}
}
/// Creates mock flow data for testing
fn get_mock_flows() -> Vec<Flow> {
let mut flows = Vec::new();
// Create a few mock flows
let mut flow1 = Flow {
id: "flow-1".to_string(),
name: "ZDFZ Business Entity Registration".to_string(),
description: "Register a new business entity within the Zanzibar Digital Freezone legal framework".to_string(),
flow_type: FlowType::CompanyRegistration,
status: FlowStatus::InProgress,
owner_id: "user-1".to_string(),
owner_name: "Ibrahim Faraji".to_string(),
steps: vec![
FlowStep {
id: "step-1-1".to_string(),
name: "Document Submission".to_string(),
description: "Submit required business registration documents including business plan, ownership structure, and KYC information".to_string(),
order: 1,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(5)),
completed_at: Some(Utc::now() - Duration::days(4)),
logs: vec![
FlowLog {
id: "log-1-1-1".to_string(),
message: "Initial document package submitted".to_string(),
timestamp: Utc::now() - Duration::days(5),
},
FlowLog {
id: "log-1-1-2".to_string(),
message: "Additional ownership verification documents requested".to_string(),
timestamp: Utc::now() - Duration::days(4) - Duration::hours(12),
},
FlowLog {
id: "log-1-1-3".to_string(),
message: "Additional documents submitted and verified".to_string(),
timestamp: Utc::now() - Duration::days(4),
},
],
},
FlowStep {
id: "step-1-2".to_string(),
name: "Regulatory Review".to_string(),
description: "ZDFZ Business Registry review of submitted documents and compliance with regulatory requirements".to_string(),
order: 2,
status: StepStatus::InProgress,
started_at: Some(Utc::now() - Duration::days(3)),
completed_at: None,
logs: vec![
FlowLog {
id: "log-1-2-1".to_string(),
message: "Regulatory review initiated by ZDFZ Business Registry".to_string(),
timestamp: Utc::now() - Duration::days(3),
},
FlowLog {
id: "log-1-2-2".to_string(),
message: "Preliminary compliance assessment completed".to_string(),
timestamp: Utc::now() - Duration::days(2),
},
FlowLog {
id: "log-1-2-3".to_string(),
message: "Awaiting final approval from regulatory committee".to_string(),
timestamp: Utc::now() - Duration::days(1),
},
],
},
FlowStep {
id: "step-1-3".to_string(),
name: "Digital Identity Creation".to_string(),
description: "Creation of the entity's digital identity and blockchain credentials within the ZDFZ ecosystem".to_string(),
order: 3,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
logs: vec![],
},
FlowStep {
id: "step-1-4".to_string(),
name: "License and Certificate Issuance".to_string(),
description: "Issuance of business licenses, certificates, and digital credentials".to_string(),
order: 4,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
logs: vec![],
},
],
created_at: Utc::now() - Duration::days(5),
updated_at: Utc::now() - Duration::days(1),
completed_at: None,
progress_percentage: 40,
current_step: None,
};
// Update the current step
flow1.current_step = flow1.steps.iter().find(|s| s.status == StepStatus::InProgress).cloned();
let mut flow2 = Flow {
id: "flow-2".to_string(),
name: "Digital Asset Tokenization Approval".to_string(),
description: "Process for approving the tokenization of a real estate asset within the ZDFZ regulatory framework".to_string(),
flow_type: FlowType::AssetTokenization,
status: FlowStatus::Completed,
owner_id: "user-2".to_string(),
owner_name: "Amina Salim".to_string(),
steps: vec![
FlowStep {
id: "step-2-1".to_string(),
name: "Asset Verification".to_string(),
description: "Verification of the underlying asset ownership and valuation".to_string(),
order: 1,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(30)),
completed_at: Some(Utc::now() - Duration::days(25)),
logs: vec![
FlowLog {
id: "log-2-1-1".to_string(),
message: "Asset documentation submitted for verification".to_string(),
timestamp: Utc::now() - Duration::days(30),
},
FlowLog {
id: "log-2-1-2".to_string(),
message: "Independent valuation completed by ZDFZ Property Registry".to_string(),
timestamp: Utc::now() - Duration::days(27),
},
FlowLog {
id: "log-2-1-3".to_string(),
message: "Asset ownership and valuation verified".to_string(),
timestamp: Utc::now() - Duration::days(25),
},
],
},
FlowStep {
id: "step-2-2".to_string(),
name: "Tokenization Structure Review".to_string(),
description: "Review of the proposed token structure, distribution model, and compliance with ZDFZ tokenization standards".to_string(),
order: 2,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(24)),
completed_at: Some(Utc::now() - Duration::days(20)),
logs: vec![
FlowLog {
id: "log-2-2-1".to_string(),
message: "Tokenization proposal submitted for review".to_string(),
timestamp: Utc::now() - Duration::days(24),
},
FlowLog {
id: "log-2-2-2".to_string(),
message: "Technical review completed by ZDFZ Digital Assets Committee".to_string(),
timestamp: Utc::now() - Duration::days(22),
},
FlowLog {
id: "log-2-2-3".to_string(),
message: "Tokenization structure approved with minor modifications".to_string(),
timestamp: Utc::now() - Duration::days(20),
},
],
},
FlowStep {
id: "step-2-3".to_string(),
name: "Smart Contract Deployment".to_string(),
description: "Deployment and verification of the asset tokenization smart contracts".to_string(),
order: 3,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(19)),
completed_at: Some(Utc::now() - Duration::days(15)),
logs: vec![
FlowLog {
id: "log-2-3-1".to_string(),
message: "Smart contract code submitted for audit".to_string(),
timestamp: Utc::now() - Duration::days(19),
},
FlowLog {
id: "log-2-3-2".to_string(),
message: "Security audit completed with no critical issues".to_string(),
timestamp: Utc::now() - Duration::days(17),
},
FlowLog {
id: "log-2-3-3".to_string(),
message: "Smart contracts deployed to ZDFZ-approved blockchain".to_string(),
timestamp: Utc::now() - Duration::days(15),
},
],
},
FlowStep {
id: "step-2-4".to_string(),
name: "Final Approval and Listing".to_string(),
description: "Final regulatory approval and listing on the ZDFZ Digital Asset Exchange".to_string(),
order: 4,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(14)),
completed_at: Some(Utc::now() - Duration::days(10)),
logs: vec![
FlowLog {
id: "log-2-4-1".to_string(),
message: "Final documentation package submitted for approval".to_string(),
timestamp: Utc::now() - Duration::days(14),
},
FlowLog {
id: "log-2-4-2".to_string(),
message: "Regulatory approval granted by ZDFZ Financial Authority".to_string(),
timestamp: Utc::now() - Duration::days(12),
},
FlowLog {
id: "log-2-4-3".to_string(),
message: "Asset tokens listed on ZDFZ Digital Asset Exchange".to_string(),
timestamp: Utc::now() - Duration::days(10),
},
],
},
],
created_at: Utc::now() - Duration::days(30),
updated_at: Utc::now() - Duration::days(10),
completed_at: Some(Utc::now() - Duration::days(10)),
progress_percentage: 100,
current_step: None,
};
flow2.current_step = flow2.steps.last().cloned();
let mut flow3 = Flow {
id: "flow-3".to_string(),
name: "Sustainable Tourism Certification".to_string(),
description: "Application process for ZDFZ Sustainable Tourism Certification for eco-tourism businesses".to_string(),
flow_type: FlowType::Certification,
status: FlowStatus::Stuck,
owner_id: "user-3".to_string(),
owner_name: "Hassan Mwinyi".to_string(),
steps: vec![
FlowStep {
id: "step-3-1".to_string(),
name: "Initial Application".to_string(),
description: "Submission of initial application and supporting documentation".to_string(),
order: 1,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(15)),
completed_at: Some(Utc::now() - Duration::days(12)),
logs: vec![
FlowLog {
id: "log-3-1-1".to_string(),
message: "Application submitted for Coral Reef Eco Tours".to_string(),
timestamp: Utc::now() - Duration::days(15),
},
FlowLog {
id: "log-3-1-2".to_string(),
message: "Application fee payment confirmed".to_string(),
timestamp: Utc::now() - Duration::days(14),
},
FlowLog {
id: "log-3-1-3".to_string(),
message: "Initial documentation review completed".to_string(),
timestamp: Utc::now() - Duration::days(12),
},
],
},
FlowStep {
id: "step-3-2".to_string(),
name: "Environmental Impact Assessment".to_string(),
description: "Assessment of the business's environmental impact and sustainability practices".to_string(),
order: 2,
status: StepStatus::Stuck,
started_at: Some(Utc::now() - Duration::days(11)),
completed_at: None,
logs: vec![
FlowLog {
id: "log-3-2-1".to_string(),
message: "Environmental assessment initiated".to_string(),
timestamp: Utc::now() - Duration::days(11),
},
FlowLog {
id: "log-3-2-2".to_string(),
message: "Site visit scheduled with environmental officer".to_string(),
timestamp: Utc::now() - Duration::days(9),
},
FlowLog {
id: "log-3-2-3".to_string(),
message: "STUCK: Missing required marine conservation plan documentation".to_string(),
timestamp: Utc::now() - Duration::days(7),
},
],
},
FlowStep {
id: "step-3-3".to_string(),
name: "Community Engagement Verification".to_string(),
description: "Verification of community engagement and benefit-sharing mechanisms".to_string(),
order: 3,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
logs: vec![],
},
FlowStep {
id: "step-3-4".to_string(),
name: "Certification Issuance".to_string(),
description: "Final review and issuance of ZDFZ Sustainable Tourism Certification".to_string(),
order: 4,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
logs: vec![],
},
],
created_at: Utc::now() - Duration::days(15),
updated_at: Utc::now() - Duration::days(7),
completed_at: None,
progress_percentage: 35,
current_step: None,
};
flow3.current_step = flow3.steps.iter().find(|s| s.status == StepStatus::Stuck).cloned();
let mut flow4 = Flow {
id: "flow-4".to_string(),
name: "Digital Payment Provider License".to_string(),
description: "Application for a license to operate as a digital payment provider within the ZDFZ financial system".to_string(),
flow_type: FlowType::LicenseApplication,
status: FlowStatus::InProgress,
owner_id: "user-4".to_string(),
owner_name: "Fatma Busaidy".to_string(),
steps: vec![
FlowStep {
id: "step-4-1".to_string(),
name: "Initial Application".to_string(),
description: "Submission of license application and company information".to_string(),
order: 1,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(20)),
completed_at: Some(Utc::now() - Duration::days(18)),
logs: vec![
FlowLog {
id: "log-4-1-1".to_string(),
message: "Application submitted for ZanziPay digital payment services".to_string(),
timestamp: Utc::now() - Duration::days(20),
},
FlowLog {
id: "log-4-1-2".to_string(),
message: "Application fee payment confirmed".to_string(),
timestamp: Utc::now() - Duration::days(19),
},
FlowLog {
id: "log-4-1-3".to_string(),
message: "Initial documentation review completed".to_string(),
timestamp: Utc::now() - Duration::days(18),
},
],
},
FlowStep {
id: "step-4-2".to_string(),
name: "Technical Infrastructure Review".to_string(),
description: "Review of the technical infrastructure, security measures, and compliance with ZDFZ financial standards".to_string(),
order: 2,
status: StepStatus::Completed,
started_at: Some(Utc::now() - Duration::days(17)),
completed_at: Some(Utc::now() - Duration::days(10)),
logs: vec![
FlowLog {
id: "log-4-2-1".to_string(),
message: "Technical documentation submitted for review".to_string(),
timestamp: Utc::now() - Duration::days(17),
},
FlowLog {
id: "log-4-2-2".to_string(),
message: "Security audit initiated by ZDFZ Financial Technology Office".to_string(),
timestamp: Utc::now() - Duration::days(15),
},
FlowLog {
id: "log-4-2-3".to_string(),
message: "Technical infrastructure approved with recommendations".to_string(),
timestamp: Utc::now() - Duration::days(10),
},
],
},
FlowStep {
id: "step-4-3".to_string(),
name: "AML/KYC Compliance Review".to_string(),
description: "Review of anti-money laundering and know-your-customer procedures".to_string(),
order: 3,
status: StepStatus::InProgress,
started_at: Some(Utc::now() - Duration::days(9)),
completed_at: None,
logs: vec![
FlowLog {
id: "log-4-3-1".to_string(),
message: "AML/KYC documentation submitted for review".to_string(),
timestamp: Utc::now() - Duration::days(9),
},
FlowLog {
id: "log-4-3-2".to_string(),
message: "Initial compliance assessment completed".to_string(),
timestamp: Utc::now() - Duration::days(5),
},
FlowLog {
id: "log-4-3-3".to_string(),
message: "Additional KYC procedure documentation requested".to_string(),
timestamp: Utc::now() - Duration::days(3),
},
],
},
FlowStep {
id: "step-4-4".to_string(),
name: "License Issuance".to_string(),
description: "Final review and issuance of Digital Payment Provider License".to_string(),
order: 4,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
logs: vec![],
},
],
created_at: Utc::now() - Duration::days(20),
updated_at: Utc::now() - Duration::days(3),
completed_at: None,
progress_percentage: 65,
current_step: None,
};
flow4.current_step = flow4.steps.iter().find(|s| s.status == StepStatus::InProgress).cloned();
flows.push(flow1);
flows.push(flow2);
flows.push(flow3);
flows.push(flow4);
flows
}
}
/// Form for creating a new flow
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct FlowForm {
/// Flow name
pub name: String,
/// Flow description
pub description: String,
/// Flow type
pub flow_type: String,
}
/// Form for marking a step as stuck
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StuckForm {
/// Reason for being stuck
pub reason: String,
}
/// Form for adding a log to a step
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LogForm {
/// Log message
pub message: String,
}

View File

@@ -0,0 +1,992 @@
use crate::db::governance::{
self, create_activity, get_all_activities, get_proposal_by_id, get_proposals,
get_recent_activities,
};
// Note: Now using heromodels directly instead of local governance models
use crate::utils::render_template;
use actix_session::Session;
use actix_web::{HttpResponse, Responder, Result, web};
use chrono::{Duration, Utc};
use heromodels::models::ActivityType;
use heromodels::models::governance::{Proposal, ProposalStatus};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tera::Tera;
use chrono::prelude::*;
/// Simple vote type for UI display
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum VoteType {
Yes,
No,
Abstain,
}
/// Simple vote structure for UI display
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
pub id: String,
pub proposal_id: String,
pub voter_id: i32,
pub voter_name: String,
pub vote_type: VoteType,
pub comment: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl 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::Uuid::new_v4().to_string(),
proposal_id,
voter_id,
voter_name,
vote_type,
comment,
created_at: now,
updated_at: now,
}
}
}
/// Simple voting results structure for UI display
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VotingResults {
pub proposal_id: String,
pub yes_count: usize,
pub no_count: usize,
pub abstain_count: usize,
pub total_votes: usize,
}
impl VotingResults {
pub fn new(proposal_id: String) -> Self {
Self {
proposal_id,
yes_count: 0,
no_count: 0,
abstain_count: 0,
total_votes: 0,
}
}
}
/// Controller for handling governance-related routes
pub struct GovernanceController;
#[allow(dead_code)]
impl GovernanceController {
/// Helper function to get user from session
/// For testing purposes, this will always return a mock user
fn get_user_from_session(session: &Session) -> Option<Value> {
// Try to get user from session first
let session_user = session
.get::<String>("user")
.ok()
.flatten()
.and_then(|user_json| serde_json::from_str(&user_json).ok());
// If user is not in session, return a mock user for testing
session_user.or_else(|| {
// Create a mock user
let mock_user = serde_json::json!({
"id": 1,
"username": "test_user",
"email": "test@example.com",
"name": "Test User",
"role": "member"
});
Some(mock_user)
})
}
/// Calculate statistics from the database
fn calculate_statistics_from_database(proposals: &[Proposal]) -> GovernanceStats {
let mut stats = GovernanceStats {
total_proposals: proposals.len(),
active_proposals: 0,
approved_proposals: 0,
rejected_proposals: 0,
draft_proposals: 0,
total_votes: 0,
participation_rate: 0.0,
};
// Count proposals by status
for proposal in proposals {
match proposal.status {
ProposalStatus::Active => stats.active_proposals += 1,
ProposalStatus::Approved => stats.approved_proposals += 1,
ProposalStatus::Rejected => stats.rejected_proposals += 1,
ProposalStatus::Draft => stats.draft_proposals += 1,
_ => {} // Handle other statuses if needed
}
// Count total votes
stats.total_votes += proposal.ballots.len();
}
// Calculate participation rate (if there are any proposals)
if stats.total_proposals > 0 {
// This is a simplified calculation - in a real application, you would
// calculate this based on the number of eligible voters
stats.participation_rate =
(stats.total_votes as f64 / stats.total_proposals as f64) * 100.0;
}
stats
}
/// Handles the governance dashboard page route
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "dashboard");
// Header data
ctx.insert("page_title", "Governance Dashboard");
ctx.insert(
"page_description",
"Participate in community decision-making",
);
ctx.insert("show_create_button", &false);
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get proposals from the database
let proposals = match crate::db::governance::get_proposals() {
Ok(props) => {
// println!(
// "📋 Proposals list page: Successfully loaded {} proposals from database",
// props.len()
// );
for (i, proposal) in props.iter().enumerate() {
println!(
" Proposal {}: ID={}, title={:?}, status={:?}",
i + 1,
proposal.base_data.id,
proposal.title,
proposal.status
);
}
props
}
Err(e) => {
println!("❌ Proposals list page: Failed to load proposals: {}", e);
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
// Make a copy of proposals for statistics
let proposals_for_stats = proposals.clone();
// Filter for active proposals only
let active_proposals: Vec<heromodels::models::Proposal> = proposals
.into_iter()
.filter(|p| p.status == heromodels::models::ProposalStatus::Active)
.collect();
// Sort active proposals by voting end date (ascending)
let mut sorted_active_proposals = active_proposals.clone();
sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date));
ctx.insert("proposals", &sorted_active_proposals);
// Get the nearest deadline proposal for the voting pane
if let Some(nearest_proposal) = sorted_active_proposals.first() {
// Calculate voting results for the nearest proposal
let results = Self::calculate_voting_results_from_proposal(nearest_proposal);
// Add both the proposal and its results to the context
ctx.insert("nearest_proposal", nearest_proposal);
ctx.insert("nearest_proposal_results", &results);
}
// Calculate statistics from the database
let stats = Self::calculate_statistics_from_database(&proposals_for_stats);
ctx.insert("stats", &stats);
// Get recent governance activities from our tracker (limit to 4 for dashboard)
let recent_activity = match Self::get_recent_governance_activities() {
Ok(activities) => activities.into_iter().take(4).collect::<Vec<_>>(),
Err(e) => {
eprintln!("Failed to load recent activities: {}", e);
Vec::new()
}
};
ctx.insert("recent_activity", &recent_activity);
render_template(&tmpl, "governance/index.html", &ctx)
}
/// Handles the proposal list page route
pub async fn proposals(
query: web::Query<ProposalQuery>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "proposals");
// Header data
ctx.insert("page_title", "All Proposals");
ctx.insert(
"page_description",
"Browse and filter all governance proposals",
);
ctx.insert("show_create_button", &false);
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Get proposals from the database
let mut proposals = match get_proposals() {
Ok(props) => props,
Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
// Filter proposals by status if provided
if let Some(status_filter) = &query.status {
if !status_filter.is_empty() {
proposals = proposals
.into_iter()
.filter(|p| {
let proposal_status = format!("{:?}", p.status);
proposal_status == *status_filter
})
.collect();
}
}
// Filter by search term if provided (title or description)
if let Some(search_term) = &query.search {
if !search_term.is_empty() {
let search_term = search_term.to_lowercase();
proposals = proposals
.into_iter()
.filter(|p| {
p.title.to_lowercase().contains(&search_term)
|| p.description.to_lowercase().contains(&search_term)
})
.collect();
}
}
// Add the filtered proposals to the context
ctx.insert("proposals", &proposals);
// Add the filter values back to the context for form persistence
ctx.insert("status_filter", &query.status);
ctx.insert("search_filter", &query.search);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the proposal detail page route
pub async fn proposal_detail(
path: web::Path<String>,
req: actix_web::HttpRequest,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
// Extract query parameters from the request
let query_str = req.query_string();
let vote_success = query_str.contains("vote_success=true");
let proposal_id = path.into_inner();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "proposals");
// Header data
ctx.insert("page_title", "Proposal Details");
ctx.insert(
"page_description",
"View proposal information and cast your vote",
);
ctx.insert("show_create_button", &false);
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Get mock proposal detail
let proposal = get_proposal_by_id(proposal_id.parse().unwrap());
if let Ok(Some(proposal)) = proposal {
ctx.insert("proposal", &proposal);
// Extract votes directly from the proposal
let votes = Self::extract_votes_from_proposal(&proposal);
ctx.insert("votes", &votes);
// Calculate voting results directly from the proposal
let results = Self::calculate_voting_results_from_proposal(&proposal);
ctx.insert("results", &results);
// Check if vote_success parameter is present and add success message
if vote_success {
ctx.insert("success", "Your vote has been successfully recorded!");
}
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
} else {
// Proposal not found
ctx.insert("error", "Proposal not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(content)),
Err(e) => {
eprintln!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!(
"Error: {}",
e
)))
}
}
}
}
/// Handles the create proposal page route
pub async fn create_proposal_form(
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "create");
// Header data
ctx.insert("page_title", "Create Proposal");
ctx.insert(
"page_description",
"Submit a new proposal for community voting",
);
ctx.insert("show_create_button", &false);
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
render_template(&tmpl, "governance/create_proposal.html", &ctx)
}
/// Handles the submission of a new proposal
pub async fn submit_proposal(
_form: web::Form<ProposalForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
let proposal_title = &_form.title;
let proposal_description = &_form.description;
// Use the DB-backed proposal creation
// Parse voting_start_date and voting_end_date from the form (YYYY-MM-DD expected)
let voting_start_date = _form.voting_start_date.as_ref().and_then(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
});
let voting_end_date = _form.voting_end_date.as_ref().and_then(|s| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(23, 59, 59))
.map(|naive| chrono::Utc.from_utc_datetime(&naive))
});
// Extract user id and name from serde_json::Value
let user_id = user
.get("id")
.and_then(|v| v.as_i64())
.unwrap_or(1)
.to_string();
let user_name = user
.get("username")
.and_then(|v| v.as_str())
.unwrap_or("Test User")
.to_string();
let is_draft = _form.draft.is_some();
let status = if is_draft {
ProposalStatus::Draft
} else {
ProposalStatus::Active
};
match governance::create_new_proposal(
&user_id,
&user_name,
proposal_title,
proposal_description,
status,
voting_start_date,
voting_end_date,
) {
Ok((proposal_id, saved_proposal)) => {
println!(
"Proposal saved to DB: ID={}, title={:?}",
proposal_id, saved_proposal.title
);
// Track the proposal creation activity
let _ = create_activity(
proposal_id,
&saved_proposal.title,
&user_name,
&ActivityType::ProposalCreated,
);
ctx.insert("success", "Proposal created successfully!");
}
Err(err) => {
println!("Failed to save proposal: {err}");
ctx.insert("error", &format!("Failed to save proposal: {err}"));
}
}
// For now, we'll just redirect to the proposals page with a success message
// Get proposals from the database
let proposals = match crate::db::governance::get_proposals() {
Ok(props) => {
println!(
"✅ Successfully loaded {} proposals from database",
props.len()
);
for (i, proposal) in props.iter().enumerate() {
println!(
" Proposal {}: ID={}, title={:?}, status={:?}",
i + 1,
proposal.base_data.id,
proposal.title,
proposal.status
);
}
props
}
Err(e) => {
println!("❌ Failed to load proposals: {}", e);
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
ctx.insert("proposals", &proposals);
// Add the required context variables for the proposals template
ctx.insert("active_tab", "proposals");
ctx.insert("status_filter", &None::<String>);
ctx.insert("search_filter", &None::<String>);
// Header data (required by _header.html template)
ctx.insert("page_title", "All Proposals");
ctx.insert(
"page_description",
"Browse and filter all governance proposals",
);
ctx.insert("show_create_button", &false);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the submission of a vote on a proposal
pub async fn submit_vote(
path: web::Path<String>,
form: web::Form<VoteForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Check if user is logged in
let user = match Self::get_user_from_session(&session) {
Some(user) => user,
None => {
return Ok(HttpResponse::Found()
.append_header(("Location", "/login"))
.finish());
}
};
ctx.insert("user", &user);
// Extract user ID
let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
// Parse proposal ID
let proposal_id_u32 = match proposal_id.parse::<u32>() {
Ok(id) => id,
Err(_) => {
ctx.insert("error", "Invalid proposal ID");
return render_template(&tmpl, "error.html", &ctx);
}
};
// Submit the vote
match crate::db::governance::submit_vote_on_proposal(
proposal_id_u32,
user_id,
&form.vote_type,
1, // Default to 1 share
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
) {
Ok(_) => {
// Record the vote activity
let user_name = user
.get("username")
.and_then(|v| v.as_str())
.unwrap_or("Unknown User");
// Track the vote cast activity
if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) {
let _ = create_activity(
proposal_id_u32,
&proposal.title,
user_name,
&ActivityType::VoteCast,
);
}
// Redirect to the proposal detail page with a success message
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/governance/proposals/{}?vote_success=true", proposal_id),
))
.finish());
}
Err(e) => {
ctx.insert("error", &format!("Failed to submit vote: {}", e));
render_template(&tmpl, "error.html", &ctx)
}
}
}
/// Handles the my votes page route
pub async fn my_votes(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "my_votes");
// Header data
ctx.insert("page_title", "My Votes");
ctx.insert(
"page_description",
"View your voting history and participation",
);
ctx.insert("show_create_button", &false);
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Extract user ID
let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
// Get all proposals from the database
let proposals = match crate::db::governance::get_proposals() {
Ok(props) => props,
Err(e) => {
ctx.insert("error", &format!("Failed to load proposals: {}", e));
vec![]
}
};
// Extract votes for this user from all proposals
let mut user_votes = Vec::new();
for proposal in &proposals {
// Extract votes from this proposal
let votes = Self::extract_votes_from_proposal(proposal);
// Filter votes for this user
for vote in votes {
if vote.voter_id == user_id {
user_votes.push((vote, proposal.clone()));
}
}
}
// Calculate total vote counts for all proposals
let total_vote_counts = Self::calculate_total_vote_counts(&proposals);
ctx.insert("total_yes_votes", &total_vote_counts.0);
ctx.insert("total_no_votes", &total_vote_counts.1);
ctx.insert("total_abstain_votes", &total_vote_counts.2);
ctx.insert("votes", &user_votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
}
/// Handles the all activities page route
pub async fn all_activities(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "activities");
// Header data
ctx.insert("page_title", "All Governance Activities");
ctx.insert(
"page_description",
"Complete history of governance actions and events",
);
ctx.insert("show_create_button", &false);
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get all governance activities from the database
let activities = match Self::get_all_governance_activities() {
Ok(activities) => activities,
Err(e) => {
eprintln!("Failed to load all activities: {}", e);
Vec::new()
}
};
ctx.insert("activities", &activities);
render_template(&tmpl, "governance/all_activities.html", &ctx)
}
/// Get recent governance activities from the database
fn get_recent_governance_activities() -> Result<Vec<Value>, String> {
// Get real activities from the database (no demo data)
let activities = get_recent_activities()?;
// Convert GovernanceActivity to the format expected by the template
let formatted_activities: Vec<Value> = activities
.into_iter()
.map(|activity| {
// Map activity type to appropriate icon
let (icon, action) = match activity.activity_type.as_str() {
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
_ => ("bi-circle-fill text-muted", "performed action"),
};
serde_json::json!({
"type": activity.activity_type,
"icon": icon,
"user": activity.creator_name,
"action": action,
"proposal_title": activity.proposal_title,
"created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"proposal_id": activity.proposal_id
})
})
.collect();
Ok(formatted_activities)
}
/// Get all governance activities from the database
fn get_all_governance_activities() -> Result<Vec<Value>, String> {
// Get all activities from the database
let activities = get_all_activities()?;
// Convert GovernanceActivity to the format expected by the template
let formatted_activities: Vec<Value> = activities
.into_iter()
.map(|activity| {
// Map activity type to appropriate icon
let (icon, action) = match activity.activity_type.as_str() {
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
_ => ("bi-circle-fill text-muted", "performed action"),
};
serde_json::json!({
"type": activity.activity_type,
"icon": icon,
"user": activity.creator_name,
"action": action,
"proposal_title": activity.proposal_title,
"created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
"proposal_id": activity.proposal_id
})
})
.collect();
Ok(formatted_activities)
}
/// Generate mock votes for a specific proposal
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
let now = Utc::now();
vec![
Vote {
id: "vote-001".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 1,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: Some("I strongly support this initiative.".to_string()),
created_at: now - Duration::days(2),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-002".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 2,
voter_name: "Jane Smith".to_string(),
vote_type: VoteType::Yes,
comment: None,
created_at: now - Duration::days(2),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-003".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 3,
voter_name: "Bob Johnson".to_string(),
vote_type: VoteType::No,
comment: Some("I have concerns about the implementation cost.".to_string()),
created_at: now - Duration::days(1),
updated_at: now - Duration::days(1),
},
Vote {
id: "vote-004".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 4,
voter_name: "Alice Williams".to_string(),
vote_type: VoteType::Abstain,
comment: Some("I need more information before making a decision.".to_string()),
created_at: now - Duration::hours(12),
updated_at: now - Duration::hours(12),
},
]
}
/// Calculate voting results from a proposal
fn calculate_voting_results_from_proposal(proposal: &Proposal) -> VotingResults {
let mut results = VotingResults::new(proposal.base_data.id.to_string());
// Count votes for each option
for option in &proposal.options {
match option.id {
1 => results.yes_count = option.count as usize,
2 => results.no_count = option.count as usize,
3 => results.abstain_count = option.count as usize,
_ => {} // Ignore other options
}
}
// Calculate total votes
results.total_votes = results.yes_count + results.no_count + results.abstain_count;
results
}
/// Extract votes from a proposal's ballots
fn extract_votes_from_proposal(proposal: &Proposal) -> Vec<Vote> {
let mut votes = Vec::new();
// Debug: Print proposal ID and number of ballots
println!(
"Extracting votes from proposal ID: {}",
proposal.base_data.id
);
println!("Number of ballots in proposal: {}", proposal.ballots.len());
// If there are no ballots, create some mock votes for testing
if proposal.ballots.is_empty() {
println!("No ballots found in proposal, creating mock votes for testing");
// Create mock votes based on the option counts
for option in &proposal.options {
if option.count > 0 {
let vote_type = match option.id {
1 => VoteType::Yes,
2 => VoteType::No,
3 => VoteType::Abstain,
_ => continue,
};
// Create a mock vote for each count
for i in 0..option.count {
let vote = Vote::new(
proposal.base_data.id.to_string(),
i as i32 + 1,
format!("User {}", i + 1),
vote_type.clone(),
option.comment.clone(),
);
votes.push(vote);
}
}
}
println!("Created {} mock votes", votes.len());
return votes;
}
// Convert each ballot to a Vote
for (i, ballot) in proposal.ballots.iter().enumerate() {
println!(
"Processing ballot {}: user_id={}, option_id={}, shares={}",
i, ballot.user_id, ballot.vote_option_id, ballot.shares_count
);
// Map option_id to VoteType
let vote_type = match ballot.vote_option_id {
1 => VoteType::Yes,
2 => VoteType::No,
3 => VoteType::Abstain,
_ => {
println!(
"Unknown option_id: {}, defaulting to Abstain",
ballot.vote_option_id
);
VoteType::Abstain // Default to Abstain for unknown options
}
};
// Convert user_id from u32 to i32 safely
let voter_id = match i32::try_from(ballot.user_id) {
Ok(id) => id,
Err(e) => {
println!("Failed to convert user_id {} to i32: {}", ballot.user_id, e);
continue; // Skip this ballot if conversion fails
}
};
let ballot_timestamp =
match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) {
Some(dt) => dt,
None => {
println!(
"Warning: Invalid timestamp {} for ballot, using current time",
ballot.base_data.created_at
);
Utc::now()
}
};
let vote = Vote {
id: uuid::Uuid::new_v4().to_string(),
proposal_id: proposal.base_data.id.to_string(),
voter_id,
voter_name: format!("User {}", voter_id),
vote_type,
comment: ballot.comment.clone(),
created_at: ballot_timestamp, // This is already local time
updated_at: ballot_timestamp, // Same as created_at for votes
};
votes.push(vote);
}
votes
}
// The calculate_statistics_from_database function is now defined at the top of the impl block
/// Calculate total vote counts across all proposals
/// Returns a tuple of (yes_count, no_count, abstain_count)
fn calculate_total_vote_counts(proposals: &[Proposal]) -> (usize, usize, usize) {
let mut yes_count = 0;
let mut no_count = 0;
let mut abstain_count = 0;
for proposal in proposals {
// Extract votes from this proposal
let votes = Self::extract_votes_from_proposal(proposal);
// Count votes by type
for vote in votes {
match vote.vote_type {
VoteType::Yes => yes_count += 1,
VoteType::No => no_count += 1,
VoteType::Abstain => abstain_count += 1,
}
}
}
(yes_count, no_count, abstain_count)
}
}
/// Represents the data submitted in the proposal form
#[derive(Debug, Deserialize)]
pub struct ProposalForm {
/// Title of the proposal
pub title: String,
/// Description of the proposal
pub description: String,
/// Status of the proposal
pub draft: Option<bool>,
/// Start date for voting
pub voting_start_date: Option<String>,
/// End date for voting
pub voting_end_date: Option<String>,
}
/// Represents the data submitted in the vote form
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct VoteForm {
/// Type of vote (yes, no, abstain)
pub vote_type: String,
/// Optional comment explaining the vote
pub comment: Option<String>,
}
/// Query parameters for filtering proposals
#[derive(Debug, Deserialize)]
pub struct ProposalQuery {
pub status: Option<String>,
pub search: Option<String>,
}
/// Represents statistics for the governance dashboard
#[derive(Debug, Serialize)]
pub struct GovernanceStats {
/// Total number of proposals
pub total_proposals: usize,
/// Number of active proposals
pub active_proposals: usize,
/// Number of approved proposals
pub approved_proposals: usize,
/// Number of rejected proposals
pub rejected_proposals: usize,
/// Number of draft proposals
pub draft_proposals: usize,
/// Total number of votes cast
pub total_votes: usize,
/// Participation rate (percentage)
pub participation_rate: f64,
}

View File

@@ -0,0 +1,418 @@
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)),
);
}

View File

@@ -1,8 +1,10 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_web::{web, Responder, Result};
use actix_session::Session;
use tera::Tera;
use serde_json::Value;
use crate::utils::render_template;
/// Controller for handling home-related routes
pub struct HomeController;
@@ -24,13 +26,7 @@ impl HomeController {
ctx.insert("user", &user);
}
let rendered = tmpl.render("editor.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "editor.html", &ctx)
}
/// Handles the home page route
@@ -43,13 +39,7 @@ impl HomeController {
ctx.insert("user", &user);
}
let rendered = tmpl.render("index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "index.html", &ctx)
}
/// Handles the about page route
@@ -62,13 +52,7 @@ impl HomeController {
ctx.insert("user", &user);
}
let rendered = tmpl.render("about.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "about.html", &ctx)
}
/// Handles the contact page route
@@ -81,13 +65,7 @@ impl HomeController {
ctx.insert("user", &user);
}
let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "contact.html", &ctx)
}
/// Handles form submissions from the contact page
@@ -112,18 +90,13 @@ impl HomeController {
ctx.insert("user", &user);
}
let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "contact.html", &ctx)
}
}
/// Represents the data submitted in the contact form
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub struct ContactForm {
pub name: String,
pub email: String,

View File

@@ -0,0 +1,611 @@
use actix_web::{HttpResponse, Result, http, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use tera::{Context, Tera};
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)]
pub struct ListingForm {
pub title: String,
pub description: String,
pub asset_id: String,
pub price: f64,
pub currency: String,
pub listing_type: String,
pub duration_days: Option<u32>,
pub tags: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct BidForm {
pub amount: f64,
pub currency: String,
}
#[derive(Debug, Deserialize)]
pub struct PurchaseForm {
pub agree_to_terms: bool,
}
pub struct MarketplaceController;
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()
.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()
.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()
.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);
let b_sold = b.sold_at.unwrap_or(b.created_at);
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()
.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(),
],
);
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();
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> {
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
})
.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
{
if let Some(bid) = listing.highest_bid() {
(Some(bid.amount), bid.amount + 1.0)
} else {
(None, listing.price + 1.0)
}
} 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()
.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(),
],
);
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(',')
.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));
// Parse listing type
let listing_type = match form.listing_type.as_str() {
"Fixed Price" => ListingType::FixedPrice,
"Auction" => ListingType::Auction,
"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,
form.description,
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_id.to_string(),
user_name.to_string(),
form.price,
form.currency,
listing_type,
expires_at,
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"))
.finish())
} else {
// Asset not found
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>,
path: web::Path<String>,
_form: web::Form<BidForm>,
) -> Result<HttpResponse> {
let listing_id = path.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),
))
.finish())
}
// Purchase a fixed-price listing
pub async fn purchase_listing(
_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),
))
.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>,
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),
AssetType::RealEstate => 50000.0 + (i as f64 * 10000.0),
AssetType::IntellectualProperty => 2000.0 + (i as f64 * 500.0),
AssetType::Commodity => 1000.0 + (i as f64 * 200.0),
AssetType::Share => 300.0 + (i as f64 * 50.0),
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
),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
Some(Utc::now() + Duration::days(30)),
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),
AssetType::RealEstate => 40000.0 + (i as f64 * 5000.0),
AssetType::IntellectualProperty => 1500.0 + (i as f64 * 300.0),
AssetType::Commodity => 800.0 + (i as f64 * 100.0),
AssetType::Share => 250.0 + (i as f64 * 40.0),
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),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
starting_price,
"USD".to_string(),
ListingType::Auction,
Some(Utc::now() + Duration::days(7)),
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
let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
let _ = listing.add_bid(
user_ids[bidder_index].to_string(),
user_names[bidder_index].to_string(),
bid_amount,
"USD".to_string(),
);
}
}
// 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),
AssetType::RealEstate => 60000.0 + (i as f64 * 15000.0),
AssetType::IntellectualProperty => 2500.0 + (i as f64 * 600.0),
AssetType::Commodity => 1200.0 + (i as f64 * 300.0),
AssetType::Share => 350.0 + (i as f64 * 70.0),
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
),
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
"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),
AssetType::RealEstate => 55000.0 + (i as f64 * 12000.0),
AssetType::IntellectualProperty => 2200.0 + (i as f64 * 550.0),
AssetType::Commodity => 1100.0 + (i as f64 * 220.0),
AssetType::Share => 320.0 + (i as f64 * 60.0),
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 mut listing = Listing::new(
format!("{} - SOLD", asset.name),
format!("This {} was sold recently.", asset.name),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[seller_index].to_string(),
user_names[seller_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
None,
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),
AssetType::RealEstate => 45000.0 + (i as f64 * 8000.0),
AssetType::IntellectualProperty => 1800.0 + (i as f64 * 400.0),
AssetType::Commodity => 900.0 + (i as f64 * 180.0),
AssetType::Share => 280.0 + (i as f64 * 45.0),
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),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
None,
vec!["cancelled".to_string()],
asset.image_url.clone(),
);
// Cancel the listing
let _ = listing.cancel();
listings.push(listing);
}
listings
}
}

View File

@@ -1,5 +1,18 @@
// Export controllers
pub mod home;
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 ticket;
pub mod calendar;
// Re-export controllers for easier imports

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ use tera::Tera;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority};
use crate::utils::render_template;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
@@ -131,13 +132,7 @@ impl TicketController {
]);
// Render the template
let rendered = tmpl.render("tickets/list.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "tickets/list.html", &ctx)
}
/// Shows the form for creating a new ticket
@@ -172,13 +167,7 @@ impl TicketController {
]);
// Render the template
let rendered = tmpl.render("tickets/new.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "tickets/new.html", &ctx)
}
/// Creates a new ticket
@@ -285,13 +274,7 @@ impl TicketController {
]);
// Render the template
let rendered = tmpl.render("tickets/show.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "tickets/show.html", &ctx)
}
/// Adds a comment to a ticket
@@ -443,12 +426,6 @@ impl TicketController {
ctx.insert("my_tickets", &true);
// Render the template
let rendered = tmpl.render("tickets/list.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
render_template(&tmpl, "tickets/list.html", &ctx)
}
}

View File

@@ -0,0 +1,362 @@
#![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)
}

View File

@@ -0,0 +1,500 @@
#![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(())
}

View File

@@ -0,0 +1,460 @@
#![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,
}

View File

@@ -0,0 +1,17 @@
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)
}

View File

@@ -0,0 +1,199 @@
#![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)
}

View File

@@ -0,0 +1,257 @@
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)
}

View File

@@ -0,0 +1,8 @@
pub mod calendar;
pub mod company;
pub mod contracts;
pub mod db;
pub mod document;
pub mod governance;
pub mod payment;
pub mod registration;

View File

@@ -0,0 +1,355 @@
#![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))
}
}
}

View File

@@ -0,0 +1,272 @@
#![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");
}
}

37
actix_mvc_app/src/lib.rs Normal file
View File

@@ -0,0 +1,37 @@
// 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};

View File

@@ -1,20 +1,23 @@
use actix_files as fs;
use actix_web::{App, HttpServer, web};
use actix_web::middleware::Logger;
use tera::Tera;
use std::io;
use std::env;
use actix_web::{App, HttpServer, web};
use lazy_static::lazy_static;
use std::env;
use std::io;
use tera::Tera;
mod config;
mod controllers;
mod db;
mod middleware;
mod models;
mod routes;
mod utils;
mod validators;
// Import middleware components
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
use middleware::{JwtAuth, RequestTimer, SecurityHeaders};
use models::initialize_mock_data;
use utils::redis_service;
// Initialize lazy_static for in-memory storage
@@ -28,13 +31,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])
};
}
@@ -44,14 +47,22 @@ 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 command line arguments
// Check for port override from environment variable or 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>() {
@@ -60,20 +71,28 @@ 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
@@ -84,10 +103,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())
@@ -101,6 +120,8 @@ 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())

View File

@@ -0,0 +1,283 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Asset types representing different categories of digital assets
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AssetType {
Artwork,
Token,
RealEstate,
Commodity,
Share,
Bond,
IntellectualProperty,
Other,
}
impl AssetType {
pub fn as_str(&self) -> &str {
match self {
AssetType::Artwork => "Artwork",
AssetType::Token => "Token",
AssetType::RealEstate => "Real Estate",
AssetType::Commodity => "Commodity",
AssetType::Share => "Share",
AssetType::Bond => "Bond",
AssetType::IntellectualProperty => "Intellectual Property",
AssetType::Other => "Other",
}
}
}
/// Status of an asset
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AssetStatus {
Active,
Locked,
ForSale,
Transferred,
Archived,
}
impl AssetStatus {
pub fn as_str(&self) -> &str {
match self {
AssetStatus::Active => "Active",
AssetStatus::Locked => "Locked",
AssetStatus::ForSale => "For Sale",
AssetStatus::Transferred => "Transferred",
AssetStatus::Archived => "Archived",
}
}
}
/// Blockchain information for an asset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockchainInfo {
pub blockchain: String,
pub token_id: String,
pub contract_address: String,
pub owner_address: String,
pub transaction_hash: Option<String>,
pub block_number: Option<u64>,
pub timestamp: Option<DateTime<Utc>>,
}
/// Valuation history point for an asset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationPoint {
pub id: String,
pub date: DateTime<Utc>,
pub value: f64,
pub currency: String,
pub source: String,
pub notes: Option<String>,
}
/// Transaction history for an asset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetTransaction {
pub id: String,
pub transaction_type: String,
pub date: DateTime<Utc>,
pub from_address: Option<String>,
pub to_address: Option<String>,
pub amount: Option<f64>,
pub currency: Option<String>,
pub transaction_hash: Option<String>,
pub notes: Option<String>,
}
/// Main Asset model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
pub id: String,
pub name: String,
pub description: String,
pub asset_type: AssetType,
pub status: AssetStatus,
pub owner_id: String,
pub owner_name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub blockchain_info: Option<BlockchainInfo>,
pub current_valuation: Option<f64>,
pub valuation_currency: Option<String>,
pub valuation_date: Option<DateTime<Utc>>,
pub valuation_history: Vec<ValuationPoint>,
pub transaction_history: Vec<AssetTransaction>,
pub metadata: serde_json::Value,
pub image_url: Option<String>,
pub external_url: Option<String>,
}
#[allow(dead_code)]
impl Asset {
/// Creates a new asset
pub fn new(
name: &str,
description: &str,
asset_type: AssetType,
owner_id: &str,
owner_name: &str,
) -> Self {
let now = Utc::now();
Self {
id: format!("asset-{}", Uuid::new_v4().to_string()[..8].to_string()),
name: name.to_string(),
description: description.to_string(),
asset_type,
status: AssetStatus::Active,
owner_id: owner_id.to_string(),
owner_name: owner_name.to_string(),
created_at: now,
updated_at: now,
blockchain_info: None,
current_valuation: None,
valuation_currency: None,
valuation_date: None,
valuation_history: Vec::new(),
transaction_history: Vec::new(),
metadata: serde_json::json!({}),
image_url: None,
external_url: None,
}
}
/// Adds blockchain information to the asset
pub fn add_blockchain_info(&mut self, blockchain_info: BlockchainInfo) {
self.blockchain_info = Some(blockchain_info);
self.updated_at = Utc::now();
}
/// Adds a valuation point to the asset's history
pub fn add_valuation(&mut self, value: f64, currency: &str, source: &str, notes: Option<String>) {
let valuation = ValuationPoint {
id: format!("val-{}", Uuid::new_v4().to_string()[..8].to_string()),
date: Utc::now(),
value,
currency: currency.to_string(),
source: source.to_string(),
notes,
};
self.current_valuation = Some(value);
self.valuation_currency = Some(currency.to_string());
self.valuation_date = Some(valuation.date);
self.valuation_history.push(valuation);
self.updated_at = Utc::now();
}
/// Adds a transaction to the asset's history
pub fn add_transaction(
&mut self,
transaction_type: &str,
from_address: Option<String>,
to_address: Option<String>,
amount: Option<f64>,
currency: Option<String>,
transaction_hash: Option<String>,
notes: Option<String>,
) {
let transaction = AssetTransaction {
id: format!("tx-{}", Uuid::new_v4().to_string()[..8].to_string()),
transaction_type: transaction_type.to_string(),
date: Utc::now(),
from_address,
to_address,
amount,
currency,
transaction_hash,
notes,
};
self.transaction_history.push(transaction);
self.updated_at = Utc::now();
}
/// Updates the status of the asset
pub fn update_status(&mut self, status: AssetStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Gets the latest valuation point
pub fn latest_valuation(&self) -> Option<&ValuationPoint> {
self.valuation_history.last()
}
/// Gets the latest transaction
pub fn latest_transaction(&self) -> Option<&AssetTransaction> {
self.transaction_history.last()
}
/// Gets the valuation history sorted by date
pub fn sorted_valuation_history(&self) -> Vec<&ValuationPoint> {
let mut history = self.valuation_history.iter().collect::<Vec<_>>();
history.sort_by(|a, b| a.date.cmp(&b.date));
history
}
/// Gets the transaction history sorted by date
pub fn sorted_transaction_history(&self) -> Vec<&AssetTransaction> {
let mut history = self.transaction_history.iter().collect::<Vec<_>>();
history.sort_by(|a, b| a.date.cmp(&b.date));
history
}
}
/// Filter for assets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetFilter {
pub asset_type: Option<AssetType>,
pub status: Option<AssetStatus>,
pub owner_id: Option<String>,
pub min_valuation: Option<f64>,
pub max_valuation: Option<f64>,
pub valuation_currency: Option<String>,
pub search_query: Option<String>,
}
/// Statistics for assets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetStatistics {
pub total_assets: usize,
pub total_value: f64,
pub value_by_type: std::collections::HashMap<String, f64>,
pub assets_by_type: std::collections::HashMap<String, usize>,
pub assets_by_status: std::collections::HashMap<String, usize>,
}
impl AssetStatistics {
pub fn new(assets: &[Asset]) -> Self {
let mut total_value = 0.0;
let mut value_by_type = std::collections::HashMap::new();
let mut assets_by_type = std::collections::HashMap::new();
let mut assets_by_status = std::collections::HashMap::new();
for asset in assets {
if let Some(valuation) = asset.current_valuation {
total_value += valuation;
let asset_type = asset.asset_type.as_str().to_string();
*value_by_type.entry(asset_type.clone()).or_insert(0.0) += valuation;
*assets_by_type.entry(asset_type).or_insert(0) += 1;
} else {
let asset_type = asset.asset_type.as_str().to_string();
*assets_by_type.entry(asset_type).or_insert(0) += 1;
}
let status = asset.status.as_str().to_string();
*assets_by_status.entry(status).or_insert(0) += 1;
}
Self {
total_assets: assets.len(),
total_value,
value_by_type,
assets_by_type,
assets_by_status,
}
}
}

View File

@@ -1,61 +1,4 @@
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)
}
}
// No imports needed for this module currently
/// Represents a view mode for the calendar
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -91,4 +34,4 @@ impl CalendarViewMode {
Self::Day => "day",
}
}
}
}

View File

@@ -0,0 +1,446 @@
#![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 {
Draft,
PendingSignatures,
Signed,
Active,
Expired,
Cancelled,
}
impl ContractStatus {
pub fn as_str(&self) -> &str {
match self {
ContractStatus::Draft => "Draft",
ContractStatus::PendingSignatures => "Pending Signatures",
ContractStatus::Signed => "Signed",
ContractStatus::Active => "Active",
ContractStatus::Expired => "Expired",
ContractStatus::Cancelled => "Cancelled",
}
}
}
/// Contract type enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractType {
Service,
Employment,
NDA,
SLA,
Partnership,
Distribution,
License,
Membership,
Other,
}
impl ContractType {
pub fn as_str(&self) -> &str {
match self {
ContractType::Service => "Service Agreement",
ContractType::Employment => "Employment Contract",
ContractType::NDA => "Non-Disclosure Agreement",
ContractType::SLA => "Service Level Agreement",
ContractType::Partnership => "Partnership Agreement",
ContractType::Distribution => "Distribution Agreement",
ContractType::License => "License Agreement",
ContractType::Membership => "Membership Agreement",
ContractType::Other => "Other",
}
}
}
/// Contract signer status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SignerStatus {
Pending,
Signed,
Rejected,
}
impl SignerStatus {
pub fn as_str(&self) -> &str {
match self {
SignerStatus::Pending => "Pending",
SignerStatus::Signed => "Signed",
SignerStatus::Rejected => "Rejected",
}
}
}
/// Contract signer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractSigner {
pub id: String,
pub name: String,
pub email: String,
pub status: SignerStatus,
pub signed_at: Option<DateTime<Utc>>,
pub comments: Option<String>,
}
#[allow(dead_code)]
impl ContractSigner {
/// Creates a new contract signer
pub fn new(name: String, email: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
email,
status: SignerStatus::Pending,
signed_at: None,
comments: None,
}
}
/// Signs the contract
pub fn sign(&mut self, comments: Option<String>) {
self.status = SignerStatus::Signed;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
/// Rejects the contract
pub fn reject(&mut self, comments: Option<String>) {
self.status = SignerStatus::Rejected;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
}
/// Contract revision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractRevision {
pub version: u32,
pub content: String,
pub created_at: DateTime<Utc>,
pub created_by: String,
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 {
Self {
version,
content,
created_at: Utc::now(),
created_by,
comments,
}
}
}
/// Table of Contents item for multi-page contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocItem {
pub title: String,
pub file: String,
pub children: Vec<TocItem>,
}
/// Contract model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub id: String,
pub title: String,
pub description: String,
pub contract_type: ContractType,
pub status: ContractStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
pub effective_date: Option<DateTime<Utc>>,
pub expiration_date: Option<DateTime<Utc>>,
pub signers: Vec<ContractSigner>,
pub revisions: Vec<ContractRevision>,
pub current_version: u32,
pub organization_id: Option<String>,
// Multi-page markdown support
pub content_dir: Option<String>,
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 {
Self {
id: Uuid::new_v4().to_string(),
title,
description,
contract_type,
status: ContractStatus::Draft,
created_at: Utc::now(),
updated_at: Utc::now(),
created_by,
effective_date: None,
expiration_date: None,
signers: Vec::new(),
revisions: Vec::new(),
current_version: 1,
organization_id,
content_dir: None,
toc: None,
}
}
/// Adds a signer to the contract
pub fn add_signer(&mut self, name: String, email: String) {
let signer = ContractSigner::new(name, email);
self.signers.push(signer);
self.updated_at = Utc::now();
}
/// Adds a revision to the contract
pub fn add_revision(&mut self, content: String, created_by: String, comments: Option<String>) {
let new_version = self.current_version + 1;
let revision = ContractRevision::new(new_version, content, created_by, comments);
self.revisions.push(revision);
self.current_version = new_version;
self.updated_at = Utc::now();
}
/// Sends the contract for signatures
pub fn send_for_signatures(&mut self) -> Result<(), String> {
if self.revisions.is_empty() {
return Err("Cannot send contract without content".to_string());
}
if self.signers.is_empty() {
return Err("Cannot send contract without signers".to_string());
}
self.status = ContractStatus::PendingSignatures;
self.updated_at = Utc::now();
Ok(())
}
/// Checks if all signers have signed
pub fn is_fully_signed(&self) -> bool {
if self.signers.is_empty() {
return false;
}
self.signers
.iter()
.all(|signer| signer.status == SignerStatus::Signed)
}
/// Marks the contract as signed if all signers have signed
pub fn finalize_if_signed(&mut self) -> bool {
if self.is_fully_signed() {
self.status = ContractStatus::Signed;
self.updated_at = Utc::now();
true
} else {
false
}
}
/// Cancels the contract
pub fn cancel(&mut self) {
self.status = ContractStatus::Cancelled;
self.updated_at = Utc::now();
}
/// Gets the latest revision
pub fn latest_revision(&self) -> Option<&ContractRevision> {
self.revisions.last()
}
/// Gets a specific revision
pub fn get_revision(&self, version: u32) -> Option<&ContractRevision> {
self.revisions.iter().find(|r| r.version == version)
}
/// Gets the number of pending signers
pub fn pending_signers_count(&self) -> usize {
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()
}
/// Gets the number of rejected signers
pub fn rejected_signers_count(&self) -> usize {
self.signers
.iter()
.filter(|s| s.status == SignerStatus::Rejected)
.count()
}
}
/// Contract filter for listing contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractFilter {
pub status: Option<ContractStatus>,
pub contract_type: Option<ContractType>,
pub created_by: Option<String>,
pub organization_id: Option<String>,
}
/// Contract statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractStatistics {
pub total_contracts: usize,
pub draft_contracts: usize,
pub pending_signature_contracts: usize,
pub signed_contracts: usize,
pub expired_contracts: usize,
pub cancelled_contracts: usize,
}
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();
Self {
total_contracts,
draft_contracts,
pending_signature_contracts,
signed_contracts,
expired_contracts,
cancelled_contracts,
}
}
}

View File

@@ -0,0 +1,209 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use lazy_static::lazy_static;
use uuid::Uuid;
// DeFi position status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DefiPositionStatus {
Active,
Completed,
Liquidated,
Cancelled
}
#[allow(dead_code)]
impl DefiPositionStatus {
pub fn as_str(&self) -> &str {
match self {
DefiPositionStatus::Active => "Active",
DefiPositionStatus::Completed => "Completed",
DefiPositionStatus::Liquidated => "Liquidated",
DefiPositionStatus::Cancelled => "Cancelled",
}
}
}
// DeFi position type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DefiPositionType {
Providing,
Receiving,
Liquidity,
Staking,
Collateral,
}
#[allow(dead_code)]
impl DefiPositionType {
pub fn as_str(&self) -> &str {
match self {
DefiPositionType::Providing => "Providing",
DefiPositionType::Receiving => "Receiving",
DefiPositionType::Liquidity => "Liquidity",
DefiPositionType::Staking => "Staking",
DefiPositionType::Collateral => "Collateral",
}
}
}
// Base DeFi position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefiPosition {
pub id: String,
pub position_type: DefiPositionType,
pub status: DefiPositionStatus,
pub asset_id: String,
pub asset_name: String,
pub asset_symbol: String,
pub amount: f64,
pub value_usd: f64,
pub expected_return: f64,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub user_id: String,
}
// Providing position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidingPosition {
pub base: DefiPosition,
pub duration_days: i32,
pub profit_share_earned: f64,
pub return_amount: f64,
}
// Receiving position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceivingPosition {
pub base: DefiPosition,
pub collateral_asset_id: String,
pub collateral_asset_name: String,
pub collateral_asset_symbol: String,
pub collateral_amount: f64,
pub collateral_value_usd: f64,
pub duration_days: i32,
pub profit_share_rate: f64,
pub profit_share_owed: f64,
pub total_to_repay: f64,
pub collateral_ratio: f64,
}
// In-memory database for DeFi positions
pub struct DefiDatabase {
providing_positions: HashMap<String, ProvidingPosition>,
receiving_positions: HashMap<String, ReceivingPosition>,
}
#[allow(dead_code)]
impl DefiDatabase {
pub fn new() -> Self {
Self {
providing_positions: HashMap::new(),
receiving_positions: HashMap::new(),
}
}
// Providing operations
pub fn add_providing_position(&mut self, position: ProvidingPosition) {
self.providing_positions.insert(position.base.id.clone(), position);
}
pub fn get_providing_position(&self, id: &str) -> Option<&ProvidingPosition> {
self.providing_positions.get(id)
}
pub fn get_all_providing_positions(&self) -> Vec<&ProvidingPosition> {
self.providing_positions.values().collect()
}
pub fn get_user_providing_positions(&self, user_id: &str) -> Vec<&ProvidingPosition> {
self.providing_positions
.values()
.filter(|p| p.base.user_id == user_id)
.collect()
}
// Receiving operations
pub fn add_receiving_position(&mut self, position: ReceivingPosition) {
self.receiving_positions.insert(position.base.id.clone(), position);
}
pub fn get_receiving_position(&self, id: &str) -> Option<&ReceivingPosition> {
self.receiving_positions.get(id)
}
pub fn get_all_receiving_positions(&self) -> Vec<&ReceivingPosition> {
self.receiving_positions.values().collect()
}
pub fn get_user_receiving_positions(&self, user_id: &str) -> Vec<&ReceivingPosition> {
self.receiving_positions
.values()
.filter(|p| p.base.user_id == user_id)
.collect()
}
}
// Global instance of the DeFi database
lazy_static! {
pub static ref DEFI_DB: Arc<Mutex<DefiDatabase>> = Arc::new(Mutex::new(DefiDatabase::new()));
}
// Initialize the database with mock data
pub fn initialize_mock_data() {
let mut db = DEFI_DB.lock().unwrap();
// Add mock providing positions
let providing_position = ProvidingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Providing,
status: DefiPositionStatus::Active,
asset_id: "TFT".to_string(),
asset_name: "ThreeFold Token".to_string(),
asset_symbol: "TFT".to_string(),
amount: 1000.0,
value_usd: 500.0,
expected_return: 4.2,
created_at: Utc::now(),
expires_at: Some(Utc::now() + chrono::Duration::days(30)),
user_id: "user123".to_string(),
},
duration_days: 30,
profit_share_earned: 3.5,
return_amount: 1003.5,
};
db.add_providing_position(providing_position);
// Add mock receiving positions
let receiving_position = ReceivingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Receiving,
status: DefiPositionStatus::Active,
asset_id: "ZDFZ".to_string(),
asset_name: "Zanzibar Token".to_string(),
asset_symbol: "ZDFZ".to_string(),
amount: 500.0,
value_usd: 250.0,
expected_return: 5.8,
created_at: Utc::now(),
expires_at: Some(Utc::now() + chrono::Duration::days(90)),
user_id: "user123".to_string(),
},
collateral_asset_id: "TFT".to_string(),
collateral_asset_name: "ThreeFold Token".to_string(),
collateral_asset_symbol: "TFT".to_string(),
collateral_amount: 1500.0,
collateral_value_usd: 750.0,
duration_days: 90,
profit_share_rate: 5.8,
profit_share_owed: 3.625,
total_to_repay: 503.625,
collateral_ratio: 300.0,
};
db.add_receiving_position(receiving_position);
}

View File

@@ -0,0 +1,254 @@
#![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))
}
}
}

View File

@@ -0,0 +1,390 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Status of a flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FlowStatus {
/// Flow is in progress
InProgress,
/// Flow is completed
Completed,
/// Flow is stuck at a step
Stuck,
/// Flow is cancelled
Cancelled,
}
impl std::fmt::Display for FlowStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowStatus::InProgress => write!(f, "In Progress"),
FlowStatus::Completed => write!(f, "Completed"),
FlowStatus::Stuck => write!(f, "Stuck"),
FlowStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
/// Type of flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FlowType {
/// Company registration flow
CompanyRegistration,
/// User onboarding flow
UserOnboarding,
/// Service activation flow
ServiceActivation,
/// Payment processing flow
PaymentProcessing,
/// Asset tokenization flow
AssetTokenization,
/// Certification flow
Certification,
/// License application flow
LicenseApplication,
}
impl std::fmt::Display for FlowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowType::CompanyRegistration => write!(f, "Company Registration"),
FlowType::UserOnboarding => write!(f, "User Onboarding"),
FlowType::ServiceActivation => write!(f, "Service Activation"),
FlowType::PaymentProcessing => write!(f, "Payment Processing"),
FlowType::AssetTokenization => write!(f, "Asset Tokenization"),
FlowType::Certification => write!(f, "Certification"),
FlowType::LicenseApplication => write!(f, "License Application"),
}
}
}
/// Filter for flows
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FlowFilter {
/// All flows
All,
/// Only in progress flows
InProgress,
/// Only completed flows
Completed,
/// Only stuck flows
Stuck,
/// Only cancelled flows
Cancelled,
/// Flows of a specific type
ByType(FlowType),
}
impl std::fmt::Display for FlowFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowFilter::All => write!(f, "All"),
FlowFilter::InProgress => write!(f, "In Progress"),
FlowFilter::Completed => write!(f, "Completed"),
FlowFilter::Stuck => write!(f, "Stuck"),
FlowFilter::Cancelled => write!(f, "Cancelled"),
FlowFilter::ByType(flow_type) => write!(f, "Type: {}", flow_type),
}
}
}
/// A step in a flow
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStep {
/// Step ID
pub id: String,
/// Step name
pub name: String,
/// Step description
pub description: String,
/// Step status
pub status: StepStatus,
/// Step order in the flow
pub order: u32,
/// Step started at
pub started_at: Option<DateTime<Utc>>,
/// Step completed at
pub completed_at: Option<DateTime<Utc>>,
/// Step logs
pub logs: Vec<FlowLog>,
}
#[allow(dead_code)]
impl FlowStep {
/// Creates a new flow step
pub fn new(name: String, description: String, order: u32) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
description,
status: StepStatus::Pending,
order,
started_at: None,
completed_at: None,
logs: Vec::new(),
}
}
/// Starts the step
pub fn start(&mut self) {
self.status = StepStatus::InProgress;
self.started_at = Some(Utc::now());
self.add_log("Step started".to_string());
}
/// Completes the step
pub fn complete(&mut self) {
self.status = StepStatus::Completed;
self.completed_at = Some(Utc::now());
self.add_log("Step completed".to_string());
}
/// Marks the step as stuck
pub fn mark_stuck(&mut self, reason: String) {
self.status = StepStatus::Stuck;
self.add_log(format!("Step stuck: {}", reason));
}
/// Adds a log entry to the step
pub fn add_log(&mut self, message: String) {
self.logs.push(FlowLog::new(message));
}
}
/// Status of a step in a flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StepStatus {
/// Step is pending
Pending,
/// Step is in progress
InProgress,
/// Step is completed
Completed,
/// Step is stuck
Stuck,
/// Step is skipped
Skipped,
}
impl std::fmt::Display for StepStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StepStatus::Pending => write!(f, "Pending"),
StepStatus::InProgress => write!(f, "In Progress"),
StepStatus::Completed => write!(f, "Completed"),
StepStatus::Stuck => write!(f, "Stuck"),
StepStatus::Skipped => write!(f, "Skipped"),
}
}
}
/// A log entry in a flow step
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowLog {
/// Log ID
pub id: String,
/// Log message
pub message: String,
/// Log timestamp
pub timestamp: DateTime<Utc>,
}
#[allow(dead_code)]
impl FlowLog {
/// Creates a new flow log
pub fn new(message: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
message,
timestamp: Utc::now(),
}
}
}
/// A flow with multiple steps
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Flow {
/// Flow ID
pub id: String,
/// Flow name
pub name: String,
/// Flow description
pub description: String,
/// Flow type
pub flow_type: FlowType,
/// Flow status
pub status: FlowStatus,
/// Flow owner ID
pub owner_id: String,
/// Flow owner name
pub owner_name: String,
/// Flow created at
pub created_at: DateTime<Utc>,
/// Flow updated at
pub updated_at: DateTime<Utc>,
/// Flow completed at
pub completed_at: Option<DateTime<Utc>>,
/// Flow steps
pub steps: Vec<FlowStep>,
/// Progress percentage
pub progress_percentage: u8,
/// Current step
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 {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let steps = vec![
FlowStep::new("Initialization".to_string(), "Setting up the flow".to_string(), 1),
FlowStep::new("Processing".to_string(), "Processing the flow data".to_string(), 2),
FlowStep::new("Finalization".to_string(), "Completing the flow".to_string(), 3),
];
// Set the first step as in progress
let mut flow = Self {
id,
name: name.to_string(),
description: description.to_string(),
flow_type,
status: FlowStatus::InProgress,
owner_id: owner_id.to_string(),
owner_name: owner_name.to_string(),
steps,
created_at: now,
updated_at: now,
completed_at: None,
progress_percentage: 0,
current_step: None,
};
// Calculate progress and set current step
flow.update_progress();
flow
}
fn update_progress(&mut self) {
// Calculate progress percentage
let total_steps = self.steps.len();
if total_steps == 0 {
self.progress_percentage = 100;
return;
}
let completed_steps = self.steps.iter().filter(|s| s.status == StepStatus::Completed).count();
self.progress_percentage = ((completed_steps as f32 / total_steps as f32) * 100.0) as u8;
// Find current step
self.current_step = self.steps.iter()
.find(|s| s.status == StepStatus::InProgress)
.cloned();
// Update flow status based on steps
if self.progress_percentage == 100 {
self.status = FlowStatus::Completed;
} else if self.steps.iter().any(|s| s.status == StepStatus::Stuck) {
self.status = FlowStatus::Stuck;
} else {
self.status = FlowStatus::InProgress;
}
}
pub fn advance_step(&mut self) -> Result<(), String> {
let current_index = self.steps.iter().position(|s| s.status == StepStatus::InProgress);
if let Some(index) = current_index {
// Mark current step as completed
self.steps[index].status = StepStatus::Completed;
self.steps[index].completed_at = Some(Utc::now());
// If there's a next step, mark it as in progress
if index + 1 < self.steps.len() {
self.steps[index + 1].status = StepStatus::InProgress;
self.steps[index + 1].started_at = Some(Utc::now());
}
self.updated_at = Utc::now();
self.update_progress();
Ok(())
} else {
Err("No step in progress to advance".to_string())
}
}
pub fn mark_step_stuck(&mut self, reason: &str) -> Result<(), String> {
let current_index = self.steps.iter().position(|s| s.status == StepStatus::InProgress);
if let Some(index) = current_index {
// Mark current step as stuck
self.steps[index].status = StepStatus::Stuck;
// Add a log entry for the stuck reason
self.steps[index].add_log(reason.to_string());
self.updated_at = Utc::now();
self.update_progress();
Ok(())
} else {
Err("No step in progress to mark as stuck".to_string())
}
}
pub fn add_log_to_step(&mut self, step_id: &str, message: &str) -> Result<(), String> {
if let Some(step) = self.steps.iter_mut().find(|s| s.id == step_id) {
step.add_log(message.to_string());
self.updated_at = Utc::now();
Ok(())
} else {
Err(format!("Step with ID {} not found", step_id))
}
}
}
/// Flow statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStatistics {
/// Total number of flows
pub total_flows: usize,
/// Number of in progress flows
pub in_progress_flows: usize,
/// Number of completed flows
pub completed_flows: usize,
/// Number of stuck flows
pub stuck_flows: usize,
/// Number of cancelled flows
pub cancelled_flows: usize,
}
impl FlowStatistics {
/// Creates new flow statistics
pub fn new(flows: &[Flow]) -> Self {
let total_flows = flows.len();
let in_progress_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::InProgress)
.count();
let completed_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Completed)
.count();
let stuck_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Stuck)
.count();
let cancelled_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Cancelled)
.count();
Self {
total_flows,
in_progress_flows,
completed_flows,
stuck_flows,
cancelled_flows,
}
}
}

View File

@@ -0,0 +1,315 @@
use crate::models::asset::AssetType;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Status of a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListingStatus {
Active,
Sold,
Cancelled,
Expired,
}
#[allow(dead_code)]
impl ListingStatus {
pub fn as_str(&self) -> &str {
match self {
ListingStatus::Active => "Active",
ListingStatus::Sold => "Sold",
ListingStatus::Cancelled => "Cancelled",
ListingStatus::Expired => "Expired",
}
}
}
/// Type of marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListingType {
FixedPrice,
Auction,
Exchange,
}
impl ListingType {
pub fn as_str(&self) -> &str {
match self {
ListingType::FixedPrice => "Fixed Price",
ListingType::Auction => "Auction",
ListingType::Exchange => "Exchange",
}
}
}
/// Represents a bid on an auction listing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bid {
pub id: String,
pub listing_id: String,
pub bidder_id: String,
pub bidder_name: String,
pub amount: f64,
pub currency: String,
pub created_at: DateTime<Utc>,
pub status: BidStatus,
}
/// Status of a bid
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BidStatus {
Active,
Accepted,
Rejected,
Cancelled,
}
#[allow(dead_code)]
impl BidStatus {
pub fn as_str(&self) -> &str {
match self {
BidStatus::Active => "Active",
BidStatus::Accepted => "Accepted",
BidStatus::Rejected => "Rejected",
BidStatus::Cancelled => "Cancelled",
}
}
}
/// Represents a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listing {
pub id: String,
pub title: String,
pub description: String,
pub asset_id: String,
pub asset_name: String,
pub asset_type: AssetType,
pub seller_id: String,
pub seller_name: String,
pub price: f64,
pub currency: String,
pub listing_type: ListingType,
pub status: ListingStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub sold_at: Option<DateTime<Utc>>,
pub buyer_id: Option<String>,
pub buyer_name: Option<String>,
pub sale_price: Option<f64>,
pub bids: Vec<Bid>,
pub views: u32,
pub featured: bool,
pub tags: Vec<String>,
pub image_url: Option<String>,
}
#[allow(dead_code)]
impl Listing {
/// Creates a new listing
pub fn new(
title: String,
description: String,
asset_id: String,
asset_name: String,
asset_type: AssetType,
seller_id: String,
seller_name: String,
price: f64,
currency: String,
listing_type: ListingType,
expires_at: Option<DateTime<Utc>>,
tags: Vec<String>,
image_url: Option<String>,
) -> Self {
let now = Utc::now();
Self {
id: format!("listing-{}", Uuid::new_v4().to_string()),
title,
description,
asset_id,
asset_name,
asset_type,
seller_id,
seller_name,
price,
currency,
listing_type,
status: ListingStatus::Active,
created_at: now,
updated_at: now,
expires_at,
sold_at: None,
buyer_id: None,
buyer_name: None,
sale_price: None,
bids: Vec::new(),
views: 0,
featured: false,
tags,
image_url,
}
}
/// Adds a bid to the listing
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());
}
if self.listing_type != ListingType::Auction {
return Err("Listing is not an auction".to_string());
}
if currency != self.currency {
return Err(format!(
"Currency mismatch: expected {}, got {}",
self.currency, currency
));
}
// Check if bid amount is higher than current highest bid or starting price
let highest_bid = self.highest_bid();
let min_bid = match highest_bid {
Some(bid) => bid.amount,
None => self.price,
};
if amount <= min_bid {
return Err(format!("Bid amount must be higher than {}", min_bid));
}
let bid = Bid {
id: format!("bid-{}", Uuid::new_v4().to_string()),
listing_id: self.id.clone(),
bidder_id,
bidder_name,
amount,
currency,
created_at: Utc::now(),
status: BidStatus::Active,
};
self.bids.push(bid);
self.updated_at = Utc::now();
Ok(())
}
/// Gets the highest bid on the listing
pub fn highest_bid(&self) -> Option<&Bid> {
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> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
self.status = ListingStatus::Sold;
self.sold_at = Some(Utc::now());
self.buyer_id = Some(buyer_id);
self.buyer_name = Some(buyer_name);
self.sale_price = Some(sale_price);
self.updated_at = Utc::now();
Ok(())
}
/// Cancels the listing
pub fn cancel(&mut self) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
self.status = ListingStatus::Cancelled;
self.updated_at = Utc::now();
Ok(())
}
/// Increments the view count
pub fn increment_views(&mut self) {
self.views += 1;
}
/// Sets the listing as featured
pub fn set_featured(&mut self, featured: bool) {
self.featured = featured;
self.updated_at = Utc::now();
}
}
/// Statistics for marketplace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplaceStatistics {
pub total_listings: usize,
pub active_listings: usize,
pub sold_listings: usize,
pub total_value: f64,
pub total_sales: f64,
pub listings_by_type: std::collections::HashMap<String, usize>,
pub sales_by_asset_type: std::collections::HashMap<String, f64>,
}
impl MarketplaceStatistics {
pub fn new(listings: &[Listing]) -> Self {
let mut total_value = 0.0;
let mut total_sales = 0.0;
let mut listings_by_type = std::collections::HashMap::new();
let mut sales_by_asset_type = std::collections::HashMap::new();
let active_listings = listings
.iter()
.filter(|l| l.status == ListingStatus::Active)
.count();
let sold_listings = listings
.iter()
.filter(|l| l.status == ListingStatus::Sold)
.count();
for listing in listings {
if listing.status == ListingStatus::Active {
total_value += listing.price;
}
if listing.status == ListingStatus::Sold {
if let Some(sale_price) = listing.sale_price {
total_sales += sale_price;
let asset_type = listing.asset_type.as_str().to_string();
*sales_by_asset_type.entry(asset_type).or_insert(0.0) += sale_price;
}
}
let listing_type = listing.listing_type.as_str().to_string();
*listings_by_type.entry(listing_type).or_insert(0) += 1;
}
Self {
total_listings: listings.len(),
active_listings,
sold_listings,
total_value,
total_sales,
listings_by_type,
sales_by_asset_type,
}
}
}

View File

@@ -0,0 +1,81 @@
#![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);
}
}

View File

@@ -1,9 +1,18 @@
// Export models
pub mod user;
pub mod ticket;
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;
// 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, TicketFilter};
pub use calendar::{CalendarEvent, CalendarViewMode};

View File

@@ -76,6 +76,7 @@ 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 {

View File

@@ -4,6 +4,7 @@ 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>,
@@ -31,6 +32,7 @@ pub enum UserRole {
Admin,
}
#[allow(dead_code)]
impl User {
/// Creates a new user with default values
pub fn new(name: String, email: String) -> Self {
@@ -125,6 +127,7 @@ impl User {
/// Represents user login credentials
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LoginCredentials {
pub email: String,
pub password: String,
@@ -132,6 +135,7 @@ pub struct LoginCredentials {
/// Represents user registration data
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct RegistrationData {
pub name: String,
pub email: String,
@@ -145,8 +149,8 @@ mod tests {
#[test]
fn test_new_user() {
let user = User::new("John Doe".to_string(), "john@example.com".to_string());
assert_eq!(user.name, "John Doe");
let user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
assert_eq!(user.name, "Robert Callingham");
assert_eq!(user.email, "john@example.com");
assert!(!user.is_admin());
}
@@ -161,13 +165,13 @@ mod tests {
#[test]
fn test_update_user() {
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string());
user.update(Some("Jane Doe".to_string()), None);
assert_eq!(user.name, "Jane Doe");
let mut user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
user.update(Some("Mary Hewell".to_string()), None);
assert_eq!(user.name, "Mary Hewell");
assert_eq!(user.email, "john@example.com");
user.update(None, Some("jane@example.com".to_string()));
assert_eq!(user.name, "Jane Doe");
assert_eq!(user.name, "Mary Hewell");
assert_eq!(user.email, "jane@example.com");
}
}

View File

@@ -1,21 +1,31 @@
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::middleware::JwtAuth;
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;
/// 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(
@@ -26,37 +36,304 @@ 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),
)
// 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("/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)),
)
// 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("/{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),
),
)
// Asset routes
.service(
web::scope("/assets")
.route("", web::get().to(AssetController::index))
.route("/list", web::get().to(AssetController::list))
.route("/my", web::get().to(AssetController::my_assets))
.route("/create", web::get().to(AssetController::create_form))
.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),
),
)
// Marketplace routes
.service(
web::scope("/marketplace")
.route("", web::get().to(MarketplaceController::index))
.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),
),
)
// 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("/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),
),
)
// Company routes
.service(
web::scope("/company")
.route("", web::get().to(CompanyController::index))
// OLD REGISTRATION ROUTE REMOVED - Now only payment flow creates companies
.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),
),
),
);
// 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
);
}
}

View File

@@ -0,0 +1,173 @@
// Company data (would be loaded from backend in production)
var companyData = {
'company1': {
name: 'Zanzibar Digital Solutions',
type: 'Startup FZC',
status: 'Active',
registrationDate: '2025-04-01',
purpose: 'Digital solutions and blockchain development',
plan: 'Startup FZC - $50/month',
nextBilling: '2025-06-01',
paymentMethod: 'Credit Card (****4582)',
shareholders: [
{ name: 'John Smith', percentage: '60%' },
{ name: 'Sarah Johnson', percentage: '40%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' }
]
},
'company2': {
name: 'Blockchain Innovations Ltd',
type: 'Growth FZC',
status: 'Active',
registrationDate: '2025-03-15',
purpose: 'Blockchain technology research and development',
plan: 'Growth FZC - $100/month',
nextBilling: '2025-06-15',
paymentMethod: 'Bank Transfer',
shareholders: [
{ name: 'Michael Chen', percentage: '35%' },
{ name: 'Aisha Patel', percentage: '35%' },
{ name: 'David Okonkwo', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' },
{ name: 'Physical Asset Holding', status: 'Signed' }
]
},
'company3': {
name: 'Sustainable Energy Cooperative',
type: 'Cooperative FZC',
status: 'Pending',
registrationDate: '2025-05-01',
purpose: 'Renewable energy production and distribution',
plan: 'Cooperative FZC - $200/month',
nextBilling: 'Pending Activation',
paymentMethod: 'Pending',
shareholders: [
{ name: 'Community Energy Group', percentage: '40%' },
{ name: 'Green Future Initiative', percentage: '30%' },
{ name: 'Sustainable Living Collective', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Cooperative Governance', status: 'Pending' }
]
}
};
// Current company ID for modal
var currentCompanyId = null;
// View company details function
function viewCompanyDetails(companyId) {
// Store current company ID
currentCompanyId = companyId;
// Get company data
const company = companyData[companyId];
if (!company) return;
// Update modal title
document.getElementById('companyDetailsModalLabel').innerHTML =
`<i class="bi bi-building me-2"></i>${company.name} Details`;
// Update general information
document.getElementById('modal-company-name').textContent = company.name;
document.getElementById('modal-company-type').textContent = company.type;
document.getElementById('modal-registration-date').textContent = company.registrationDate;
// Update status with appropriate badge
const statusBadge = company.status === 'Active' ?
`<span class="badge bg-success">${company.status}</span>` :
`<span class="badge bg-warning text-dark">${company.status}</span>`;
document.getElementById('modal-status').innerHTML = statusBadge;
document.getElementById('modal-purpose').textContent = company.purpose;
// Update billing information
document.getElementById('modal-plan').textContent = company.plan;
document.getElementById('modal-next-billing').textContent = company.nextBilling;
document.getElementById('modal-payment-method').textContent = company.paymentMethod;
// Update shareholders table
const shareholdersTable = document.getElementById('modal-shareholders');
shareholdersTable.innerHTML = '';
company.shareholders.forEach(shareholder => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${shareholder.name}</td>
<td>${shareholder.percentage}</td>
`;
shareholdersTable.appendChild(row);
});
// Update contracts table
const contractsTable = document.getElementById('modal-contracts');
contractsTable.innerHTML = '';
company.contracts.forEach(contract => {
const row = document.createElement('tr');
const statusBadge = contract.status === 'Signed' ?
`<span class="badge bg-success">${contract.status}</span>` :
`<span class="badge bg-warning text-dark">${contract.status}</span>`;
row.innerHTML = `
<td>${contract.name}</td>
<td>${statusBadge}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="viewContract('${contract.name.toLowerCase().replace(/\s+/g, '-')}')">View</button></td>
`;
contractsTable.appendChild(row);
});
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('companyDetailsModal'));
modal.show();
}
// Switch to entity function
function switchToEntity(companyId) {
const company = companyData[companyId];
if (!company) return;
// In a real application, this would redirect to the entity context
// For now, we'll just show an alert
alert(`Switching to ${company.name} entity context. All UI will now reflect this entity's governance, billing, and other features.`);
// This would typically involve:
// 1. Setting a session/cookie for the current entity
// 2. Redirecting to the dashboard with that entity context
// window.location.href = `/dashboard?entity=${companyId}`;
}
// Switch to entity from modal
function switchToEntityFromModal() {
if (currentCompanyId) {
switchToEntity(currentCompanyId);
// Close the modal
const modal = bootstrap.Modal.getInstance(document.getElementById('companyDetailsModal'));
modal.hide();
}
}
// View contract function
function viewContract(contractId) {
// In a real application, this would open the contract document
// For now, we'll just show an alert
alert(`Viewing contract: ${contractId.replace(/-/g, ' ')}`);
// This would typically involve:
// 1. Fetching the contract document from the server
// 2. Opening it in a viewer or new tab
// window.open(`/contracts/view/${contractId}`, '_blank');
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Company management script loaded');
});

View File

@@ -1,16 +1,44 @@
use actix_web::{Error, HttpResponse};
use chrono::{DateTime, Utc};
use tera::{self, Function, Result, Value};
use pulldown_cmark::{Options, Parser, html};
use std::error::Error as StdError;
use tera::{self, Context, Function, Tera, Value};
// Export modules
pub mod redis_service;
pub mod secure_logging;
pub mod stripe_security;
// Re-export for easier imports
pub use redis_service::RedisCalendarService;
// pub use redis_service::RedisCalendarService; // Currently unused
/// Registers custom Tera functions
/// Error type for template rendering
#[derive(Debug)]
#[allow(dead_code)]
pub struct TemplateError {
pub message: String,
pub details: String,
pub location: String,
}
impl std::fmt::Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Template error in {}: {}", self.location, self.message)
}
}
impl std::error::Error for TemplateError {}
/// Registers custom Tera functions and filters
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
@@ -18,7 +46,7 @@ pub fn register_tera_functions(tera: &mut tera::Tera) {
pub struct NowFunction;
impl Function for NowFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
@@ -28,7 +56,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()));
@@ -43,21 +71,17 @@ impl Function for NowFunction {
pub struct FormatDateFunction;
impl Function for FormatDateFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
let timestamp = match args.get("timestamp") {
Some(val) => match val.as_i64() {
Some(ts) => ts,
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") {
@@ -71,23 +95,130 @@ 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()
@@ -96,6 +227,112 @@ 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
/// the error template with detailed error information.
pub fn render_template(
tmpl: &Tera,
template_name: &str,
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: 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>
<html>
<head>
<title>Template Error</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }}
.error-container {{ border: 1px solid #f5c6cb; background-color: #f8d7da; padding: 20px; border-radius: 5px; }}
.error-title {{ color: #721c24; }}
.error-details {{ background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 20px; }}
pre {{ background-color: #f1f1f1; padding: 10px; overflow: auto; }}
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">Template Rendering Error</h1>
<p>There was an error rendering the template: <strong>{}</strong></p>
<div class="error-details">
<h3>Error Details:</h3>
<pre>{}</pre>
<h3>Error Chain:</h3>
<pre>{}</pre>
</div>
</div>
</body>
</html>"#,
template_name,
e,
error_chain.join("\n")
);
println!("DEBUG: Returning simple error page");
Ok(HttpResponse::InternalServerError()
.content_type("text/html")
.body(error_html))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -106,4 +343,4 @@ mod tests {
assert_eq!(truncate_string("Hello, world!", 5), "Hello...");
assert_eq!(truncate_string("", 5), "");
}
}
}

View File

@@ -1,7 +1,9 @@
#![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! {
@@ -11,21 +13,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 {
@@ -42,14 +44,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(
@@ -57,25 +59,25 @@ impl RedisCalendarService {
format!("Failed to serialize event: {}", e),
))
})?;
// Save the event
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id);
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.base_data.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.id)?;
let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.base_data.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| {
@@ -84,34 +86,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 {
@@ -119,23 +121,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)
}
}
}

View File

@@ -0,0 +1,315 @@
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"));
}
}

View File

@@ -0,0 +1,257 @@
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,
&current_signature_header,
webhook_secret,
Some(300),
);
assert!(result.is_ok());
assert!(result.unwrap());
}
}

View File

@@ -0,0 +1,403 @@
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"));
}
}

View File

@@ -0,0 +1,4 @@
pub mod company;
// Re-export for easier imports
pub use company::{CompanyRegistrationValidator, ValidationError, ValidationResult};

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %}
{% block title %}About - Actix MVC App{% endblock %}
{% block title %}About - Zanzibar Digital Freezone{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title">About Actix MVC App</h1>
<p class="card-text">This is a sample application demonstrating how to build a web application using Rust with an MVC architecture.</p>
<h1 class="card-title">About Zanzibar Digital Freezone</h1>
<p class="card-text">Convenience, Safety and Privacy</p>
<h2 class="mt-4">Technology Stack</h2>
<div class="row">

View File

@@ -0,0 +1,271 @@
{% extends "base.html" %}
{% block title %}Create New Digital Asset{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Create New Digital Asset</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
<li class="breadcrumb-item active">Create New Asset</li>
</ol>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-plus-circle me-1"></i>
Asset Details
</div>
<div class="card-body">
<form id="createAssetForm" method="post" action="/assets/create">
<!-- Basic Information -->
<div class="mb-4">
<h5>Basic Information</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="asset_type" class="form-label">Asset Type</label>
<select class="form-select" id="asset_type" name="asset_type" required>
{% for type_value, type_label in asset_types %}
<option value="{{ type_value }}">{{ type_label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="image_url" class="form-label">Image URL (optional)</label>
<input type="url" class="form-control" id="image_url" name="image_url">
<div class="form-text">URL to an image representing this asset</div>
</div>
<div class="col-md-6 mb-3">
<label for="external_url" class="form-label">External URL (optional)</label>
<input type="url" class="form-control" id="external_url" name="external_url">
<div class="form-text">URL to an external resource for this asset</div>
</div>
</div>
</div>
<!-- Blockchain Information -->
<div class="mb-4">
<h5>Blockchain Information (optional)</h5>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="has_blockchain_info" name="has_blockchain_info">
<label class="form-check-label" for="has_blockchain_info">
This asset has blockchain information
</label>
</div>
<div id="blockchainInfoSection" style="display: none;">
<div class="row">
<div class="col-md-6 mb-3">
<label for="blockchain" class="form-label">Blockchain</label>
<input type="text" class="form-control" id="blockchain" name="blockchain">
</div>
<div class="col-md-6 mb-3">
<label for="token_id" class="form-label">Token ID</label>
<input type="text" class="form-control" id="token_id" name="token_id">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="contract_address" class="form-label">Contract Address</label>
<input type="text" class="form-control" id="contract_address" name="contract_address">
</div>
<div class="col-md-6 mb-3">
<label for="owner_address" class="form-label">Owner Address</label>
<input type="text" class="form-control" id="owner_address" name="owner_address">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="transaction_hash" class="form-label">Transaction Hash (optional)</label>
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
</div>
<div class="col-md-6 mb-3">
<label for="block_number" class="form-label">Block Number (optional)</label>
<input type="number" class="form-control" id="block_number" name="block_number">
</div>
</div>
</div>
</div>
<!-- Initial Valuation -->
<div class="mb-4">
<h5>Initial Valuation (optional)</h5>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="has_valuation" name="has_valuation">
<label class="form-check-label" for="has_valuation">
Add an initial valuation for this asset
</label>
</div>
<div id="valuationSection" style="display: none;">
<div class="row">
<div class="col-md-4 mb-3">
<label for="value" class="form-label">Value</label>
<input type="number" class="form-control" id="value" name="value" step="0.01">
</div>
<div class="col-md-4 mb-3">
<label for="currency" class="form-label">Currency</label>
<input type="text" class="form-control" id="currency" name="currency" value="USD">
</div>
<div class="col-md-4 mb-3">
<label for="source" class="form-label">Source</label>
<input type="text" class="form-control" id="source" name="source">
</div>
</div>
<div class="mb-3">
<label for="valuation_notes" class="form-label">Notes</label>
<textarea class="form-control" id="valuation_notes" name="valuation_notes" rows="2"></textarea>
</div>
</div>
</div>
<!-- Metadata -->
<div class="mb-4">
<h5>Additional Metadata (optional)</h5>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="has_metadata" name="has_metadata">
<label class="form-check-label" for="has_metadata">
Add additional metadata for this asset
</label>
</div>
<div id="metadataSection" style="display: none;">
<div class="mb-3">
<label for="metadata" class="form-label">Metadata (JSON format)</label>
<textarea class="form-control" id="metadata" name="metadata" rows="5"></textarea>
<div class="form-text">Enter additional metadata in JSON format</div>
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/assets" class="btn btn-secondary me-md-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Asset</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Toggle blockchain info section
const hasBlockchainInfo = document.getElementById('has_blockchain_info');
const blockchainInfoSection = document.getElementById('blockchainInfoSection');
hasBlockchainInfo.addEventListener('change', function() {
blockchainInfoSection.style.display = this.checked ? 'block' : 'none';
});
// Toggle valuation section
const hasValuation = document.getElementById('has_valuation');
const valuationSection = document.getElementById('valuationSection');
hasValuation.addEventListener('change', function() {
valuationSection.style.display = this.checked ? 'block' : 'none';
});
// Toggle metadata section
const hasMetadata = document.getElementById('has_metadata');
const metadataSection = document.getElementById('metadataSection');
hasMetadata.addEventListener('change', function() {
metadataSection.style.display = this.checked ? 'block' : 'none';
});
// Form validation
const form = document.getElementById('createAssetForm');
form.addEventListener('submit', function(event) {
let isValid = true;
// Validate required fields
const name = document.getElementById('name').value.trim();
const description = document.getElementById('description').value.trim();
if (!name) {
isValid = false;
document.getElementById('name').classList.add('is-invalid');
} else {
document.getElementById('name').classList.remove('is-invalid');
}
if (!description) {
isValid = false;
document.getElementById('description').classList.add('is-invalid');
} else {
document.getElementById('description').classList.remove('is-invalid');
}
// Validate blockchain info if checked
if (hasBlockchainInfo.checked) {
const blockchain = document.getElementById('blockchain').value.trim();
const tokenId = document.getElementById('token_id').value.trim();
const contractAddress = document.getElementById('contract_address').value.trim();
const ownerAddress = document.getElementById('owner_address').value.trim();
if (!blockchain || !tokenId || !contractAddress || !ownerAddress) {
isValid = false;
if (!blockchain) document.getElementById('blockchain').classList.add('is-invalid');
if (!tokenId) document.getElementById('token_id').classList.add('is-invalid');
if (!contractAddress) document.getElementById('contract_address').classList.add('is-invalid');
if (!ownerAddress) document.getElementById('owner_address').classList.add('is-invalid');
}
}
// Validate valuation if checked
if (hasValuation.checked) {
const value = document.getElementById('value').value.trim();
const currency = document.getElementById('currency').value.trim();
const source = document.getElementById('source').value.trim();
if (!value || !currency || !source) {
isValid = false;
if (!value) document.getElementById('value').classList.add('is-invalid');
if (!currency) document.getElementById('currency').classList.add('is-invalid');
if (!source) document.getElementById('source').classList.add('is-invalid');
}
}
// Validate metadata if checked
if (hasMetadata.checked) {
const metadata = document.getElementById('metadata').value.trim();
if (metadata) {
try {
JSON.parse(metadata);
document.getElementById('metadata').classList.remove('is-invalid');
} catch (e) {
isValid = false;
document.getElementById('metadata').classList.add('is-invalid');
}
}
}
if (!isValid) {
event.preventDefault();
alert('Please fix the errors in the form before submitting.');
}
});
});
</script>
<style>
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.is-invalid {
border-color: #dc3545;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,556 @@
{% extends "base.html" %}
{% block title %}Asset Details - {{ asset.name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Asset Details</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
<li class="breadcrumb-item active">{{ asset.name }}</li>
</ol>
<!-- Asset Overview -->
<div class="row">
<div class="col-xl-4">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-info-circle me-1"></i>
Asset Information
</div>
<div class="card-body">
<div class="text-center mb-4">
{% if asset.image_url %}
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="img-fluid asset-image mb-3">
{% else %}
<div class="asset-placeholder mb-3">
<i class="fas fa-cube fa-5x"></i>
</div>
{% endif %}
<h3>{{ asset.name }}</h3>
<div>
{% if asset.status == "Active" %}
<span class="badge bg-success">{{ asset.status }}</span>
{% elif asset.status == "For Sale" %}
<span class="badge bg-warning">{{ asset.status }}</span>
{% elif asset.status == "Locked" %}
<span class="badge bg-secondary">{{ asset.status }}</span>
{% elif asset.status == "Transferred" %}
<span class="badge bg-info">{{ asset.status }}</span>
{% elif asset.status == "Archived" %}
<span class="badge bg-danger">{{ asset.status }}</span>
{% else %}
<span class="badge bg-primary">{{ asset.status }}</span>
{% endif %}
<span class="badge bg-primary">{{ asset.asset_type }}</span>
</div>
</div>
<div class="mb-3">
<h5>Description</h5>
<p>{{ asset.description }}</p>
</div>
<div class="mb-3">
<h5>Current Valuation</h5>
{% if asset.current_valuation %}
<h3 class="text-primary">{{ asset.valuation_currency }}{{ asset.current_valuation }}</h3>
<small class="text-muted">Last updated: {{ asset.valuation_date }}</small>
{% else %}
<p>No valuation available</p>
{% endif %}
</div>
<div class="mb-3">
<h5>Owner Information</h5>
<p><strong>Owner:</strong> {{ asset.owner_name }}</p>
<p><strong>Owner ID:</strong> {{ asset.owner_id }}</p>
</div>
<div class="mb-3">
<h5>Dates</h5>
<p><strong>Created:</strong> {{ asset.created_at }}</p>
<p><strong>Last Updated:</strong> {{ asset.updated_at }}</p>
</div>
{% if asset.external_url %}
<div class="mb-3">
<a href="{{ asset.external_url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-external-link-alt me-1"></i> View External Resource
</a>
</div>
{% endif %}
</div>
<div class="card-footer">
<div class="d-grid gap-2">
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#valuationModal">
<i class="fas fa-dollar-sign me-1"></i> Add Valuation
</button>
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#transactionModal">
<i class="fas fa-exchange-alt me-1"></i> Record Transaction
</button>
<button class="btn btn-warning" type="button" data-bs-toggle="modal" data-bs-target="#statusModal">
<i class="fas fa-edit me-1"></i> Change Status
</button>
</div>
</div>
</div>
</div>
<div class="col-xl-8">
<!-- Blockchain Information -->
{% if asset.blockchain_info %}
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-link me-1"></i>
Blockchain Information
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Blockchain:</strong> {{ asset.blockchain_info.blockchain }}</p>
<p><strong>Token ID:</strong> {{ asset.blockchain_info.token_id }}</p>
<p><strong>Contract Address:</strong>
<code class="blockchain-address">{{ asset.blockchain_info.contract_address }}</code>
</p>
</div>
<div class="col-md-6">
<p><strong>Owner Address:</strong>
<code class="blockchain-address">{{ asset.blockchain_info.owner_address }}</code>
</p>
{% if asset.blockchain_info.transaction_hash %}
<p><strong>Transaction Hash:</strong>
<code class="blockchain-address">{{ asset.blockchain_info.transaction_hash }}</code>
</p>
{% endif %}
{% if asset.blockchain_info.block_number %}
<p><strong>Block Number:</strong> {{ asset.blockchain_info.block_number }}</p>
{% endif %}
{% if asset.blockchain_info.timestamp %}
<p><strong>Timestamp:</strong> {{ asset.blockchain_info.timestamp }}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Valuation History Chart -->
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-line me-1"></i>
Valuation History
</div>
<div class="card-body">
{% if valuation_history and valuation_history|length > 0 %}
<canvas id="valuationChart" width="100%" height="40"></canvas>
{% else %}
<div class="alert alert-info">
No valuation history available for this asset.
</div>
{% endif %}
</div>
</div>
<!-- Valuation History Table -->
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-history me-1"></i>
Valuation History
</div>
<div class="card-body">
{% if asset.valuation_history and asset.valuation_history|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Value</th>
<th>Currency</th>
<th>Source</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for valuation in asset.valuation_history %}
<tr>
<td>{{ valuation.date }}</td>
<td>{{ valuation.value }}</td>
<td>{{ valuation.currency }}</td>
<td>{{ valuation.source }}</td>
<td>{% if valuation.notes %}{{ valuation.notes }}{% else %}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
No valuation history available for this asset.
</div>
{% endif %}
</div>
</div>
<!-- Transaction History -->
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-exchange-alt me-1"></i>
Transaction History
</div>
<div class="card-body">
{% if asset.transaction_history and asset.transaction_history|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Amount</th>
<th>Transaction Hash</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for transaction in asset.transaction_history %}
<tr>
<td>{{ transaction.date }}</td>
<td>{{ transaction.transaction_type }}</td>
<td>
{% if transaction.from_address %}
<code class="blockchain-address-small">{{ transaction.from_address }}</code>
{% else %}
N/A
{% endif %}
</td>
<td>
{% if transaction.to_address %}
<code class="blockchain-address-small">{{ transaction.to_address }}</code>
{% else %}
N/A
{% endif %}
</td>
<td>
{% if transaction.amount %}
{{ transaction.currency }}{{ transaction.amount }}
{% else %}
N/A
{% endif %}
</td>
<td>
{% if transaction.transaction_hash %}
<code class="blockchain-address-small">{{ transaction.transaction_hash }}</code>
{% else %}
N/A
{% endif %}
</td>
<td>{% if transaction.notes %}{{ transaction.notes }}{% else %}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
No transaction history available for this asset.
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Valuation Modal -->
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="valuationForm" method="post" action="/assets/{{ asset.id }}/valuation">
<div class="mb-3">
<label for="value" class="form-label">Value</label>
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
</div>
<div class="mb-3">
<label for="currency" class="form-label">Currency</label>
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
</div>
<div class="mb-3">
<label for="source" class="form-label">Source</label>
<input type="text" class="form-control" id="source" name="source" required>
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
</div>
</div>
</div>
</div>
<!-- Transaction Modal -->
<div class="modal fade" id="transactionModal" tabindex="-1" aria-labelledby="transactionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="transactionModalLabel">Record Transaction</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="transactionForm" method="post" action="/assets/{{ asset.id }}/transaction">
<div class="mb-3">
<label for="transaction_type" class="form-label">Transaction Type</label>
<select class="form-select" id="transaction_type" name="transaction_type" required>
<option value="Purchase">Purchase</option>
<option value="Sale">Sale</option>
<option value="Transfer">Transfer</option>
<option value="Mint">Mint</option>
<option value="Burn">Burn</option>
<option value="Licensing">Licensing</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-3">
<label for="from_address" class="form-label">From Address</label>
<input type="text" class="form-control" id="from_address" name="from_address">
</div>
<div class="mb-3">
<label for="to_address" class="form-label">To Address</label>
<input type="text" class="form-control" id="to_address" name="to_address">
</div>
<div class="mb-3">
<label for="amount" class="form-label">Amount</label>
<input type="number" class="form-control" id="amount" name="amount" step="0.01">
</div>
<div class="mb-3">
<label for="transaction_currency" class="form-label">Currency</label>
<input type="text" class="form-control" id="transaction_currency" name="currency" value="USD">
</div>
<div class="mb-3">
<label for="transaction_hash" class="form-label">Transaction Hash</label>
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
</div>
<div class="mb-3">
<label for="transaction_notes" class="form-label">Notes</label>
<textarea class="form-control" id="transaction_notes" name="notes" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveTransactionBtn">Save</button>
</div>
</div>
</div>
</div>
<!-- Status Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm" method="post" action="/assets/{{ asset.id }}/status/">
<div class="mb-3">
<label for="newStatus" class="form-label">New Status</label>
<select class="form-select" id="newStatus" name="status">
<option value="Active" {% if asset.status == "Active" %}selected{% endif %}>Active</option>
<option value="Locked" {% if asset.status == "Locked" %}selected{% endif %}>Locked</option>
<option value="ForSale" {% if asset.status == "For Sale" %}selected{% endif %}>For Sale</option>
<option value="Transferred" {% if asset.status == "Transferred" %}selected{% endif %}>Transferred</option>
<option value="Archived" {% if asset.status == "Archived" %}selected{% endif %}>Archived</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Valuation History Chart
{% if valuation_history and valuation_history|length > 0 %}
const ctx = document.getElementById('valuationChart');
const dates = [
{% for point in valuation_history %}
"{{ point.date }}"{% if not loop.last %},{% endif %}
{% endfor %}
];
const values = [
{% for point in valuation_history %}
{{ point.value }}{% if not loop.last %},{% endif %}
{% endfor %}
];
new Chart(ctx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Valuation ({{ valuation_history[0].currency }})',
data: values,
lineTension: 0.3,
backgroundColor: "rgba(78, 115, 223, 0.05)",
borderColor: "rgba(78, 115, 223, 1)",
pointRadius: 3,
pointBackgroundColor: "rgba(78, 115, 223, 1)",
pointBorderColor: "rgba(78, 115, 223, 1)",
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
pointHoverBorderColor: "rgba(78, 115, 223, 1)",
pointHitRadius: 10,
pointBorderWidth: 2,
fill: true
}],
},
options: {
maintainAspectRatio: false,
scales: {
x: {
grid: {
display: false,
drawBorder: false
},
ticks: {
maxTicksLimit: 7
}
},
y: {
ticks: {
maxTicksLimit: 5,
padding: 10,
callback: function(value, index, values) {
return '{{ valuation_history[0].currency }}' + value;
}
},
grid: {
color: "rgb(234, 236, 244)",
zeroLineColor: "rgb(234, 236, 244)",
drawBorder: false,
borderDash: [2],
zeroLineBorderDash: [2]
}
},
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: "rgb(255,255,255)",
bodyFontColor: "#858796",
titleMarginBottom: 10,
titleFontColor: '#6e707e',
titleFontSize: 14,
borderColor: '#dddfeb',
borderWidth: 1,
xPadding: 15,
yPadding: 15,
displayColors: false,
intersect: false,
mode: 'index',
caretPadding: 10,
callbacks: {
label: function(context) {
var label = context.dataset.label || '';
if (label) {
label += ': ';
}
label += '{{ valuation_history[0].currency }}' + context.parsed.y;
return label;
}
}
}
}
}
});
{% endif %}
// Form submission handlers
const saveValuationBtn = document.getElementById('saveValuationBtn');
if (saveValuationBtn) {
saveValuationBtn.addEventListener('click', function() {
document.getElementById('valuationForm').submit();
});
}
const saveTransactionBtn = document.getElementById('saveTransactionBtn');
if (saveTransactionBtn) {
saveTransactionBtn.addEventListener('click', function() {
document.getElementById('transactionForm').submit();
});
}
const saveStatusBtn = document.getElementById('saveStatusBtn');
if (saveStatusBtn) {
saveStatusBtn.addEventListener('click', function() {
const form = document.getElementById('statusForm');
const newStatus = document.getElementById('newStatus').value;
form.action = form.action + newStatus;
form.submit();
});
}
});
</script>
<style>
.asset-image {
max-height: 200px;
border-radius: 8px;
}
.asset-placeholder {
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 8px;
color: #6c757d;
}
.blockchain-address {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.85rem;
}
.blockchain-address-small {
display: inline-block;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,197 @@
{% extends "base.html" %}
{% block title %}Digital Assets Dashboard{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Digital Assets Dashboard</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active">Digital Assets</li>
</ol>
<!-- Stats Cards -->
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">
<h2 class="display-4">{{ stats.total_assets }}</h2>
<p class="mb-0">Total Assets</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/assets/list">View All Assets</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">
<h2 class="display-4">${{ stats.total_value }}</h2>
<p class="mb-0">Total Valuation</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/assets/list">View Details</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">
<h2 class="display-4">{{ stats.assets_by_status.Active }}</h2>
<p class="mb-0">Active Assets</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/assets/list">View Active Assets</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-danger text-white mb-4">
<div class="card-body">
<h2 class="display-4">0</h2>
<p class="mb-0">Pending Transactions</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/assets/list">View Transactions</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
</div>
<!-- Recent Assets Table -->
<div class="row mt-4">
<div class="col-12">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-table me-1"></i>
Recent Assets
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Valuation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for asset in recent_assets %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if asset.asset_type == "Token" %}
<i class="bi bi-coin me-2 text-warning"></i>
{% elif asset.asset_type == "Artwork" %}
<i class="bi bi-image me-2 text-primary"></i>
{% elif asset.asset_type == "Real Estate" %}
<i class="bi bi-building me-2 text-success"></i>
{% elif asset.asset_type == "Intellectual Property" %}
<i class="bi bi-file-earmark-text me-2 text-info"></i>
{% elif asset.asset_type == "Share" %}
<i class="bi bi-graph-up me-2 text-danger"></i>
{% elif asset.asset_type == "Bond" %}
<i class="bi bi-cash-stack me-2 text-secondary"></i>
{% elif asset.asset_type == "Commodity" %}
<i class="bi bi-box me-2 text-dark"></i>
{% else %}
<i class="bi bi-question-circle me-2"></i>
{% endif %}
{{ asset.name }}
</div>
</td>
<td>{{ asset.asset_type }}</td>
<td>
<span class="badge {% if asset.status == 'Active' %}bg-success{% elif asset.status == 'Locked' %}bg-warning{% elif asset.status == 'For Sale' %}bg-info{% elif asset.status == 'Transferred' %}bg-secondary{% else %}bg-dark{% endif %}">
{{ asset.status }}
</span>
</td>
<td>
{% if asset.current_valuation %}
${{ asset.current_valuation }}
{% else %}
<span class="text-muted">Not valued</span>
{% endif %}
</td>
<td>
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<a href="/assets/list" class="btn btn-primary">View All Assets</a>
</div>
</div>
</div>
</div>
<!-- Asset Types Distribution -->
<div class="row mt-4">
<div class="col-12">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-pie-chart me-1"></i>
Asset Types Distribution
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Asset Type</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{% for asset_type in assets_by_type %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if asset_type.type == "Token" %}
<i class="bi bi-coin me-2 text-warning"></i>
{% elif asset_type.type == "Artwork" %}
<i class="bi bi-image me-2 text-primary"></i>
{% elif asset_type.type == "Real Estate" %}
<i class="bi bi-building me-2 text-success"></i>
{% elif asset_type.type == "Intellectual Property" %}
<i class="bi bi-file-earmark-text me-2 text-info"></i>
{% elif asset_type.type == "Share" %}
<i class="bi bi-graph-up me-2 text-danger"></i>
{% elif asset_type.type == "Bond" %}
<i class="bi bi-cash-stack me-2 text-secondary"></i>
{% elif asset_type.type == "Commodity" %}
<i class="bi bi-box me-2 text-dark"></i>
{% else %}
<i class="bi bi-question-circle me-2"></i>
{% endif %}
{{ asset_type.type }}
</div>
</td>
<td>{{ asset_type.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/defi.js"></script>

View File

@@ -0,0 +1,286 @@
{% extends "base.html" %}
{% block title %}Digital Assets List{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Digital Assets</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
<li class="breadcrumb-item active">All Assets</li>
</ol>
<!-- Filters -->
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-filter me-1"></i>
Filter Assets
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="assetTypeFilter" class="form-label">Asset Type</label>
<select class="form-select" id="assetTypeFilter">
<option value="all">All Types</option>
<option value="Artwork">Artwork</option>
<option value="Token">Token</option>
<option value="RealEstate">Real Estate</option>
<option value="Commodity">Commodity</option>
<option value="Share">Share</option>
<option value="Bond">Bond</option>
<option value="IntellectualProperty">Intellectual Property</option>
<option value="Other">Other</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Status</label>
<select class="form-select" id="statusFilter">
<option value="all">All Statuses</option>
<option value="Active">Active</option>
<option value="Locked">Locked</option>
<option value="ForSale">For Sale</option>
<option value="Transferred">Transferred</option>
<option value="Archived">Archived</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="valuationFilter" class="form-label">Valuation</label>
<select class="form-select" id="valuationFilter">
<option value="all">All Valuations</option>
<option value="under1000">Under $1,000</option>
<option value="1000to10000">$1,000 - $10,000</option>
<option value="10000to100000">$10,000 - $100,000</option>
<option value="over100000">Over $100,000</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput" placeholder="Search by name or description">
</div>
</div>
<div class="row">
<div class="col-12">
<button id="applyFilters" class="btn btn-primary">Apply Filters</button>
<button id="resetFilters" class="btn btn-secondary">Reset</button>
</div>
</div>
</div>
</div>
<!-- Assets Table -->
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-table me-1"></i>
All Digital Assets
</div>
<div>
<a href="/assets/create" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> Create New Asset
</a>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="assetsTable">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Owner</th>
<th>Valuation</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for asset in assets %}
<tr class="asset-row"
data-type="{{ asset.asset_type }}"
data-status="{{ asset.status }}"
data-valuation="{% if asset.current_valuation %}{{ asset.current_valuation }}{% else %}0{% endif %}">
<td>
{% if asset.image_url %}
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="asset-thumbnail me-2">
{% endif %}
{{ asset.name }}
</td>
<td>{{ asset.asset_type }}</td>
<td>
{% if asset.status == "Active" %}
<span class="badge bg-success">{{ asset.status }}</span>
{% elif asset.status == "For Sale" %}
<span class="badge bg-warning">{{ asset.status }}</span>
{% elif asset.status == "Locked" %}
<span class="badge bg-secondary">{{ asset.status }}</span>
{% elif asset.status == "Transferred" %}
<span class="badge bg-info">{{ asset.status }}</span>
{% elif asset.status == "Archived" %}
<span class="badge bg-danger">{{ asset.status }}</span>
{% else %}
<span class="badge bg-primary">{{ asset.status }}</span>
{% endif %}
</td>
<td>{{ asset.owner_name }}</td>
<td>
{% if asset.current_valuation %}
{{ asset.valuation_currency }}{{ asset.current_valuation }}
{% else %}
N/A
{% endif %}
</td>
<td>{{ asset.created_at }}</td>
<td>
<div class="btn-group" role="group">
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
<i class="fas fa-eye"></i>
</a>
{% if asset.status == "Active" %}
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#statusModal" data-asset-id="{{ asset.id }}">
<i class="fas fa-exchange-alt"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Status Change Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm" method="post" action="">
<div class="mb-3">
<label for="newStatus" class="form-label">New Status</label>
<select class="form-select" id="newStatus" name="status">
<option value="Active">Active</option>
<option value="Locked">Locked</option>
<option value="ForSale">For Sale</option>
<option value="Transferred">Transferred</option>
<option value="Archived">Archived</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Status modal functionality
const statusModal = document.getElementById('statusModal');
if (statusModal) {
statusModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const assetId = button.getAttribute('data-asset-id');
const form = document.getElementById('statusForm');
form.action = `/assets/${assetId}/status/`;
});
const saveStatusBtn = document.getElementById('saveStatusBtn');
saveStatusBtn.addEventListener('click', function() {
const form = document.getElementById('statusForm');
const newStatus = document.getElementById('newStatus').value;
form.action = form.action + newStatus;
form.submit();
});
}
// Filtering functionality
const applyFilters = document.getElementById('applyFilters');
if (applyFilters) {
applyFilters.addEventListener('click', function() {
filterAssets();
});
}
const resetFilters = document.getElementById('resetFilters');
if (resetFilters) {
resetFilters.addEventListener('click', function() {
document.getElementById('assetTypeFilter').value = 'all';
document.getElementById('statusFilter').value = 'all';
document.getElementById('valuationFilter').value = 'all';
document.getElementById('searchInput').value = '';
filterAssets();
});
}
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
filterAssets();
}
});
}
function filterAssets() {
const typeFilter = document.getElementById('assetTypeFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const valuationFilter = document.getElementById('valuationFilter').value;
const searchText = document.getElementById('searchInput').value.toLowerCase();
const rows = document.querySelectorAll('#assetsTable tbody tr');
rows.forEach(row => {
const type = row.getAttribute('data-type');
const status = row.getAttribute('data-status');
const valuation = parseFloat(row.getAttribute('data-valuation'));
const name = row.querySelector('td:first-child').textContent.toLowerCase();
let typeMatch = typeFilter === 'all' || type === typeFilter;
let statusMatch = statusFilter === 'all' || status === statusFilter;
let searchMatch = searchText === '' || name.includes(searchText);
let valuationMatch = true;
if (valuationFilter === 'under1000') {
valuationMatch = valuation < 1000;
} else if (valuationFilter === '1000to10000') {
valuationMatch = valuation >= 1000 && valuation < 10000;
} else if (valuationFilter === '10000to100000') {
valuationMatch = valuation >= 10000 && valuation < 100000;
} else if (valuationFilter === 'over100000') {
valuationMatch = valuation >= 100000;
}
if (typeMatch && statusMatch && valuationMatch && searchMatch) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
});
</script>
<style>
.asset-thumbnail {
width: 30px;
height: 30px;
object-fit: cover;
border-radius: 4px;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,373 @@
{% extends "base.html" %}
{% block title %}My Digital Assets{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">My Digital Assets</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
<li class="breadcrumb-item active">My Assets</li>
</ol>
<!-- Summary Cards -->
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">
<h2 class="display-4">{{ assets | length }}</h2>
<p class="mb-0">Total Assets</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">
{% set active_count = 0 %}
{% for asset in assets %}
{% if asset.status == "Active" %}
{% set active_count = active_count + 1 %}
{% endif %}
{% endfor %}
<h2 class="display-4">{{ active_count }}</h2>
<p class="mb-0">Active Assets</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">
{% set for_sale_count = 0 %}
{% for asset in assets %}
{% if asset.status == "For Sale" %}
{% set for_sale_count = for_sale_count + 1 %}
{% endif %}
{% endfor %}
<h2 class="display-4">{{ for_sale_count }}</h2>
<p class="mb-0">For Sale</p>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-info text-white mb-4">
<div class="card-body">
{% set total_value = 0 %}
{% for asset in assets %}
{% if asset.current_valuation %}
{% set total_value = total_value + asset.current_valuation %}
{% endif %}
{% endfor %}
<h2 class="display-4">${% if total_value %}{{ total_value }}{% else %}0.00{% endif %}</h2>
<p class="mb-0">Total Value</p>
</div>
</div>
</div>
</div>
<!-- Assets Table -->
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-table me-1"></i>
My Digital Assets
</div>
<div>
<a href="/assets/create" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> Create New Asset
</a>
</div>
</div>
</div>
<div class="card-body">
{% if assets and assets|length > 0 %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="myAssetsTable">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Valuation</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for asset in assets %}
<tr>
<td>
{% if asset.image_url %}
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="asset-thumbnail me-2">
{% endif %}
{{ asset.name }}
</td>
<td>{{ asset.asset_type }}</td>
<td>
{% if asset.status == "Active" %}
<span class="badge bg-success">{{ asset.status }}</span>
{% elif asset.status == "For Sale" %}
<span class="badge bg-warning">{{ asset.status }}</span>
{% elif asset.status == "Locked" %}
<span class="badge bg-secondary">{{ asset.status }}</span>
{% elif asset.status == "Transferred" %}
<span class="badge bg-info">{{ asset.status }}</span>
{% elif asset.status == "Archived" %}
<span class="badge bg-danger">{{ asset.status }}</span>
{% else %}
<span class="badge bg-primary">{{ asset.status }}</span>
{% endif %}
</td>
<td>
{% if asset.current_valuation %}
{{ asset.valuation_currency }}{{ asset.current_valuation }}
{% else %}
N/A
{% endif %}
</td>
<td>{{ asset.created_at }}</td>
<td>
<div class="btn-group" role="group">
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
<i class="fas fa-eye"></i>
</a>
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#statusModal" data-asset-id="{{ asset.id }}">
<i class="fas fa-exchange-alt"></i>
</button>
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#valuationModal" data-asset-id="{{ asset.id }}">
<i class="fas fa-dollar-sign"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p>You don't have any digital assets yet.</p>
<a href="/assets/create" class="btn btn-primary">Create Your First Asset</a>
</div>
{% endif %}
</div>
</div>
<!-- Asset Types Distribution -->
<div class="row">
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-pie me-1"></i>
Asset Types Distribution
</div>
<div class="card-body">
<canvas id="assetTypesChart" width="100%" height="40"></canvas>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-bar me-1"></i>
Asset Value Distribution
</div>
<div class="card-body">
<canvas id="assetValueChart" width="100%" height="40"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Status Change Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm" method="post" action="">
<div class="mb-3">
<label for="newStatus" class="form-label">New Status</label>
<select class="form-select" id="newStatus" name="status">
<option value="Active">Active</option>
<option value="Locked">Locked</option>
<option value="ForSale">For Sale</option>
<option value="Transferred">Transferred</option>
<option value="Archived">Archived</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Valuation Modal -->
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="valuationForm" method="post" action="">
<div class="mb-3">
<label for="value" class="form-label">Value</label>
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
</div>
<div class="mb-3">
<label for="currency" class="form-label">Currency</label>
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
</div>
<div class="mb-3">
<label for="source" class="form-label">Source</label>
<input type="text" class="form-control" id="source" name="source" required>
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Status modal functionality
const statusModal = document.getElementById('statusModal');
if (statusModal) {
statusModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const assetId = button.getAttribute('data-asset-id');
const form = document.getElementById('statusForm');
form.action = `/assets/${assetId}/status/`;
});
const saveStatusBtn = document.getElementById('saveStatusBtn');
saveStatusBtn.addEventListener('click', function() {
const form = document.getElementById('statusForm');
const newStatus = document.getElementById('newStatus').value;
form.action = form.action + newStatus;
form.submit();
});
}
// Valuation modal functionality
const valuationModal = document.getElementById('valuationModal');
if (valuationModal) {
valuationModal.addEventListener('show.bs.modal', function(event) {
const button = event.relatedTarget;
const assetId = button.getAttribute('data-asset-id');
const form = document.getElementById('valuationForm');
form.action = `/assets/${assetId}/valuation`;
});
const saveValuationBtn = document.getElementById('saveValuationBtn');
saveValuationBtn.addEventListener('click', function() {
document.getElementById('valuationForm').submit();
});
}
// Asset Types Chart
const assetTypesCtx = document.getElementById('assetTypesChart');
if (assetTypesCtx) {
// Count assets by type
const assetTypes = {};
{% for asset in assets %}
if (!assetTypes['{{ asset.asset_type }}']) {
assetTypes['{{ asset.asset_type }}'] = 0;
}
assetTypes['{{ asset.asset_type }}']++;
{% endfor %}
const typeLabels = Object.keys(assetTypes);
const typeCounts = Object.values(assetTypes);
new Chart(assetTypesCtx, {
type: 'pie',
data: {
labels: typeLabels,
datasets: [{
data: typeCounts,
backgroundColor: [
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e',
'#e74a3b', '#858796', '#5a5c69', '#2c9faf'
],
}],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
}
}
},
});
}
// Asset Value Chart
const assetValueCtx = document.getElementById('assetValueChart');
if (assetValueCtx) {
// Prepare data for assets with valuation
const assetNames = [];
const assetValues = [];
{% for asset in assets %}
{% if asset.current_valuation %}
assetNames.push('{{ asset.name }}');
assetValues.push({{ asset.current_valuation }});
{% endif %}
{% endfor %}
new Chart(assetValueCtx, {
type: 'bar',
data: {
labels: assetNames,
datasets: [{
label: 'Asset Value ($)',
data: assetValues,
backgroundColor: '#4e73df',
}],
},
options: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
},
});
}
});
</script>
<style>
.asset-thumbnail {
width: 30px;
height: 30px;
object-fit: cover;
border-radius: 4px;
}
</style>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Login - Actix MVC App{% endblock %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Register - Actix MVC App{% endblock %}
{% block title %}Register{% endblock %}
{% block content %}
<div class="row justify-content-center">

View File

@@ -3,48 +3,96 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Actix MVC App{% endblock %}</title>
<title>{% block title %}Zanzibar Digital Freezone{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/unpoly@3.7.2/unpoly.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
/* Minimal custom CSS that can't be achieved with Bootstrap classes */
body {
padding-top: 50px; /* Height of the fixed header */
}
.header {
height: 50px;
position: fixed;
top: 0;
width: 100%;
z-index: 1030;
}
.footer {
height: 40px;
line-height: 40px;
}
@media (min-width: 768px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px); /* Subtract header and footer height */
top: 50px; /* Position below header */
}
.main-content {
margin-left: 240px;
min-height: calc(100vh - 90px);
}
}
@media (max-width: 767.98px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px);
top: 50px;
left: -240px;
transition: left 0.3s ease;
z-index: 1020;
}
.sidebar.show {
left: 0;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">Actix MVC App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
<!-- Header - Full Width -->
<header class="header bg-dark text-white">
<div class="d-flex container-fluid justify-content-between align-items-center h-100">
<div class="align-items-center">
<button class="navbar-toggler d-md-none me-2" type="button" id="sidebarToggle" aria-label="Toggle navigation">
<i class="bi bi-list text-white"></i>
</button>
<h5 class="mb-0">Zanzibar Digital Freezone {% if entity_name %}| <span class="text-info">{{ entity_name }}</span>{% endif %}</h5>
</div>
<div class="d-none d-md-flex">
<ul class="navbar-nav flex-row">
<li class="nav-item mx-3">
<a class="nav-link text-white {% if active_page == 'about' %}active{% endif %}" target="_blank" href="https://info.ourworld.tf/zdfz">
About
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'about' %}active{% endif %}" href="/about">About</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'contact' %}active{% endif %}" href="/contact">Contact</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'tickets' %}active{% endif %}" href="/tickets">Support Tickets</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'calendar' %}active{% endif %}" href="/calendar">Calendar</a>
<li class="nav-item mx-3">
<a class="nav-link text-white {% if active_page == 'contact' %}active{% endif %}" href="/contact">
Contact
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
</div>
<div>
<ul class="navbar-nav flex-row">
{% if user and user.id %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ user.name }}
<a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> {{ user.name }}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="/tickets/new">New Ticket</a></li>
<li><a class="dropdown-item" href="/tickets/my">My Tickets</a></li>
<li><a class="dropdown-item" href="/my-tickets">My Tickets</a></li>
<li><a class="dropdown-item" href="/assets/my">My Assets</a></li>
<li><a class="dropdown-item" href="/marketplace/my">My Listings</a></li>
<li><a class="dropdown-item" href="/governance/my-votes">My Votes</a></li>
{% if user.role == "Admin" %}
<li><a class="dropdown-item" href="/admin">Admin Panel</a></li>
{% endif %}
@@ -53,39 +101,163 @@
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a>
<li class="nav-item me-2">
<a class="nav-link text-white {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'register' %}active{% endif %}" href="/register">Register</a>
<a class="nav-link text-white {% if active_page == 'register' %}active{% endif %}" href="/register">Register</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
</header>
<main class="container py-4">
{% block content %}{% endblock %}
</main>
<div class="d-flex flex-column min-vh-100">
<!-- Sidebar -->
<div class="sidebar bg-light shadow-sm border-end d-flex" id="sidebar">
<div class="py-2">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'home' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/">
<i class="bi bi-house-door me-2"></i> Home
</a>
</li>
<!-- Support Tickets link hidden
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'tickets' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/tickets">
<i class="bi bi-ticket-perforated me-2"></i> Support Tickets
</a>
</li>
-->
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'governance' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/governance">
<i class="bi bi-people me-2"></i> Governance
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'flows' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/flows">
<i class="bi bi-diagram-3 me-2"></i> Flows
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'contracts' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/contracts">
<i class="bi bi-file-earmark-text me-2"></i> Contracts
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'assets' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/assets">
<i class="bi bi-coin me-2"></i> Digital Assets
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'defi' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/defi">
<i class="bi bi-bank me-2"></i> DeFi Platform
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'company' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/company">
<i class="bi bi-building me-2"></i> Companies
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'marketplace' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/marketplace">
<i class="bi bi-shop me-2"></i> Marketplace
</a>
</li>
<!-- Markdown Editor link hidden
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'editor' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/editor">
<i class="bi bi-markdown me-2"></i> Markdown Editor
</a>
</li>
-->
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'calendar' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/calendar">
<i class="bi bi-calendar3 me-2"></i> Calendar
</a>
</li>
</ul>
</div>
</div>
<footer class="bg-dark text-white py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>Actix MVC App</h5>
<p>A Rust web application using Actix Web, Tera templates, and Bootstrap.</p>
<!-- Main Content -->
<div class="main-content flex-grow-1">
<!-- Page Content -->
<main class="py-3 w-100 d-block">
<div class="container-fluid">
{% block content %}{% endblock %}
</div>
<div class="col-md-6 text-md-end">
<p>&copy; {{ now(year=true) }} Actix MVC App. All rights reserved.</p>
</main>
</div>
<!-- Footer - Full Width -->
<footer class="footer bg-dark text-white">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-md-4 text-center text-md-start mb-2 mb-md-0">
<small>Convenience, Safety and Privacy</small>
</div>
<div class="col-md-4 text-center mb-2 mb-md-0">
<a class="text-white text-decoration-none mx-2" target="_blank" href="https://info.ourworld.tf/zdfz">About</a>
<span class="text-white">|</span>
<a class="text-white text-decoration-none mx-2" href="/contact">Contact</a>
</div>
<div class="col-md-4 text-center text-md-end">
<small>&copy; 2024 Zanzibar Digital Freezone</small>
</div>
</div>
</div>
</div>
</footer>
</footer>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
{% if success %}
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-success text-white">
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ success }}
</div>
</div>
{% endif %}
{% if error %}
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-danger text-white">
<strong class="me-auto">Error</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ error }}
</div>
</div>
{% endif %}
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/unpoly@3.7.2/unpoly.min.js"></script>
<script src="https://unpkg.com/unpoly@3.7.2/unpoly-bootstrap5.min.js"></script>
<script>
// Toggle sidebar on mobile
document.getElementById('sidebarToggle').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('show');
});
// Auto-hide toasts after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
const toasts = document.querySelectorAll('.toast.show');
toasts.forEach(toast => {
setTimeout(() => {
const bsToast = new bootstrap.Toast(toast);
bsToast.hide();
}, 5000);
});
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -3,31 +3,31 @@
{% block title %}New Calendar Event{% endblock %}
{% block content %}
<div class="container">
<div class="container-fluid">
<h1>Create New Event</h1>
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<form action="/calendar/new" method="post">
<form action="/calendar/events" 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,7 +38,14 @@
<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">
@@ -50,7 +57,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>
@@ -59,37 +66,106 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
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);
}
// 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();
});

View File

@@ -0,0 +1,417 @@
{% 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 %}

View File

@@ -0,0 +1,249 @@
{% 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 %}

View File

@@ -0,0 +1,111 @@
{% extends "base.html" %}
{% block title %}Company Management{% endblock %}
{% block head %}
{{ super() }}
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
</style>
{% endblock %}
{% block content %}
<!-- Toast notification for success messages -->
{% if success_message %}
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-header bg-success text-white">
<i class="bi bi-check-circle me-2"></i>
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ success_message }}
</div>
</div>
</div>
{% endif %}
<div class="container-fluid py-4">
<h2 class="mb-4">Company & Legal Entity Management (Freezone)</h2>
<!-- Company Management Tabs -->
<div class="mb-4">
<div class="card-body">
<ul class="nav nav-tabs" id="companyTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="true">
<i class="bi bi-building me-1"></i> Manage Companies
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">
<i class="bi bi-file-earmark-plus me-1"></i> Register New Company
</button>
</li>
</ul>
<div class="tab-content mt-3" id="companyTabsContent">
<div class="tab-pane fade show active" id="manage" role="tabpanel" aria-labelledby="manage-tab">
{% include "company/manage.html" %}
</div>
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
{% include "company/register.html" %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="/static/js/company.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Show toast if success message exists
const urlParams = new URLSearchParams(window.location.search);
const successMessage = urlParams.get('success');
if (successMessage) {
const toastEl = document.querySelector('.toast');
if (toastEl) {
const toastBody = toastEl.querySelector('.toast-body');
toastBody.textContent = decodeURIComponent(successMessage);
const toast = new bootstrap.Toast(toastEl);
toast.show();
// Auto-hide after 5 seconds
setTimeout(function() {
toast.hide();
}, 5000);
}
}
// Handle tab tracking in URL
const tabParam = urlParams.get('tab');
if (tabParam) {
const tabButton = document.querySelector(`button[data-bs-target="#${tabParam}"]`);
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
}
// Update URL when tab changes
const tabButtons = document.querySelectorAll('button[data-bs-toggle="tab"]');
tabButtons.forEach(function(button) {
button.addEventListener('shown.bs.tab', function(event) {
const targetId = event.target.getAttribute('data-bs-target').substring(1);
const url = new URL(window.location);
url.searchParams.set('tab', targetId);
window.history.replaceState({}, '', url);
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,210 @@
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<i class="bi bi-building me-1"></i> Your Companies
</div>
<div class="card-body">
<!-- Company list table -->
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Date Registered</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if companies and companies|length > 0 %}
{% for company in companies %}
<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>
<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>
</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>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Company Details Modal -->
<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>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="companyDetailsContent">
<!-- Company details will be loaded here -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">General Information</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th>Company Name:</th>
<td id="modal-company-name">Zanzibar Digital Solutions</td>
</tr>
<tr>
<th>Type:</th>
<td id="modal-company-type">Startup FZC</td>
</tr>
<tr>
<th>Registration Date:</th>
<td id="modal-registration-date">2025-04-01</td>
</tr>
<tr>
<th>Status:</th>
<td id="modal-status"><span class="badge bg-success">Active</span></td>
</tr>
<tr>
<th>Purpose:</th>
<td id="modal-purpose">Digital solutions and blockchain development</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Billing Information</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th>Plan:</th>
<td id="modal-plan">Startup FZC - $50/month</td>
</tr>
<tr>
<th>Next Billing:</th>
<td id="modal-next-billing">2025-06-01</td>
</tr>
<tr>
<th>Payment Method:</th>
<td id="modal-payment-method">Credit Card (****4582)</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Shareholders</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Name</th>
<th>Percentage</th>
</tr>
</thead>
<tbody id="modal-shareholders">
<tr>
<td>John Smith</td>
<td>60%</td>
</tr>
<tr>
<td>Sarah Johnson</td>
<td>40%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Contracts</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Contract</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="modal-contracts">
<tr>
<td>Articles of Incorporation</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
<tr>
<td>Terms & Conditions</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
<tr>
<td>Digital Asset Issuance</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</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>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,78 @@
{% 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 %}

View File

@@ -0,0 +1,89 @@
{% 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

View File

@@ -0,0 +1,21 @@
<ul class="nav nav-tabs" id="companyTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="true">
<i class="bi bi-building me-1"></i> Manage Companies
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">
<i class="bi bi-file-earmark-plus me-1"></i> Register New Company
</button>
</li>
</ul>
<div class="tab-content mt-4" id="companyTabsContent">
<div class="tab-pane fade show active" id="manage" role="tabpanel" aria-labelledby="manage-tab">
{% include "company/manage.html" %}
</div>
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
{% include "company/register.html" %}
</div>
</div>

View File

@@ -0,0 +1,355 @@
{% extends "base.html" %}
{% 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>
{% 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>
<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>
</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">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>General Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th style="width: 30%">Company Name:</th>
<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>
</tr>
<tr>
<th>Registration Date:</th>
<td>{{ incorporation_date_formatted }}</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>
{% else %}
<span class="badge bg-secondary">{{ company.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>
</tr>
</table>
</div>
</div>
</div>
<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>
</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>
</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>
</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>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-people me-2"></i>Shareholders</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{% if shareholders and shareholders|length > 0 %}
{% for shareholder in shareholders %}
<tr>
<td>{{ shareholder.name }}</td>
<td>{{ shareholder.percentage }}%</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>
</div>
</div>
<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>
</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>
{% 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>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('Company view page loaded');
});
</script>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Contact - Actix MVC App{% endblock %}
{% block title %}Contact - Zanzibar Digital Freezone{% endblock %}
{% block content %}
<div class="row">
@@ -37,15 +37,15 @@
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Email</h5>
<p class="card-text">info@example.com</p>
<p class="card-text">info@ourworld.tf</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">GitHub</h5>
<p class="card-text">github.com/example/actix-mvc-app</p>
<h5 class="card-title">Website</h5>
<p class="card-text">https://info.ourworld.tf/zdfz</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,200 @@
{% 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 %}

View File

@@ -0,0 +1,128 @@
{% 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

View File

@@ -0,0 +1,226 @@
{% extends "base.html" %}
{% block title %}All Contracts{% 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 active" aria-current="page">All Contracts</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">All 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
</a>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
</div>
<div class="card-body">
<form action="/contracts/list" 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="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="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>
</div>
</div>
<!-- Contract List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contracts</h5>
</div>
<div class="card-body">
{% if contracts and contracts | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Status</th>
<th>Created By</th>
<th>Signers</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</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 %}">
{{ contract.status }}
</span>
</td>
<td>{{ contract.created_by }}</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>
<div class="btn-group">
<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">
<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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-file-earmark-text fs-1 text-muted"></i>
<p class="mt-3 text-muted">No contracts found</p>
<a href="/contracts/create" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
{% endif %}
</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('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 %}

View File

@@ -0,0 +1,286 @@
{% extends "base.html" %}
{% block title %}Create New 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 active" aria-current="page">Create New Contract</li>
</ol>
</nav>
<h1 class="display-5 mb-3">Create New 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/create" 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" 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>
<option value="" selected disabled>Select a contract type</option>
{% for type in contract_types %}
<option value="{{ type }}">{{ 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></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>
</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>
</div>
</form>
</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>
<ul>
<li>Add signers who need to approve the contract</li>
<li>Edit the contract content</li>
<li>Send the contract for signatures</li>
<li>Track the signing progress</li>
</ul>
<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>
</div>
<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')">
Non-Disclosure Agreement
</button>
<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')">
Employment Contract
</button>
<button type="button" class="list-group-item list-group-item-action"
onclick="loadTemplate('sla')">
Service Level Agreement
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function loadTemplate(type) {
// In a real application, this would load template content from the server
let title = '';
let description = '';
let content = '';
let contractType = '';
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`;
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`;
break;
case 'employment':
title = 'Employment Contract';
description = 'Standard employment agreement';
contractType = 'Employment Contract';
content = 'This Employment Agreement (the "Agreement") is made and entered into as of [DATE] by and between [EMPLOYER] and [EMPLOYEE].\n\n1. Position and Duties\n2. Compensation and Benefits\n3. Term and Termination\n...';
break;
case 'sla':
title = 'Service Level Agreement';
description = 'Agreement defining service levels and metrics';
contractType = 'Service Level Agreement';
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) {
selectElement.selectedIndex = i;
break;
}
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% 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 %}

View File

@@ -0,0 +1,349 @@
{% extends "base.html" %}
{% block title %}Contracts Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-3">Contracts Dashboard</h1>
<p class="lead">Manage legal agreements and contracts across your organization.</p>
</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>
</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>
</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>
</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>
</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>
</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>
</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">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Quick Actions</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
<a href="/contracts/list" class="btn btn-outline-secondary">
<i class="bi bi-list me-1"></i> View All Contracts
</a>
<a href="/contracts/my-contracts" class="btn btn-outline-secondary">
<i class="bi bi-person me-1"></i> My Contracts
</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Pending Signature Contracts -->
{% if pending_signature_contracts and pending_signature_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Pending Signature ({{ pending_signature_contracts|length }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Pending Signers</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in pending_signature_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.pending_signers }} of {{ contract.signers|length }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Draft Contracts -->
{% if draft_contracts and draft_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Draft Contracts</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in draft_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<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">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</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 %}

View File

@@ -0,0 +1,10 @@
{% macro render_toc(items, section_param) %}
{% for item in items %}
<a href="?section={{ item.file }}" class="list-group-item list-group-item-action{% if section_param == item.file %} active{% endif %}">{{ item.title }}</a>
{% if item.children and item.children | length > 0 %}
<div class="ms-3">
{{ self::render_toc(items=item.children, section_param=section_param) }}
</div>
{% endif %}
{% endfor %}
{% endmacro %}

View File

@@ -0,0 +1,477 @@
{% extends "base.html" %}
{% block title %}My Contracts{% 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 active" aria-current="page">My Contracts</li>
</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>
<div class="btn-group">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
</div>
</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>
<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>
</div>
</div>
</div>
<!-- Contract List -->
<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>
<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">
<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>
</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 %}">
<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>
</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 %}
{{ contract.status }}
</span>
</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">
<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">
<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>
</tr>
{% endfor %}
</tbody>
</table>
</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>
</div>
{% endif %}
</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:</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 %}

View File

@@ -0,0 +1,370 @@
{% 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 %}

Some files were not shown because too many files have changed in this diff Show More