...
This commit is contained in:
2
doc.vsh
2
doc.vsh
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import os
|
||||
|
||||
|
||||
1
lib/biz/planner/.gitignore
vendored
Normal file
1
lib/biz/planner/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.db
|
||||
303
lib/biz/planner/README.md
Normal file
303
lib/biz/planner/README.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Task/Project Management System with Integrated CRM
|
||||
|
||||
A comprehensive task and project management system with integrated CRM capabilities, built using V language and V's built-in ORM.
|
||||
|
||||
## Overview
|
||||
|
||||
This system provides a complete solution for:
|
||||
- **Project Management**: Projects, tasks, milestones with dependencies and time tracking
|
||||
- **Scrum Methodology**: Sprints, story points, velocity tracking, burndown charts
|
||||
- **Issue Tracking**: Bug tracking with severity levels and resolution workflow
|
||||
- **Team Management**: Capacity planning, skill tracking, and performance metrics
|
||||
- **CRM Integration**: Customer lifecycle management integrated with project work
|
||||
- **Communication**: Real-time chat system with threading and integrations
|
||||
- **Calendar/Scheduling**: Meeting management with recurrence and attendee tracking
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data Storage Strategy
|
||||
- **Root Objects**: Stored as JSON in database tables with matching names
|
||||
- **Incremental IDs**: Each root object has auto-incrementing integer IDs
|
||||
- **Targeted Indexing**: Additional indexes on frequently queried fields
|
||||
- **ORM Integration**: Uses V's built-in ORM for type-safe database operations
|
||||
|
||||
### Core Models
|
||||
|
||||
#### Foundation
|
||||
- [`BaseModel`](models/base.v) - Common fields and functionality for all entities
|
||||
- [`Enums`](models/enums.v) - Comprehensive status and type definitions
|
||||
- [`SubObjects`](models/subobjects.v) - Embedded objects like Contact, Address, TimeEntry
|
||||
|
||||
#### Business Objects
|
||||
- [`User`](models/user.v) - System users with roles, skills, and preferences
|
||||
- [`Customer`](models/customer.v) - CRM entities with contacts and project relationships
|
||||
- [`Project`](models/project.v) - Main project containers with budgets and timelines
|
||||
- [`Task`](models/task.v) - Work items with dependencies and time tracking
|
||||
- [`Sprint`](models/sprint.v) - Scrum sprints with velocity and burndown tracking
|
||||
- [`Milestone`](models/milestone.v) - Project goals with conditions and deliverables
|
||||
- [`Issue`](models/issue.v) - Problem tracking with severity and resolution workflow
|
||||
- [`Team`](models/team.v) - Groups with capacity planning and skill management
|
||||
- [`Agenda`](models/agenda.v) - Calendar events with recurrence and attendee management
|
||||
- [`Chat`](models/chat.v) - Communication channels with threading and integrations
|
||||
|
||||
## Using the ORM
|
||||
|
||||
### Database Setup
|
||||
|
||||
```v
|
||||
import db.sqlite
|
||||
import lib.biz.planner.examples
|
||||
|
||||
// Initialize SQLite database
|
||||
mut repo := examples.new_chat_repository('myapp.db')!
|
||||
|
||||
// Or PostgreSQL
|
||||
mut pg_repo := examples.new_chat_repository_pg('localhost', 5432, 'user', 'pass', 'mydb')!
|
||||
```
|
||||
|
||||
### Model Definitions
|
||||
|
||||
Models use V's ORM attributes for database mapping:
|
||||
|
||||
```v
|
||||
@[table: 'chat']
|
||||
pub struct ChatORM {
|
||||
pub mut:
|
||||
id int @[primary; sql: serial]
|
||||
name string @[nonull]
|
||||
description string
|
||||
chat_type string @[nonull]
|
||||
status string @[nonull]
|
||||
owner_id int @[nonull]
|
||||
project_id int
|
||||
created_at time.Time @[default: 'CURRENT_TIMESTAMP']
|
||||
updated_at time.Time @[default: 'CURRENT_TIMESTAMP']
|
||||
deleted_at time.Time
|
||||
}
|
||||
```
|
||||
|
||||
### CRUD Operations
|
||||
|
||||
#### Create
|
||||
```v
|
||||
// Create a new chat
|
||||
mut chat := repo.create_chat('Project Discussion', 'project_chat', owner_id, created_by)!
|
||||
println('Created chat with ID: ${chat.id}')
|
||||
```
|
||||
|
||||
#### Read
|
||||
```v
|
||||
// Get by ID
|
||||
chat := repo.get_chat(1)!
|
||||
|
||||
// List with filtering
|
||||
chats := repo.list_chats('project_chat', 'active', owner_id, 10, 0)!
|
||||
|
||||
// Search
|
||||
found := repo.search_chats('project', 5)!
|
||||
```
|
||||
|
||||
#### Update
|
||||
```v
|
||||
// Update chat
|
||||
mut chat := repo.get_chat(1)!
|
||||
chat.description = 'Updated description'
|
||||
repo.update_chat(mut chat, updated_by)!
|
||||
```
|
||||
|
||||
#### Delete (Soft Delete)
|
||||
```v
|
||||
// Soft delete
|
||||
repo.delete_chat(1, deleted_by)!
|
||||
```
|
||||
|
||||
### Advanced Queries
|
||||
|
||||
#### Filtering with Multiple Conditions
|
||||
```v
|
||||
// Using ORM's where clause
|
||||
chats := sql repo.db {
|
||||
select from ChatORM where chat_type == 'project_chat' &&
|
||||
status == 'active' && owner_id == user_id &&
|
||||
deleted_at == time.Time{}
|
||||
order by updated_at desc limit 10
|
||||
}!
|
||||
```
|
||||
|
||||
#### Joins and Relationships
|
||||
```v
|
||||
// Get chat with member count
|
||||
result := sql repo.db {
|
||||
select ChatORM.id, ChatORM.name, count(ChatMemberORM.id) as member_count
|
||||
from ChatORM
|
||||
inner join ChatMemberORM on ChatORM.id == ChatMemberORM.chat_id
|
||||
where ChatORM.deleted_at == time.Time{} && ChatMemberORM.status == 'active'
|
||||
group by ChatORM.id
|
||||
}!
|
||||
```
|
||||
|
||||
#### Aggregations
|
||||
```v
|
||||
// Count records
|
||||
total := sql repo.db {
|
||||
select count from ChatORM where deleted_at == time.Time{}
|
||||
}!
|
||||
|
||||
// Sum and averages
|
||||
stats := sql repo.db {
|
||||
select sum(message_count) as total_messages, avg(message_count) as avg_messages
|
||||
from ChatORM where status == 'active'
|
||||
}!
|
||||
```
|
||||
|
||||
### Working with Related Data
|
||||
|
||||
#### One-to-Many Relationships
|
||||
```v
|
||||
// Get chat and its messages
|
||||
chat := repo.get_chat(1)!
|
||||
messages := repo.get_messages(chat.id, 50, 0)!
|
||||
|
||||
// Get chat members
|
||||
members := repo.get_chat_members(chat.id)!
|
||||
```
|
||||
|
||||
#### Many-to-Many Relationships
|
||||
```v
|
||||
// Add user to chat
|
||||
member := repo.add_chat_member(chat_id, user_id, 'member', invited_by)!
|
||||
|
||||
// Remove user from chat
|
||||
repo.remove_chat_member(chat_id, user_id)!
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```v
|
||||
// Using transactions for data consistency
|
||||
sql repo.db {
|
||||
begin
|
||||
|
||||
// Create chat
|
||||
insert chat into ChatORM
|
||||
|
||||
// Add owner as admin
|
||||
insert member into ChatMemberORM
|
||||
|
||||
// Send welcome message
|
||||
insert message into MessageORM
|
||||
|
||||
commit
|
||||
}!
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
#### Indexing Strategy
|
||||
```v
|
||||
// Create indexes for frequently queried fields
|
||||
sql db {
|
||||
create index idx_chat_type on ChatORM(chat_type)
|
||||
create index idx_chat_owner on ChatORM(owner_id)
|
||||
create index idx_chat_project on ChatORM(project_id)
|
||||
create index idx_message_chat on MessageORM(chat_id)
|
||||
create index idx_message_created on MessageORM(created_at)
|
||||
}!
|
||||
```
|
||||
|
||||
#### Pagination
|
||||
```v
|
||||
// Efficient pagination
|
||||
page_size := 20
|
||||
offset := (page - 1) * page_size
|
||||
|
||||
chats := sql repo.db {
|
||||
select from ChatORM where deleted_at == time.Time{}
|
||||
order by updated_at desc
|
||||
limit page_size offset offset
|
||||
}!
|
||||
```
|
||||
|
||||
#### Selective Loading
|
||||
```v
|
||||
// Load only needed fields
|
||||
chat_summaries := sql repo.db {
|
||||
select id, name, message_count, last_activity
|
||||
from ChatORM where status == 'active'
|
||||
}!
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```v
|
||||
// Proper error handling
|
||||
chat := repo.get_chat(id) or {
|
||||
eprintln('Failed to get chat: ${err}')
|
||||
return error('Chat not found')
|
||||
}
|
||||
|
||||
// Validation before operations
|
||||
if chat.name.len == 0 {
|
||||
return error('Chat name cannot be empty')
|
||||
}
|
||||
```
|
||||
|
||||
### Migration and Schema Evolution
|
||||
|
||||
```v
|
||||
// Schema migrations
|
||||
pub fn migrate_v1_to_v2(mut db sqlite.DB) ! {
|
||||
// Add new columns
|
||||
sql db {
|
||||
alter table ChatORM add column archived_at time.Time
|
||||
alter table ChatORM add column file_count int default 0
|
||||
}!
|
||||
|
||||
// Create new indexes
|
||||
sql db {
|
||||
create index idx_chat_archived on ChatORM(archived_at)
|
||||
}!
|
||||
}
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
See [`examples/chat_orm_example.v`](examples/chat_orm_example.v) for a complete working example that demonstrates:
|
||||
|
||||
- Database initialization
|
||||
- Table creation with ORM
|
||||
- CRUD operations
|
||||
- Relationship management
|
||||
- Advanced querying
|
||||
- Performance optimization
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Transactions**: For operations that modify multiple tables
|
||||
2. **Implement Soft Deletes**: Set `deleted_at` instead of hard deletes
|
||||
3. **Index Strategically**: Add indexes on frequently queried fields
|
||||
4. **Validate Input**: Always validate data before database operations
|
||||
5. **Handle Errors**: Proper error handling for all database operations
|
||||
6. **Use Pagination**: For large result sets
|
||||
7. **Optimize Queries**: Select only needed fields and use appropriate filters
|
||||
|
||||
## Database Support
|
||||
|
||||
- **SQLite**: For development and small deployments
|
||||
- **PostgreSQL**: For production environments with high concurrency
|
||||
- **MySQL**: Supported through V's ORM (configuration needed)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- JSON storage allows flexible schema evolution
|
||||
- Targeted indexing on frequently queried fields
|
||||
- Pagination support for large datasets
|
||||
- Connection pooling for high-concurrency scenarios
|
||||
- Prepared statements for security and performance
|
||||
|
||||
## Security
|
||||
|
||||
- Parameterized queries prevent SQL injection
|
||||
- Soft deletes preserve audit trails
|
||||
- User-based access control through role permissions
|
||||
- Audit logging with created_by/updated_by tracking
|
||||
711
lib/biz/planner/examples/chat_orm_example.vsh
Executable file
711
lib/biz/planner/examples/chat_orm_example.vsh
Executable file
@@ -0,0 +1,711 @@
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
|
||||
import db.sqlite
|
||||
import db.pg
|
||||
import time
|
||||
// import lib.biz.planner.models
|
||||
|
||||
// Enums for better type safety
|
||||
pub enum ChatType {
|
||||
direct_message
|
||||
group_chat
|
||||
project_chat
|
||||
team_chat
|
||||
customer_chat
|
||||
support_chat
|
||||
announcement
|
||||
}
|
||||
|
||||
pub enum ChatStatus {
|
||||
active
|
||||
inactive
|
||||
archived
|
||||
deleted
|
||||
}
|
||||
|
||||
pub enum ChatVisibility {
|
||||
private
|
||||
public
|
||||
restricted
|
||||
}
|
||||
|
||||
pub enum MessageType {
|
||||
text
|
||||
image
|
||||
file
|
||||
audio
|
||||
video
|
||||
system
|
||||
notification
|
||||
}
|
||||
|
||||
pub enum MessagePriority {
|
||||
low
|
||||
normal
|
||||
high
|
||||
urgent
|
||||
}
|
||||
|
||||
pub enum MessageDeliveryStatus {
|
||||
pending
|
||||
sent
|
||||
delivered
|
||||
read
|
||||
failed
|
||||
}
|
||||
|
||||
pub enum MemberRole {
|
||||
owner
|
||||
admin
|
||||
moderator
|
||||
member
|
||||
guest
|
||||
}
|
||||
|
||||
pub enum MemberStatus {
|
||||
active
|
||||
inactive
|
||||
banned
|
||||
left
|
||||
}
|
||||
|
||||
// Parameter structs using @[params]
|
||||
@[params]
|
||||
pub struct ChatNewArgs {
|
||||
pub mut:
|
||||
name string
|
||||
description ?string
|
||||
chat_type ChatType
|
||||
visibility ChatVisibility = .private
|
||||
owner_id int
|
||||
project_id ?int
|
||||
team_id ?int
|
||||
customer_id ?int
|
||||
task_id ?int
|
||||
issue_id ?int
|
||||
milestone_id ?int
|
||||
sprint_id ?int
|
||||
agenda_id ?int
|
||||
created_by int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct MessageNewArgs {
|
||||
pub mut:
|
||||
chat_id int
|
||||
sender_id int
|
||||
content string
|
||||
message_type MessageType = .text
|
||||
thread_id ?int
|
||||
reply_to_id ?int
|
||||
priority MessagePriority = .normal
|
||||
scheduled_at ?u32
|
||||
expires_at ?u32
|
||||
created_by int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ChatMemberNewArgs {
|
||||
pub mut:
|
||||
chat_id int
|
||||
user_id int
|
||||
role MemberRole = .member
|
||||
invited_by ?int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ChatListArgs {
|
||||
pub mut:
|
||||
chat_type ChatType = ChatType.direct_message // default value, will be ignored if not set
|
||||
status ChatStatus = ChatStatus.active // default value, will be ignored if not set
|
||||
owner_id int
|
||||
limit int = 50
|
||||
offset int
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ChatSearchArgs {
|
||||
pub mut:
|
||||
search_term string
|
||||
limit int = 20
|
||||
}
|
||||
|
||||
// Chat model for ORM - simplified version with ORM attributes
|
||||
@[table: 'chat']
|
||||
pub struct ChatORM {
|
||||
pub mut:
|
||||
id int @[primary; sql: serial]
|
||||
name string
|
||||
description ?string
|
||||
chat_type ChatType
|
||||
status ChatStatus
|
||||
visibility ChatVisibility
|
||||
owner_id int
|
||||
project_id ?int
|
||||
team_id ?int
|
||||
customer_id ?int
|
||||
task_id ?int
|
||||
issue_id ?int
|
||||
milestone_id ?int
|
||||
sprint_id ?int
|
||||
agenda_id ?int
|
||||
last_activity u32
|
||||
message_count int
|
||||
file_count int
|
||||
archived_at u32
|
||||
created_at u32
|
||||
updated_at u32
|
||||
created_by int
|
||||
updated_by int
|
||||
version int
|
||||
deleted_at u32
|
||||
}
|
||||
|
||||
// Message model for ORM
|
||||
@[table: 'message']
|
||||
pub struct MessageORM {
|
||||
pub mut:
|
||||
id int @[primary; sql: serial]
|
||||
chat_id int
|
||||
sender_id int
|
||||
content string
|
||||
message_type MessageType
|
||||
thread_id ?int
|
||||
reply_to_id ?int
|
||||
edited_at u32
|
||||
edited_by ?int
|
||||
deleted_at u32
|
||||
deleted_by ?int
|
||||
pinned bool
|
||||
pinned_at u32
|
||||
pinned_by ?int
|
||||
forwarded_from ?int
|
||||
scheduled_at u32
|
||||
delivery_status MessageDeliveryStatus
|
||||
priority MessagePriority
|
||||
expires_at u32
|
||||
system_message bool
|
||||
bot_message bool
|
||||
external_id ?string
|
||||
created_at u32
|
||||
updated_at u32
|
||||
created_by int
|
||||
updated_by int
|
||||
version int
|
||||
}
|
||||
|
||||
// ChatMember model for ORM
|
||||
@[table: 'chat_member']
|
||||
pub struct ChatMemberORM {
|
||||
pub mut:
|
||||
id int @[primary; sql: serial]
|
||||
user_id int
|
||||
chat_id int
|
||||
role MemberRole
|
||||
joined_at u32
|
||||
last_read_at u32
|
||||
last_read_message_id ?int
|
||||
status MemberStatus
|
||||
invited_by ?int
|
||||
muted bool
|
||||
muted_until u32
|
||||
custom_title ?string
|
||||
created_at u32
|
||||
updated_at u32
|
||||
}
|
||||
|
||||
// ChatRepository using V ORM
|
||||
pub struct ChatRepository {
|
||||
mut:
|
||||
db sqlite.DB
|
||||
}
|
||||
|
||||
// PostgreSQL version
|
||||
pub struct ChatRepositoryPG {
|
||||
mut:
|
||||
db pg.DB
|
||||
}
|
||||
|
||||
// Initialize SQLite database with ORM
|
||||
pub fn new_chat_repository(db_path string) !ChatRepository {
|
||||
mut db := sqlite.connect(db_path)!
|
||||
|
||||
// Create tables using ORM
|
||||
sql db {
|
||||
create table ChatORM
|
||||
create table MessageORM
|
||||
create table ChatMemberORM
|
||||
}!
|
||||
|
||||
return ChatRepository{db: db}
|
||||
}
|
||||
|
||||
// Initialize PostgreSQL database with ORM
|
||||
pub fn new_chat_repository_pg(host string, port int, user string, password string, dbname string) !ChatRepositoryPG {
|
||||
mut db := pg.connect(host: host, port: port, user: user, password: password, dbname: dbname)!
|
||||
|
||||
// Create tables using ORM
|
||||
sql db {
|
||||
create table ChatORM
|
||||
create table MessageORM
|
||||
create table ChatMemberORM
|
||||
}!
|
||||
|
||||
return ChatRepositoryPG{db: db}
|
||||
}
|
||||
|
||||
// Create a new chat using ORM
|
||||
pub fn (mut repo ChatRepository) create_chat(args_ ChatNewArgs) !ChatORM {
|
||||
mut args := args_
|
||||
now := u32(time.now().unix())
|
||||
mut chat := ChatORM{
|
||||
name: args.name
|
||||
description: args.description
|
||||
chat_type: args.chat_type
|
||||
status: .active
|
||||
visibility: args.visibility
|
||||
owner_id: args.owner_id
|
||||
project_id: args.project_id
|
||||
team_id: args.team_id
|
||||
customer_id: args.customer_id
|
||||
task_id: args.task_id
|
||||
issue_id: args.issue_id
|
||||
milestone_id: args.milestone_id
|
||||
sprint_id: args.sprint_id
|
||||
agenda_id: args.agenda_id
|
||||
created_by: args.created_by
|
||||
updated_by: args.created_by
|
||||
created_at: now
|
||||
updated_at: now
|
||||
deleted_at: 0
|
||||
last_activity: 0
|
||||
message_count: 0
|
||||
file_count: 0
|
||||
archived_at: 0
|
||||
version: 1
|
||||
}
|
||||
|
||||
// Insert using ORM
|
||||
sql repo.db {
|
||||
insert chat into ChatORM
|
||||
}!
|
||||
|
||||
// Get the last inserted ID
|
||||
chat.id = repo.db.last_id()
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
// Get chat by ID using ORM
|
||||
pub fn (repo ChatRepository) get_chat(id int) !ChatORM {
|
||||
chat := sql repo.db {
|
||||
select from ChatORM where id == id && deleted_at == 0
|
||||
}!
|
||||
|
||||
if chat.len == 0 {
|
||||
return error('Chat not found')
|
||||
}
|
||||
|
||||
return chat[0]
|
||||
}
|
||||
|
||||
// Update chat using ORM
|
||||
pub fn (mut repo ChatRepository) update_chat(mut chat ChatORM, updated_by int) ! {
|
||||
chat.updated_at = u32(time.now().unix())
|
||||
chat.updated_by = updated_by
|
||||
chat.version++
|
||||
|
||||
sql repo.db {
|
||||
update ChatORM set name = chat.name, description = chat.description,
|
||||
status = chat.status, updated_at = chat.updated_at,
|
||||
updated_by = chat.updated_by, version = chat.version
|
||||
where id == chat.id
|
||||
}!
|
||||
}
|
||||
|
||||
// Delete chat (soft delete) using ORM
|
||||
pub fn (mut repo ChatRepository) delete_chat(id int, deleted_by int) ! {
|
||||
now := u32(time.now().unix())
|
||||
|
||||
sql repo.db {
|
||||
update ChatORM set deleted_at = now, updated_by = deleted_by, updated_at = now
|
||||
where id == id
|
||||
}!
|
||||
}
|
||||
|
||||
// List chats with filtering using ORM
|
||||
pub fn (repo ChatRepository) list_chats(args_ ChatListArgs) ![]ChatORM {
|
||||
mut args := args_
|
||||
mut chats := []ChatORM{}
|
||||
|
||||
if args.owner_id > 0 {
|
||||
chats = sql repo.db {
|
||||
select from ChatORM where owner_id == args.owner_id && deleted_at == 0
|
||||
order by updated_at desc limit args.limit offset args.offset
|
||||
}!
|
||||
} else {
|
||||
chats = sql repo.db {
|
||||
select from ChatORM where deleted_at == 0
|
||||
order by updated_at desc limit args.limit offset args.offset
|
||||
}!
|
||||
}
|
||||
|
||||
return chats
|
||||
}
|
||||
|
||||
// Search chats by name using ORM
|
||||
pub fn (repo ChatRepository) search_chats(args_ ChatSearchArgs) ![]ChatORM {
|
||||
mut args := args_
|
||||
chats := sql repo.db {
|
||||
select from ChatORM where name like '%${args.search_term}%' && deleted_at == 0
|
||||
order by updated_at desc limit args.limit
|
||||
}!
|
||||
|
||||
return chats
|
||||
}
|
||||
|
||||
// Get chats by project using ORM
|
||||
pub fn (repo ChatRepository) get_chats_by_project(project_id int) ![]ChatORM {
|
||||
chats := sql repo.db {
|
||||
select from ChatORM where project_id == project_id && deleted_at == 0
|
||||
order by updated_at desc
|
||||
}!
|
||||
|
||||
return chats
|
||||
}
|
||||
|
||||
// Get chats by team using ORM
|
||||
pub fn (repo ChatRepository) get_chats_by_team(team_id int) ![]ChatORM {
|
||||
chats := sql repo.db {
|
||||
select from ChatORM where team_id == team_id && deleted_at == 0
|
||||
order by updated_at desc
|
||||
}!
|
||||
|
||||
return chats
|
||||
}
|
||||
|
||||
// Count total chats using ORM
|
||||
pub fn (repo ChatRepository) count_chats() !int {
|
||||
result := sql repo.db {
|
||||
select count from ChatORM where deleted_at == 0
|
||||
}!
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Add member to chat using ORM
|
||||
pub fn (mut repo ChatRepository) add_chat_member(args_ ChatMemberNewArgs) !ChatMemberORM {
|
||||
mut args := args_
|
||||
now := u32(time.now().unix())
|
||||
mut member := ChatMemberORM{
|
||||
user_id: args.user_id
|
||||
chat_id: args.chat_id
|
||||
role: args.role
|
||||
invited_by: args.invited_by
|
||||
joined_at: now
|
||||
created_at: now
|
||||
updated_at: now
|
||||
last_read_at: 0
|
||||
status: .active
|
||||
muted: false
|
||||
muted_until: 0
|
||||
}
|
||||
|
||||
sql repo.db {
|
||||
insert member into ChatMemberORM
|
||||
}!
|
||||
|
||||
// Get the last inserted ID
|
||||
member.id = repo.db.last_id()
|
||||
|
||||
return member
|
||||
}
|
||||
|
||||
// Get chat members using ORM
|
||||
pub fn (repo ChatRepository) get_chat_members(chat_id int) ![]ChatMemberORM {
|
||||
members := sql repo.db {
|
||||
select from ChatMemberORM where chat_id == chat_id && status == MemberStatus.active
|
||||
order by joined_at
|
||||
}!
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
// Remove member from chat using ORM
|
||||
pub fn (mut repo ChatRepository) remove_chat_member(chat_id int, user_id int) ! {
|
||||
now := u32(time.now().unix())
|
||||
sql repo.db {
|
||||
update ChatMemberORM set status = MemberStatus.inactive, updated_at = now
|
||||
where chat_id == chat_id && user_id == user_id
|
||||
}!
|
||||
}
|
||||
|
||||
// Send message using ORM
|
||||
pub fn (mut repo ChatRepository) send_message(args_ MessageNewArgs) !MessageORM {
|
||||
mut args := args_
|
||||
now := u32(time.now().unix())
|
||||
mut message := MessageORM{
|
||||
chat_id: args.chat_id
|
||||
sender_id: args.sender_id
|
||||
content: args.content
|
||||
message_type: args.message_type
|
||||
thread_id: args.thread_id
|
||||
reply_to_id: args.reply_to_id
|
||||
priority: args.priority
|
||||
scheduled_at: args.scheduled_at or { 0 }
|
||||
expires_at: args.expires_at or { 0 }
|
||||
created_by: args.created_by
|
||||
updated_by: args.created_by
|
||||
created_at: now
|
||||
updated_at: now
|
||||
deleted_at: 0
|
||||
edited_at: 0
|
||||
pinned: false
|
||||
pinned_at: 0
|
||||
delivery_status: .sent
|
||||
system_message: false
|
||||
bot_message: false
|
||||
version: 1
|
||||
}
|
||||
|
||||
sql repo.db {
|
||||
insert message into MessageORM
|
||||
}!
|
||||
|
||||
// Get the last inserted ID
|
||||
message.id = repo.db.last_id()
|
||||
|
||||
// Update chat message count and last activity
|
||||
sql repo.db {
|
||||
update ChatORM set message_count = message_count + 1, last_activity = now, updated_at = now
|
||||
where id == args.chat_id
|
||||
}!
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// Get messages for chat using ORM
|
||||
pub fn (repo ChatRepository) get_messages(chat_id int, limit int, offset int) ![]MessageORM {
|
||||
messages := sql repo.db {
|
||||
select from MessageORM where chat_id == chat_id && deleted_at == 0
|
||||
order by created_at desc limit limit offset offset
|
||||
}!
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// Get message by ID using ORM
|
||||
pub fn (repo ChatRepository) get_message(id int) !MessageORM {
|
||||
message := sql repo.db {
|
||||
select from MessageORM where id == id && deleted_at == 0
|
||||
}!
|
||||
|
||||
if message.len == 0 {
|
||||
return error('Message not found')
|
||||
}
|
||||
|
||||
return message[0]
|
||||
}
|
||||
|
||||
// Edit message using ORM
|
||||
pub fn (mut repo ChatRepository) edit_message(id int, new_content string, edited_by int) ! {
|
||||
now := u32(time.now().unix())
|
||||
|
||||
sql repo.db {
|
||||
update MessageORM set content = new_content, edited_at = now,
|
||||
edited_by = edited_by, updated_at = now
|
||||
where id == id
|
||||
}!
|
||||
}
|
||||
|
||||
// Delete message using ORM
|
||||
pub fn (mut repo ChatRepository) delete_message(id int, deleted_by int) ! {
|
||||
now := u32(time.now().unix())
|
||||
|
||||
sql repo.db {
|
||||
update MessageORM set deleted_at = now, deleted_by = deleted_by, updated_at = now
|
||||
where id == id
|
||||
}!
|
||||
}
|
||||
|
||||
// Pin message using ORM
|
||||
pub fn (mut repo ChatRepository) pin_message(id int, pinned_by int) ! {
|
||||
now := u32(time.now().unix())
|
||||
|
||||
sql repo.db {
|
||||
update MessageORM set pinned = true, pinned_at = now,
|
||||
pinned_by = pinned_by, updated_at = now
|
||||
where id == id
|
||||
}!
|
||||
}
|
||||
|
||||
// Get pinned messages using ORM
|
||||
pub fn (repo ChatRepository) get_pinned_messages(chat_id int) ![]MessageORM {
|
||||
messages := sql repo.db {
|
||||
select from MessageORM where chat_id == chat_id && pinned == true && deleted_at == 0
|
||||
order by pinned_at desc
|
||||
}!
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// Mark messages as read using ORM
|
||||
pub fn (mut repo ChatRepository) mark_as_read(chat_id int, user_id int, message_id int) ! {
|
||||
now := u32(time.now().unix())
|
||||
|
||||
sql repo.db {
|
||||
update ChatMemberORM set last_read_at = now, last_read_message_id = message_id
|
||||
where chat_id == chat_id && user_id == user_id
|
||||
}!
|
||||
}
|
||||
|
||||
// Get unread count using ORM
|
||||
pub fn (repo ChatRepository) get_unread_count(chat_id int, user_id int) !int {
|
||||
// Get user's last read message ID
|
||||
member := sql repo.db {
|
||||
select from ChatMemberORM where chat_id == chat_id && user_id == user_id
|
||||
}!
|
||||
|
||||
if member.len == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
last_read_id := member[0].last_read_message_id or { 0 }
|
||||
|
||||
// Count messages after last read
|
||||
result := sql repo.db {
|
||||
select count from MessageORM where chat_id == chat_id &&
|
||||
id > last_read_id && deleted_at == 0 && system_message == false
|
||||
}!
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete all data from repository (removes all records from all tables)
|
||||
pub fn (mut repo ChatRepository) delete_all()! {
|
||||
sql repo.db {
|
||||
delete from MessageORM where id > 0
|
||||
}!
|
||||
|
||||
sql repo.db {
|
||||
delete from ChatMemberORM where id > 0
|
||||
}!
|
||||
|
||||
sql repo.db {
|
||||
delete from ChatORM where id > 0
|
||||
}!
|
||||
}
|
||||
|
||||
// Delete all data from PostgreSQL repository (removes all records from all tables)
|
||||
pub fn (mut repo ChatRepositoryPG) delete_all()! {
|
||||
sql repo.db {
|
||||
delete from MessageORM where id > 0
|
||||
}!
|
||||
|
||||
sql repo.db {
|
||||
delete from ChatMemberORM where id > 0
|
||||
}!
|
||||
|
||||
sql repo.db {
|
||||
delete from ChatORM where id > 0
|
||||
}!
|
||||
}
|
||||
|
||||
// Example usage function
|
||||
pub fn example_usage() ! {
|
||||
// Initialize repository
|
||||
mut repo := new_chat_repository('chat_example.db')!
|
||||
|
||||
// Create a new chat using the new parameter struct
|
||||
mut chat := repo.create_chat(
|
||||
name: 'Project Alpha Discussion'
|
||||
chat_type: .project_chat
|
||||
owner_id: 1
|
||||
created_by: 1
|
||||
)!
|
||||
println('Created chat: ${chat.name} with ID: ${chat.id}')
|
||||
|
||||
// Add members to chat using the new parameter struct
|
||||
member1 := repo.add_chat_member(
|
||||
chat_id: chat.id
|
||||
user_id: 2
|
||||
role: .member
|
||||
invited_by: 1
|
||||
)!
|
||||
member2 := repo.add_chat_member(
|
||||
chat_id: chat.id
|
||||
user_id: 3
|
||||
role: .moderator
|
||||
invited_by: 1
|
||||
)!
|
||||
println('Added members: ${member1.user_id}, ${member2.user_id}')
|
||||
|
||||
// Send messages using the new parameter struct
|
||||
msg1 := repo.send_message(
|
||||
chat_id: chat.id
|
||||
sender_id: 1
|
||||
content: 'Welcome to the project chat!'
|
||||
message_type: .text
|
||||
created_by: 1
|
||||
)!
|
||||
msg2 := repo.send_message(
|
||||
chat_id: chat.id
|
||||
sender_id: 2
|
||||
content: 'Thanks for adding me!'
|
||||
message_type: .text
|
||||
created_by: 2
|
||||
)!
|
||||
println('Sent messages: ${msg1.id}, ${msg2.id}')
|
||||
|
||||
// Debug: Check what's in the database
|
||||
all_messages := sql repo.db {
|
||||
select from MessageORM
|
||||
}!
|
||||
println('Debug: Total messages in DB: ${all_messages.len}')
|
||||
for i, msg in all_messages {
|
||||
println(' DB Message ${i + 1}: ID=${msg.id}, chat_id=${msg.chat_id}, content="${msg.content}", deleted_at=${msg.deleted_at}')
|
||||
}
|
||||
|
||||
// Get messages
|
||||
messages := repo.get_messages(chat.id, 10, 0)!
|
||||
println('Retrieved ${messages.len} messages')
|
||||
for i, msg in messages {
|
||||
println(' Message ${i + 1}: "${msg.content}" from user ${msg.sender_id}')
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
repo.mark_as_read(chat.id, 2, msg2.id)!
|
||||
|
||||
// Get unread count
|
||||
unread := repo.get_unread_count(chat.id, 3)!
|
||||
println('User 3 has ${unread} unread messages')
|
||||
|
||||
// Search chats using the new parameter struct
|
||||
found_chats := repo.search_chats(
|
||||
search_term: 'Alpha'
|
||||
limit: 5
|
||||
)!
|
||||
println('Found ${found_chats.len} chats matching "Alpha"')
|
||||
for i, found_chat in found_chats {
|
||||
println(' Chat ${i + 1}: "${found_chat.name}" (ID: ${found_chat.id})')
|
||||
}
|
||||
|
||||
// Pin a message
|
||||
repo.pin_message(msg1.id, 1)!
|
||||
|
||||
// Get pinned messages
|
||||
pinned := repo.get_pinned_messages(chat.id)!
|
||||
println('Found ${pinned.len} pinned messages')
|
||||
for i, pinned_msg in pinned {
|
||||
println(' Pinned message ${i + 1}: "${pinned_msg.content}"')
|
||||
}
|
||||
|
||||
// Test the delete_all method
|
||||
// println('Testing delete_all method...')
|
||||
// repo.delete_all()!
|
||||
// println('All data deleted successfully!')
|
||||
}
|
||||
|
||||
// Run the example
|
||||
example_usage() or { panic(err) }
|
||||
561
lib/biz/planner/examples/orm_instructions.md
Normal file
561
lib/biz/planner/examples/orm_instructions.md
Normal file
@@ -0,0 +1,561 @@
|
||||
|
||||
|
||||
# **ORM**
|
||||
|
||||
V has a powerful, concise ORM baked in! Create tables, insert records, manage relationships, all regardless of the DB driver you decide to use.
|
||||
|
||||
|
||||
## **Nullable**
|
||||
|
||||
For a nullable column, use an option field. If the field is non-option, the column will be defined with `NOT NULL` at table creation.
|
||||
|
||||
|
||||
```
|
||||
struct Foo {
|
||||
notnull string
|
||||
nullable ?string
|
||||
}
|
||||
```
|
||||
|
||||
## **Attributes**
|
||||
|
||||
|
||||
### **Structs**
|
||||
|
||||
|
||||
|
||||
* `[table: 'name']` explicitly sets the name of the table for the struct
|
||||
|
||||
|
||||
### **Fields**
|
||||
|
||||
|
||||
|
||||
* `[primary]` sets the field as the primary key
|
||||
* `[unique]` gives the field a `UNIQUE` constraint
|
||||
* `[unique: 'foo']` adds the field to a `UNIQUE` group
|
||||
* `[skip]` or `[sql: '-']` field will be skipped
|
||||
* `[sql: type]` where `type` is a V type such as `int` or `f64`
|
||||
* `[serial]` or `[sql: serial]` lets the DB backend choose a column type for an auto-increment field
|
||||
* `[sql: 'name']` sets a custom column name for the field
|
||||
* `[sql_type: 'SQL TYPE']` explicitly sets the type in SQL
|
||||
* `[default: 'raw_sql']` inserts `raw_sql` verbatim in a "DEFAULT" clause whencreating a new table, allowing for SQL functions like `CURRENT_TIME`. For raw strings, surround `raw_sql` with backticks (`).
|
||||
* `[fkey: 'parent_id']` sets foreign key for an field which holds an array
|
||||
|
||||
|
||||
## **Usage**
|
||||
|
||||
|
||||
[!NOTE] > For using the Function Call API for `orm`, please check <code>[Function Call API](https://modules.vlang.io/orm.html#function-call-api)</code>.
|
||||
|
||||
Here are a couple example structs showing most of the features outlined above.
|
||||
|
||||
|
||||
```
|
||||
import time
|
||||
|
||||
@[table: 'foos']
|
||||
struct Foo {
|
||||
id int @[primary; sql: serial]
|
||||
name string
|
||||
created_at time.Time @[default: 'CURRENT_TIME']
|
||||
updated_at ?string @[sql_type: 'TIMESTAMP']
|
||||
deleted_at ?time.Time
|
||||
children []Child @[fkey: 'parent_id']
|
||||
}
|
||||
|
||||
struct Child {
|
||||
id int @[primary; sql: serial]
|
||||
parent_id int
|
||||
name string
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
To use the ORM, there is a special interface that lets you use the structs and V itself in queries. This interface takes the database instance as an argument.
|
||||
|
||||
|
||||
```
|
||||
import db.sqlite
|
||||
|
||||
db := sqlite.connect(':memory:')!
|
||||
|
||||
sql db {
|
||||
// query; see below
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
When you need to reference the table, simply pass the struct itself.
|
||||
|
||||
|
||||
```
|
||||
import models.Foo
|
||||
|
||||
struct Bar {
|
||||
id int @[primary; sql: serial]
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table models.Foo
|
||||
create table Bar
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **Create & Drop Tables**
|
||||
|
||||
You can create and drop tables by passing the struct to `create table` and `drop table`.
|
||||
|
||||
|
||||
```
|
||||
import models.Foo
|
||||
|
||||
struct Bar {
|
||||
id int @[primary; sql: serial]
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table models.Foo
|
||||
drop table Bar
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **Insert Records**
|
||||
|
||||
To insert a record, create a struct and pass the variable to the query. Again, reference the struct as the table.
|
||||
|
||||
|
||||
```
|
||||
foo := Foo{
|
||||
name: 'abc'
|
||||
created_at: time.now()
|
||||
// updated_at defaults to none
|
||||
// deleted_at defaults to none
|
||||
children: [
|
||||
Child{
|
||||
name: 'abc'
|
||||
},
|
||||
Child{
|
||||
name: 'def'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
foo_id := sql db {
|
||||
insert foo into Foo
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
If the `id` field is marked as `sql: serial` and `primary`, the insert expression returns the database ID of the newly added object. Getting an ID of a newly added DB row is often useful.
|
||||
|
||||
When inserting, `[sql: serial]` fields, and fields with a `[default: 'raw_sql']` attribute, are not sent to the database when the value being sent is the default for the V struct field (e.g., 0 int, or an empty string). This allows the database to insert default values for auto-increment fields and where you have specified a default.
|
||||
|
||||
|
||||
### **Select**
|
||||
|
||||
You can select rows from the database by passing the struct as the table, and use V syntax and functions for expressions. Selecting returns an array of the results.
|
||||
|
||||
|
||||
```
|
||||
result := sql db {
|
||||
select from Foo where id == 1
|
||||
}!
|
||||
|
||||
foo := result.first()
|
||||
result := sql db {
|
||||
select from Foo where id > 1 && name != 'lasanha' limit 5
|
||||
}!
|
||||
result := sql db {
|
||||
select from Foo where id > 1 order by id
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **Update**
|
||||
|
||||
You can update fields in a row using V syntax and functions. Again, pass the struct as the table.
|
||||
|
||||
|
||||
```
|
||||
sql db {
|
||||
update Foo set updated_at = time.now() where name == 'abc' && updated_at is none
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
Note that `is none` and `!is none` can be used to select for NULL fields.
|
||||
|
||||
|
||||
### **Delete**
|
||||
|
||||
You can delete rows using V syntax and functions. Again, pass the struct as the table.
|
||||
|
||||
|
||||
```
|
||||
sql db {
|
||||
delete from Foo where id > 10
|
||||
}!
|
||||
```
|
||||
|
||||
|
||||
|
||||
### **time.Time Fields**
|
||||
|
||||
It's definitely useful to cast a field as `time.Time` so you can use V's built-in time functions; however, this is handled a bit differently than expected in the ORM. `time.Time` fields are created as integer columns in the database. Because of this, the usual time functions (`current_timestamp`, `NOW()`, etc) in SQL do not work as defaults.
|
||||
|
||||
|
||||
## **Example**
|
||||
|
||||
|
||||
```
|
||||
import db.pg
|
||||
|
||||
struct Member {
|
||||
id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
|
||||
name string
|
||||
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
|
||||
}
|
||||
|
||||
fn main() {
|
||||
db := pg.connect(pg.Config{
|
||||
host: 'localhost'
|
||||
port: 5432
|
||||
user: 'user'
|
||||
password: 'password'
|
||||
dbname: 'dbname'
|
||||
})!
|
||||
|
||||
defer {
|
||||
db.close()
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table Member
|
||||
}!
|
||||
|
||||
new_member := Member{
|
||||
name: 'John Doe'
|
||||
}
|
||||
|
||||
sql db {
|
||||
insert new_member into Member
|
||||
}!
|
||||
|
||||
selected_members := sql db {
|
||||
select from Member where name == 'John Doe' limit 1
|
||||
}!
|
||||
john_doe := selected_members.first()
|
||||
|
||||
sql db {
|
||||
update Member set name = 'Hitalo' where id == john_doe.id
|
||||
}!
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## **Function Call API**
|
||||
|
||||
You can utilize the `Function Call API` to work with `ORM`. It provides the capability to dynamically construct SQL statements. The Function Call API supports common operations such as `Create Table`/`Drop Table`/`Insert`/`Delete`/`Update`/`Select`, and offers convenient yet powerful features for constructing `WHERE` clauses, `SET` clauses, `SELECT` clauses, and more.
|
||||
|
||||
A complete example is available [here](https://github.com/vlang/v/blob/master/vlib/orm/orm_func_test.v).
|
||||
|
||||
Below, we illustrate its usage through several examples.
|
||||
|
||||
1. Define your struct with the same method definitions as before:
|
||||
|
||||
|
||||
```
|
||||
@[table: 'sys_users']
|
||||
struct User {
|
||||
id int @[primary;serial]
|
||||
name string
|
||||
age int
|
||||
role string
|
||||
status int
|
||||
salary int
|
||||
title string
|
||||
score int
|
||||
created_at ?time.Time @[sql_type: 'TIMESTAMP']
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
2. Create a database connection:
|
||||
|
||||
|
||||
```
|
||||
mut db := sqlite.connect(':memory:')!
|
||||
defer { db.close() or {} }
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Create a `QueryBuilder` (which also completes struct mapping):
|
||||
|
||||
|
||||
```
|
||||
mut qb := orm.new_query[User](db)
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Create a database table:
|
||||
|
||||
|
||||
```
|
||||
qb.create()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Insert multiple records into the table:
|
||||
|
||||
|
||||
```
|
||||
qb.insert_many(users)!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Delete records (note: `delete()` must follow `where()`):
|
||||
|
||||
|
||||
```
|
||||
qb.where('name = ?','John')!.delete()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Query records (you can specify fields of interest via `select`):
|
||||
|
||||
|
||||
```
|
||||
// Returns []User with only 'name' populated; other fields are zero values.
|
||||
only_names := qb.select('name')!.query()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Update records (note: `update()` must be placed last):
|
||||
|
||||
|
||||
```
|
||||
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Drop the table:
|
||||
|
||||
|
||||
```
|
||||
qb.drop()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Chainable method calls: Most Function Call API support chainable calls, allowing easy method chaining:
|
||||
|
||||
|
||||
```
|
||||
final_users :=
|
||||
qb
|
||||
.drop()!
|
||||
.create()!
|
||||
.insert_many(users)!
|
||||
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
|
||||
.where('age >= ?', 30)!.delete()!
|
||||
.query()!
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
1. Writing complex nested `WHERE` clauses: The API includes a built-in parser to handle intricate `WHERE` clause conditions. For example:
|
||||
|
||||
|
||||
```
|
||||
where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))', 2000, 30, '%employee%')!
|
||||
```
|
||||
|
||||
|
||||
Note the use of placeholders `?`. The conditional expressions support logical operators including `AND`, `OR`, `||`, and `&&`.
|
||||
|
||||
|
||||
## Constants [#](https://modules.vlang.io/orm.html#Constants)
|
||||
|
||||
|
||||
## fn new_query [#](https://modules.vlang.io/orm.html#new_query)
|
||||
|
||||
new_query create a new query object for struct `T`
|
||||
|
||||
|
||||
## fn orm_select_gen [#](https://modules.vlang.io/orm.html#orm_select_gen)
|
||||
|
||||
Generates an sql select stmt, from universal parameter orm - See SelectConfig q, num, qm, start_pos - see orm_stmt_gen where - See QueryData
|
||||
|
||||
|
||||
## fn orm_stmt_gen [#](https://modules.vlang.io/orm.html#orm_stmt_gen)
|
||||
|
||||
Generates an sql stmt, from universal parameter q - The quotes character, which can be different in every type, so it's variable num - Stmt uses nums at prepared statements (? or ?1) qm - Character for prepared statement (qm for question mark, as in sqlite) start_pos - When num is true, it's the start position of the counter
|
||||
|
||||
|
||||
## fn orm_table_gen [#](https://modules.vlang.io/orm.html#orm_table_gen)
|
||||
|
||||
Generates an sql table stmt, from universal parameter table - Table struct q - see orm_stmt_gen defaults - enables default values in stmt def_unique_len - sets default unique length for texts fields - See TableField sql_from_v - Function which maps type indices to sql type names alternative - Needed for msdb
|
||||
|
||||
|
||||
## interface Connection [#](https://modules.vlang.io/orm.html#Connection)
|
||||
|
||||
Interfaces gets called from the backend and can be implemented Since the orm supports arrays aswell, they have to be returned too. A row is represented as []Primitive, where the data is connected to the fields of the struct by their index. The indices are mapped with the SelectConfig.field array. This is the mapping for a struct. To have an array, there has to be an array of structs, basically [][]Primitive
|
||||
|
||||
Every function without last_id() returns an optional, which returns an error if present last_id returns the last inserted id of the db
|
||||
|
||||
|
||||
## type Primitive [#](https://modules.vlang.io/orm.html#Primitive)
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) reset [#](https://modules.vlang.io/orm.html#QueryBuilder[T].reset)
|
||||
|
||||
reset reset a query object, but keep the connection and table name
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) where [#](https://modules.vlang.io/orm.html#QueryBuilder[T].where)
|
||||
|
||||
where create a `where` clause, it will `AND` with previous `where` clause. valid token in the `condition` include: `field's names`, `operator`, `(`, `)`, `?`, `AND`, `OR`, `||`, `&&`, valid `operator` incldue: `=`, `!=`, `<>`, `>=`, `<=`, `>`, `<`, `LIKE`, `ILIKE`, `IS NULL`, `IS NOT NULL`, `IN`, `NOT IN` example: `where('(a > ? AND b <= ?) OR (c <> ? AND (x = ? OR y = ?))', a, b, c, x, y)`
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) or_where [#](https://modules.vlang.io/orm.html#QueryBuilder[T].or_where)
|
||||
|
||||
or_where create a `where` clause, it will `OR` with previous `where` clause.
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) order [#](https://modules.vlang.io/orm.html#QueryBuilder[T].order)
|
||||
|
||||
order create a `order` clause
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) limit [#](https://modules.vlang.io/orm.html#QueryBuilder[T].limit)
|
||||
|
||||
limit create a `limit` clause
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) offset [#](https://modules.vlang.io/orm.html#QueryBuilder[T].offset)
|
||||
|
||||
offset create a `offset` clause
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) select [#](https://modules.vlang.io/orm.html#QueryBuilder[T].select)
|
||||
|
||||
select create a `select` clause
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) set [#](https://modules.vlang.io/orm.html#QueryBuilder[T].set)
|
||||
|
||||
set create a `set` clause for `update`
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) query [#](https://modules.vlang.io/orm.html#QueryBuilder[T].query)
|
||||
|
||||
query start a query and return result in struct `T`
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) count [#](https://modules.vlang.io/orm.html#QueryBuilder[T].count)
|
||||
|
||||
count start a count query and return result
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) insert [#](https://modules.vlang.io/orm.html#QueryBuilder[T].insert)
|
||||
|
||||
insert insert a record into the database
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) insert_many [#](https://modules.vlang.io/orm.html#QueryBuilder[T].insert_many)
|
||||
|
||||
insert_many insert records into the database
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) update [#](https://modules.vlang.io/orm.html#QueryBuilder[T].update)
|
||||
|
||||
update update record(s) in the database
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) delete [#](https://modules.vlang.io/orm.html#QueryBuilder[T].delete)
|
||||
|
||||
delete delete record(s) in the database
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) create [#](https://modules.vlang.io/orm.html#QueryBuilder[T].create)
|
||||
|
||||
create create a table
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) drop [#](https://modules.vlang.io/orm.html#QueryBuilder[T].drop)
|
||||
|
||||
drop drop a table
|
||||
|
||||
|
||||
## fn (QueryBuilder[T]) last_id [#](https://modules.vlang.io/orm.html#QueryBuilder[T].last_id)
|
||||
|
||||
last_id returns the last inserted id of the db
|
||||
|
||||
|
||||
## enum MathOperationKind [#](https://modules.vlang.io/orm.html#MathOperationKind)
|
||||
|
||||
|
||||
## enum OperationKind [#](https://modules.vlang.io/orm.html#OperationKind)
|
||||
|
||||
|
||||
## enum OrderType [#](https://modules.vlang.io/orm.html#OrderType)
|
||||
|
||||
|
||||
## enum SQLDialect [#](https://modules.vlang.io/orm.html#SQLDialect)
|
||||
|
||||
|
||||
## enum StmtKind [#](https://modules.vlang.io/orm.html#StmtKind)
|
||||
|
||||
|
||||
## struct InfixType [#](https://modules.vlang.io/orm.html#InfixType)
|
||||
|
||||
|
||||
## struct Null [#](https://modules.vlang.io/orm.html#Null)
|
||||
|
||||
|
||||
## struct QueryBuilder [#](https://modules.vlang.io/orm.html#QueryBuilder)
|
||||
|
||||
|
||||
```
|
||||
@[heap]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## struct QueryData [#](https://modules.vlang.io/orm.html#QueryData)
|
||||
|
||||
Examples for QueryData in SQL: abc == 3 && b == 'test' => fields[abc, b]; data[3, 'test']; types[index of int, index of string]; kinds[.eq, .eq]; is_and[true]; Every field, data, type & kind of operation in the expr share the same index in the arrays is_and defines how they're addicted to each other either and or or parentheses defines which fields will be inside () auto_fields are indexes of fields where db should generate a value when absent in an insert
|
||||
|
||||
|
||||
## struct SelectConfig [#](https://modules.vlang.io/orm.html#SelectConfig)
|
||||
|
||||
table - Table struct is_count - Either the data will be returned or an integer with the count has_where - Select all or use a where expr has_order - Order the results order - Name of the column which will be ordered order_type - Type of order (asc, desc) has_limit - Limits the output data primary - Name of the primary field has_offset - Add an offset to the result fields - Fields to select types - Types to select
|
||||
|
||||
|
||||
## struct Table [#](https://modules.vlang.io/orm.html#Table)
|
||||
|
||||
|
||||
## struct TableField [#](https://modules.vlang.io/orm.html#TableField)
|
||||
615
lib/biz/planner/models/agenda.v
Normal file
615
lib/biz/planner/models/agenda.v
Normal file
@@ -0,0 +1,615 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Agenda represents a calendar event or meeting
|
||||
pub struct Agenda {
|
||||
BaseModel
|
||||
pub mut:
|
||||
title string @[required]
|
||||
description string
|
||||
agenda_type AgendaType
|
||||
status AgendaStatus
|
||||
priority Priority
|
||||
start_time time.Time
|
||||
end_time time.Time
|
||||
all_day bool
|
||||
location string
|
||||
virtual_link string
|
||||
organizer_id int // User who organized the event
|
||||
attendees []Attendee
|
||||
required_attendees []int // User IDs who must attend
|
||||
optional_attendees []int // User IDs who are optional
|
||||
resources []Resource // Rooms, equipment, etc.
|
||||
project_id int // Links to Project (optional)
|
||||
task_id int // Links to Task (optional)
|
||||
milestone_id int // Links to Milestone (optional)
|
||||
sprint_id int // Links to Sprint (optional)
|
||||
team_id int // Links to Team (optional)
|
||||
customer_id int // Links to Customer (optional)
|
||||
recurrence Recurrence
|
||||
reminders []Reminder
|
||||
agenda_items []AgendaItem
|
||||
attachments []Attachment
|
||||
notes string
|
||||
meeting_notes string
|
||||
action_items []ActionItem
|
||||
decisions []Decision
|
||||
recording_url string
|
||||
transcript string
|
||||
follow_up_tasks []int // Task IDs created from this meeting
|
||||
time_zone string
|
||||
visibility EventVisibility
|
||||
booking_type BookingType
|
||||
cost f64 // Cost of the meeting (room, catering, etc.)
|
||||
capacity int // Maximum attendees
|
||||
waiting_list []int // User IDs on waiting list
|
||||
tags []string
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// AgendaType for categorizing events
|
||||
pub enum AgendaType {
|
||||
meeting
|
||||
appointment
|
||||
call
|
||||
interview
|
||||
presentation
|
||||
training
|
||||
workshop
|
||||
conference
|
||||
social
|
||||
break
|
||||
travel
|
||||
focus_time
|
||||
review
|
||||
planning
|
||||
retrospective
|
||||
standup
|
||||
demo
|
||||
one_on_one
|
||||
all_hands
|
||||
client_meeting
|
||||
vendor_meeting
|
||||
}
|
||||
|
||||
// AgendaStatus for event lifecycle
|
||||
pub enum AgendaStatus {
|
||||
draft
|
||||
scheduled
|
||||
confirmed
|
||||
in_progress
|
||||
completed
|
||||
cancelled
|
||||
postponed
|
||||
no_show
|
||||
}
|
||||
|
||||
// EventVisibility for privacy settings
|
||||
pub enum EventVisibility {
|
||||
public
|
||||
private
|
||||
confidential
|
||||
team_only
|
||||
project_only
|
||||
}
|
||||
|
||||
// BookingType for different booking models
|
||||
pub enum BookingType {
|
||||
fixed
|
||||
flexible
|
||||
recurring
|
||||
tentative
|
||||
blocked
|
||||
}
|
||||
|
||||
// Attendee represents a meeting attendee
|
||||
pub struct Attendee {
|
||||
pub mut:
|
||||
user_id int
|
||||
agenda_id int
|
||||
attendance_type AttendanceType
|
||||
response_status ResponseStatus
|
||||
response_time time.Time
|
||||
response_note string
|
||||
actual_attendance bool
|
||||
check_in_time time.Time
|
||||
check_out_time time.Time
|
||||
role AttendeeRole
|
||||
permissions []string
|
||||
delegate_id int // User ID if someone else attends on their behalf
|
||||
cost f64 // Cost for this attendee (travel, accommodation, etc.)
|
||||
}
|
||||
|
||||
// AttendanceType for attendee requirements
|
||||
pub enum AttendanceType {
|
||||
required
|
||||
optional
|
||||
informational
|
||||
presenter
|
||||
facilitator
|
||||
note_taker
|
||||
}
|
||||
|
||||
// ResponseStatus for meeting responses
|
||||
pub enum ResponseStatus {
|
||||
pending
|
||||
accepted
|
||||
declined
|
||||
tentative
|
||||
no_response
|
||||
}
|
||||
|
||||
// AttendeeRole for meeting roles
|
||||
pub enum AttendeeRole {
|
||||
participant
|
||||
presenter
|
||||
facilitator
|
||||
note_taker
|
||||
observer
|
||||
decision_maker
|
||||
subject_matter_expert
|
||||
}
|
||||
|
||||
// Resource represents a bookable resource
|
||||
pub struct Resource {
|
||||
pub mut:
|
||||
id int
|
||||
name string
|
||||
resource_type ResourceType
|
||||
location string
|
||||
capacity int
|
||||
cost_per_hour f64
|
||||
booking_status ResourceStatus
|
||||
equipment []string
|
||||
requirements []string
|
||||
contact_person string
|
||||
booking_notes string
|
||||
}
|
||||
|
||||
// ResourceType for categorizing resources
|
||||
pub enum ResourceType {
|
||||
meeting_room
|
||||
conference_room
|
||||
phone_booth
|
||||
desk
|
||||
equipment
|
||||
vehicle
|
||||
catering
|
||||
av_equipment
|
||||
parking_space
|
||||
}
|
||||
|
||||
// ResourceStatus for resource availability
|
||||
pub enum ResourceStatus {
|
||||
available
|
||||
booked
|
||||
maintenance
|
||||
unavailable
|
||||
}
|
||||
|
||||
// Recurrence represents recurring event patterns
|
||||
pub struct Recurrence {
|
||||
pub mut:
|
||||
pattern RecurrencePattern
|
||||
interval int // Every N days/weeks/months
|
||||
days_of_week []int // 0=Sunday, 1=Monday, etc.
|
||||
day_of_month int // For monthly recurrence
|
||||
week_of_month int // First, second, third, fourth, last week
|
||||
months []int // For yearly recurrence
|
||||
end_type RecurrenceEndType
|
||||
end_date time.Time
|
||||
occurrence_count int
|
||||
exceptions []time.Time // Dates to skip
|
||||
modifications []RecurrenceModification
|
||||
}
|
||||
|
||||
// RecurrencePattern for different recurrence types
|
||||
pub enum RecurrencePattern {
|
||||
none
|
||||
daily
|
||||
weekly
|
||||
monthly
|
||||
yearly
|
||||
custom
|
||||
}
|
||||
|
||||
// RecurrenceEndType for when recurrence ends
|
||||
pub enum RecurrenceEndType {
|
||||
never
|
||||
on_date
|
||||
after_occurrences
|
||||
}
|
||||
|
||||
// RecurrenceModification for modifying specific occurrences
|
||||
pub struct RecurrenceModification {
|
||||
pub mut:
|
||||
original_date time.Time
|
||||
new_start_time time.Time
|
||||
new_end_time time.Time
|
||||
cancelled bool
|
||||
title_override string
|
||||
location_override string
|
||||
}
|
||||
|
||||
// AgendaItem represents an item on the meeting agenda
|
||||
pub struct AgendaItem {
|
||||
pub mut:
|
||||
id int
|
||||
agenda_id int
|
||||
title string
|
||||
description string
|
||||
item_type AgendaItemType
|
||||
presenter_id int
|
||||
duration_minutes int
|
||||
order_index int
|
||||
status AgendaItemStatus
|
||||
notes string
|
||||
attachments []Attachment
|
||||
action_items []ActionItem
|
||||
decisions []Decision
|
||||
}
|
||||
|
||||
// AgendaItemType for categorizing agenda items
|
||||
pub enum AgendaItemType {
|
||||
discussion
|
||||
presentation
|
||||
decision
|
||||
information
|
||||
brainstorming
|
||||
review
|
||||
planning
|
||||
update
|
||||
demo
|
||||
training
|
||||
break
|
||||
}
|
||||
|
||||
// AgendaItemStatus for tracking agenda item progress
|
||||
pub enum AgendaItemStatus {
|
||||
pending
|
||||
in_progress
|
||||
completed
|
||||
skipped
|
||||
deferred
|
||||
}
|
||||
|
||||
// Decision represents a decision made during a meeting
|
||||
pub struct Decision {
|
||||
pub mut:
|
||||
id int
|
||||
agenda_id int
|
||||
agenda_item_id int
|
||||
title string
|
||||
description string
|
||||
decision_type DecisionType
|
||||
decision_maker_id int
|
||||
participants []int // User IDs involved in decision
|
||||
rationale string
|
||||
alternatives []string
|
||||
impact string
|
||||
implementation_date time.Time
|
||||
review_date time.Time
|
||||
status DecisionStatus
|
||||
follow_up_tasks []int // Task IDs for implementation
|
||||
created_at time.Time
|
||||
created_by int
|
||||
}
|
||||
|
||||
// DecisionType for categorizing decisions
|
||||
pub enum DecisionType {
|
||||
strategic
|
||||
tactical
|
||||
operational
|
||||
technical
|
||||
financial
|
||||
personnel
|
||||
process
|
||||
product
|
||||
}
|
||||
|
||||
// DecisionStatus for tracking decision implementation
|
||||
pub enum DecisionStatus {
|
||||
pending
|
||||
approved
|
||||
rejected
|
||||
deferred
|
||||
implemented
|
||||
under_review
|
||||
}
|
||||
|
||||
// get_duration returns the event duration in minutes
|
||||
pub fn (a Agenda) get_duration() int {
|
||||
if a.start_time.unix == 0 || a.end_time.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((a.end_time.unix - a.start_time.unix) / 60)
|
||||
}
|
||||
|
||||
// is_past checks if the event is in the past
|
||||
pub fn (a Agenda) is_past() bool {
|
||||
return time.now() > a.end_time
|
||||
}
|
||||
|
||||
// is_current checks if the event is currently happening
|
||||
pub fn (a Agenda) is_current() bool {
|
||||
now := time.now()
|
||||
return now >= a.start_time && now <= a.end_time
|
||||
}
|
||||
|
||||
// is_upcoming checks if the event is in the future
|
||||
pub fn (a Agenda) is_upcoming() bool {
|
||||
return time.now() < a.start_time
|
||||
}
|
||||
|
||||
// get_time_until_start returns minutes until the event starts
|
||||
pub fn (a Agenda) get_time_until_start() int {
|
||||
if a.is_past() || a.is_current() {
|
||||
return 0
|
||||
}
|
||||
return int((a.start_time.unix - time.now().unix) / 60)
|
||||
}
|
||||
|
||||
// has_conflicts checks if this event conflicts with another
|
||||
pub fn (a Agenda) has_conflicts(other Agenda) bool {
|
||||
// Check if events overlap
|
||||
return a.start_time < other.end_time && a.end_time > other.start_time
|
||||
}
|
||||
|
||||
// get_attendee_count returns the number of attendees
|
||||
pub fn (a Agenda) get_attendee_count() int {
|
||||
return a.attendees.len
|
||||
}
|
||||
|
||||
// get_confirmed_attendees returns attendees who have accepted
|
||||
pub fn (a Agenda) get_confirmed_attendees() []Attendee {
|
||||
return a.attendees.filter(it.response_status == .accepted)
|
||||
}
|
||||
|
||||
// get_attendance_rate returns the percentage of attendees who actually attended
|
||||
pub fn (a Agenda) get_attendance_rate() f32 {
|
||||
if a.attendees.len == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
attended := a.attendees.filter(it.actual_attendance).len
|
||||
return f32(attended) / f32(a.attendees.len) * 100
|
||||
}
|
||||
|
||||
// add_attendee adds an attendee to the event
|
||||
pub fn (mut a Agenda) add_attendee(user_id int, attendance_type AttendanceType, role AttendeeRole, by_user_id int) {
|
||||
// Check if attendee already exists
|
||||
for i, attendee in a.attendees {
|
||||
if attendee.user_id == user_id {
|
||||
// Update existing attendee
|
||||
a.attendees[i].attendance_type = attendance_type
|
||||
a.attendees[i].role = role
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new attendee
|
||||
a.attendees << Attendee{
|
||||
user_id: user_id
|
||||
agenda_id: a.id
|
||||
attendance_type: attendance_type
|
||||
response_status: .pending
|
||||
role: role
|
||||
}
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_attendee removes an attendee from the event
|
||||
pub fn (mut a Agenda) remove_attendee(user_id int, by_user_id int) {
|
||||
for i, attendee in a.attendees {
|
||||
if attendee.user_id == user_id {
|
||||
a.attendees.delete(i)
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// respond_to_invitation responds to a meeting invitation
|
||||
pub fn (mut a Agenda) respond_to_invitation(user_id int, response ResponseStatus, note string, by_user_id int) {
|
||||
for i, mut attendee in a.attendees {
|
||||
if attendee.user_id == user_id {
|
||||
a.attendees[i].response_status = response
|
||||
a.attendees[i].response_time = time.now()
|
||||
a.attendees[i].response_note = note
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check_in marks an attendee as present
|
||||
pub fn (mut a Agenda) check_in(user_id int, by_user_id int) {
|
||||
for i, mut attendee in a.attendees {
|
||||
if attendee.user_id == user_id {
|
||||
a.attendees[i].actual_attendance = true
|
||||
a.attendees[i].check_in_time = time.now()
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check_out marks an attendee as leaving
|
||||
pub fn (mut a Agenda) check_out(user_id int, by_user_id int) {
|
||||
for i, mut attendee in a.attendees {
|
||||
if attendee.user_id == user_id {
|
||||
a.attendees[i].check_out_time = time.now()
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add_resource adds a resource to the event
|
||||
pub fn (mut a Agenda) add_resource(resource Resource, by_user_id int) {
|
||||
a.resources << resource
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_agenda_item adds an item to the meeting agenda
|
||||
pub fn (mut a Agenda) add_agenda_item(title string, description string, item_type AgendaItemType, presenter_id int, duration_minutes int, by_user_id int) {
|
||||
a.agenda_items << AgendaItem{
|
||||
id: a.agenda_items.len + 1
|
||||
agenda_id: a.id
|
||||
title: title
|
||||
description: description
|
||||
item_type: item_type
|
||||
presenter_id: presenter_id
|
||||
duration_minutes: duration_minutes
|
||||
order_index: a.agenda_items.len
|
||||
status: .pending
|
||||
}
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_agenda_item marks an agenda item as completed
|
||||
pub fn (mut a Agenda) complete_agenda_item(item_id int, notes string, by_user_id int) {
|
||||
for i, mut item in a.agenda_items {
|
||||
if item.id == item_id {
|
||||
a.agenda_items[i].status = .completed
|
||||
a.agenda_items[i].notes = notes
|
||||
a.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add_decision records a decision made during the meeting
|
||||
pub fn (mut a Agenda) add_decision(title string, description string, decision_type DecisionType, decision_maker_id int, participants []int, rationale string, by_user_id int) {
|
||||
a.decisions << Decision{
|
||||
id: a.decisions.len + 1
|
||||
agenda_id: a.id
|
||||
title: title
|
||||
description: description
|
||||
decision_type: decision_type
|
||||
decision_maker_id: decision_maker_id
|
||||
participants: participants
|
||||
rationale: rationale
|
||||
status: .pending
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// start_meeting starts the meeting
|
||||
pub fn (mut a Agenda) start_meeting(by_user_id int) {
|
||||
a.status = .in_progress
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// end_meeting ends the meeting
|
||||
pub fn (mut a Agenda) end_meeting(meeting_notes string, by_user_id int) {
|
||||
a.status = .completed
|
||||
a.meeting_notes = meeting_notes
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// cancel_meeting cancels the meeting
|
||||
pub fn (mut a Agenda) cancel_meeting(by_user_id int) {
|
||||
a.status = .cancelled
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// postpone_meeting postpones the meeting
|
||||
pub fn (mut a Agenda) postpone_meeting(new_start_time time.Time, new_end_time time.Time, by_user_id int) {
|
||||
a.status = .postponed
|
||||
a.start_time = new_start_time
|
||||
a.end_time = new_end_time
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_reminder adds a reminder for the event
|
||||
pub fn (mut a Agenda) add_reminder(reminder_type ReminderType, minutes_before int, by_user_id int) {
|
||||
a.reminders << Reminder{
|
||||
reminder_type: reminder_type
|
||||
minutes_before: minutes_before
|
||||
sent: false
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
a.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_cost calculates the total cost of the meeting
|
||||
pub fn (a Agenda) calculate_cost() f64 {
|
||||
mut total_cost := a.cost
|
||||
|
||||
// Add attendee costs
|
||||
for attendee in a.attendees {
|
||||
total_cost += attendee.cost
|
||||
}
|
||||
|
||||
// Add resource costs
|
||||
duration_hours := f64(a.get_duration()) / 60.0
|
||||
for resource in a.resources {
|
||||
total_cost += resource.cost_per_hour * duration_hours
|
||||
}
|
||||
|
||||
return total_cost
|
||||
}
|
||||
|
||||
// get_next_occurrence returns the next occurrence for recurring events
|
||||
pub fn (a Agenda) get_next_occurrence() ?time.Time {
|
||||
if a.recurrence.pattern == .none {
|
||||
return none
|
||||
}
|
||||
|
||||
// Simple implementation - in practice this would be more complex
|
||||
match a.recurrence.pattern {
|
||||
.daily {
|
||||
return time.Time{unix: a.start_time.unix + (86400 * a.recurrence.interval)}
|
||||
}
|
||||
.weekly {
|
||||
return time.Time{unix: a.start_time.unix + (86400 * 7 * a.recurrence.interval)}
|
||||
}
|
||||
.monthly {
|
||||
// Simplified - would need proper month calculation
|
||||
return time.Time{unix: a.start_time.unix + (86400 * 30 * a.recurrence.interval)}
|
||||
}
|
||||
else {
|
||||
return none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is_overbooked checks if the event has more attendees than capacity
|
||||
pub fn (a Agenda) is_overbooked() bool {
|
||||
return a.capacity > 0 && a.get_attendee_count() > a.capacity
|
||||
}
|
||||
|
||||
// get_effectiveness_score calculates meeting effectiveness
|
||||
pub fn (a Agenda) get_effectiveness_score() f32 {
|
||||
if a.status != .completed {
|
||||
return 0
|
||||
}
|
||||
|
||||
mut score := f32(1.0)
|
||||
|
||||
// Attendance rate (30% weight)
|
||||
attendance_rate := a.get_attendance_rate()
|
||||
score *= 0.3 + (0.7 * attendance_rate / 100)
|
||||
|
||||
// Agenda completion (40% weight)
|
||||
if a.agenda_items.len > 0 {
|
||||
completed_items := a.agenda_items.filter(it.status == .completed).len
|
||||
completion_rate := f32(completed_items) / f32(a.agenda_items.len)
|
||||
score *= 0.4 + (0.6 * completion_rate)
|
||||
}
|
||||
|
||||
// Decision making (30% weight)
|
||||
if a.decisions.len > 0 {
|
||||
approved_decisions := a.decisions.filter(it.status == .approved).len
|
||||
decision_rate := f32(approved_decisions) / f32(a.decisions.len)
|
||||
score *= 0.3 + (0.7 * decision_rate)
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
63
lib/biz/planner/models/base.v
Normal file
63
lib/biz/planner/models/base.v
Normal file
@@ -0,0 +1,63 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// BaseModel provides common fields and functionality for all root objects
|
||||
pub struct BaseModel {
|
||||
pub mut:
|
||||
id int @[primary; sql: serial]
|
||||
created_at time.Time @[sql_type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP']
|
||||
updated_at time.Time @[sql_type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP']
|
||||
created_by int // User ID who created this record
|
||||
updated_by int // User ID who last updated this record
|
||||
version int // For optimistic locking
|
||||
tags []string // Flexible tagging system for categorization
|
||||
metadata map[string]string // Extensible key-value data for custom fields
|
||||
is_active bool = true // Soft delete flag
|
||||
}
|
||||
|
||||
// update_timestamp updates the updated_at field and version for optimistic locking
|
||||
pub fn (mut base BaseModel) update_timestamp(user_id int) {
|
||||
base.updated_at = time.now()
|
||||
base.updated_by = user_id
|
||||
base.version++
|
||||
}
|
||||
|
||||
// add_tag adds a tag if it doesn't already exist
|
||||
pub fn (mut base BaseModel) add_tag(tag string) {
|
||||
if tag !in base.tags {
|
||||
base.tags << tag
|
||||
}
|
||||
}
|
||||
|
||||
// remove_tag removes a tag if it exists
|
||||
pub fn (mut base BaseModel) remove_tag(tag string) {
|
||||
base.tags = base.tags.filter(it != tag)
|
||||
}
|
||||
|
||||
// has_tag checks if a tag exists
|
||||
pub fn (base BaseModel) has_tag(tag string) bool {
|
||||
return tag in base.tags
|
||||
}
|
||||
|
||||
// set_metadata sets a metadata key-value pair
|
||||
pub fn (mut base BaseModel) set_metadata(key string, value string) {
|
||||
base.metadata[key] = value
|
||||
}
|
||||
|
||||
// get_metadata gets a metadata value by key
|
||||
pub fn (base BaseModel) get_metadata(key string) ?string {
|
||||
return base.metadata[key] or { none }
|
||||
}
|
||||
|
||||
// soft_delete marks the record as inactive instead of deleting it
|
||||
pub fn (mut base BaseModel) soft_delete(user_id int) {
|
||||
base.is_active = false
|
||||
base.update_timestamp(user_id)
|
||||
}
|
||||
|
||||
// restore reactivates a soft-deleted record
|
||||
pub fn (mut base BaseModel) restore(user_id int) {
|
||||
base.is_active = true
|
||||
base.update_timestamp(user_id)
|
||||
}
|
||||
699
lib/biz/planner/models/chat.v
Normal file
699
lib/biz/planner/models/chat.v
Normal file
@@ -0,0 +1,699 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Chat represents a communication channel or conversation
|
||||
pub struct Chat {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
description string
|
||||
chat_type ChatType
|
||||
status ChatStatus
|
||||
visibility ChatVisibility
|
||||
owner_id int // User who owns/created the chat
|
||||
members []ChatMember
|
||||
messages []Message
|
||||
project_id int // Links to Project (optional)
|
||||
team_id int // Links to Team (optional)
|
||||
customer_id int // Links to Customer (optional)
|
||||
task_id int // Links to Task (optional)
|
||||
issue_id int // Links to Issue (optional)
|
||||
milestone_id int // Links to Milestone (optional)
|
||||
sprint_id int // Links to Sprint (optional)
|
||||
agenda_id int // Links to Agenda (optional)
|
||||
settings ChatSettings
|
||||
integrations []ChatIntegration
|
||||
pinned_messages []int // Message IDs that are pinned
|
||||
archived_at time.Time
|
||||
last_activity time.Time
|
||||
message_count int
|
||||
file_count int
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// ChatType for categorizing chats
|
||||
pub enum ChatType {
|
||||
direct_message
|
||||
group_chat
|
||||
channel
|
||||
announcement
|
||||
support
|
||||
project_chat
|
||||
team_chat
|
||||
customer_chat
|
||||
incident_chat
|
||||
meeting_chat
|
||||
thread
|
||||
}
|
||||
|
||||
// ChatStatus for chat lifecycle
|
||||
pub enum ChatStatus {
|
||||
active
|
||||
archived
|
||||
locked
|
||||
deleted
|
||||
suspended
|
||||
}
|
||||
|
||||
// ChatVisibility for access control
|
||||
pub enum ChatVisibility {
|
||||
public
|
||||
private
|
||||
restricted
|
||||
invite_only
|
||||
}
|
||||
|
||||
// ChatMember represents a member of a chat
|
||||
pub struct ChatMember {
|
||||
pub mut:
|
||||
user_id int
|
||||
chat_id int
|
||||
role ChatRole
|
||||
permissions []ChatPermission
|
||||
joined_at time.Time
|
||||
last_read_at time.Time
|
||||
last_read_message_id int
|
||||
notification_settings NotificationSettings
|
||||
status MemberStatus
|
||||
invited_by int
|
||||
muted bool
|
||||
muted_until time.Time
|
||||
custom_title string
|
||||
}
|
||||
|
||||
// ChatRole for member roles in chat
|
||||
pub enum ChatRole {
|
||||
member
|
||||
moderator
|
||||
admin
|
||||
owner
|
||||
guest
|
||||
bot
|
||||
}
|
||||
|
||||
// ChatPermission for granular permissions
|
||||
pub enum ChatPermission {
|
||||
read_messages
|
||||
send_messages
|
||||
send_files
|
||||
send_links
|
||||
mention_all
|
||||
delete_messages
|
||||
edit_messages
|
||||
pin_messages
|
||||
invite_members
|
||||
remove_members
|
||||
manage_settings
|
||||
manage_integrations
|
||||
}
|
||||
|
||||
// Message represents a chat message
|
||||
pub struct Message {
|
||||
BaseModel
|
||||
pub mut:
|
||||
chat_id int
|
||||
sender_id int
|
||||
content string
|
||||
message_type MessageType
|
||||
thread_id int // For threaded conversations
|
||||
reply_to_id int // Message this is replying to
|
||||
mentions []int // User IDs mentioned in message
|
||||
attachments []Attachment
|
||||
reactions []Reaction
|
||||
edited_at time.Time
|
||||
edited_by int
|
||||
deleted_at time.Time
|
||||
deleted_by int
|
||||
pinned bool
|
||||
pinned_at time.Time
|
||||
pinned_by int
|
||||
forwarded_from int // Original message ID if forwarded
|
||||
scheduled_at time.Time // For scheduled messages
|
||||
delivery_status MessageDeliveryStatus
|
||||
read_by []MessageRead
|
||||
priority MessagePriority
|
||||
expires_at time.Time // For ephemeral messages
|
||||
rich_content RichContent
|
||||
system_message bool // Is this a system-generated message?
|
||||
bot_message bool // Is this from a bot?
|
||||
external_id string // ID from external system (Slack, Teams, etc.)
|
||||
}
|
||||
|
||||
// MessageType for categorizing messages
|
||||
pub enum MessageType {
|
||||
text
|
||||
file
|
||||
image
|
||||
video
|
||||
audio
|
||||
link
|
||||
code
|
||||
quote
|
||||
poll
|
||||
announcement
|
||||
system
|
||||
bot_response
|
||||
task_update
|
||||
issue_update
|
||||
project_update
|
||||
meeting_summary
|
||||
reminder
|
||||
}
|
||||
|
||||
// MessageDeliveryStatus for tracking message delivery
|
||||
pub enum MessageDeliveryStatus {
|
||||
sending
|
||||
sent
|
||||
delivered
|
||||
read
|
||||
failed
|
||||
}
|
||||
|
||||
// MessagePriority for message importance
|
||||
pub enum MessagePriority {
|
||||
low
|
||||
normal
|
||||
high
|
||||
urgent
|
||||
}
|
||||
|
||||
// MessageRead tracks who has read a message
|
||||
pub struct MessageRead {
|
||||
pub mut:
|
||||
user_id int
|
||||
message_id int
|
||||
read_at time.Time
|
||||
device string
|
||||
}
|
||||
|
||||
// Reaction represents an emoji reaction to a message
|
||||
pub struct Reaction {
|
||||
pub mut:
|
||||
id int
|
||||
message_id int
|
||||
user_id int
|
||||
emoji string
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
// RichContent for rich message formatting
|
||||
pub struct RichContent {
|
||||
pub mut:
|
||||
formatted_text string // HTML or markdown
|
||||
embeds []Embed
|
||||
buttons []ActionButton
|
||||
cards []Card
|
||||
polls []Poll
|
||||
}
|
||||
|
||||
// Embed for rich content embeds
|
||||
pub struct Embed {
|
||||
pub mut:
|
||||
title string
|
||||
description string
|
||||
url string
|
||||
thumbnail_url string
|
||||
image_url string
|
||||
video_url string
|
||||
author_name string
|
||||
author_url string
|
||||
color string
|
||||
fields []EmbedField
|
||||
footer_text string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// EmbedField for structured embed data
|
||||
pub struct EmbedField {
|
||||
pub mut:
|
||||
name string
|
||||
value string
|
||||
inline bool
|
||||
}
|
||||
|
||||
// ActionButton for interactive messages
|
||||
pub struct ActionButton {
|
||||
pub mut:
|
||||
id string
|
||||
label string
|
||||
style ButtonStyle
|
||||
action string
|
||||
url string
|
||||
confirmation string
|
||||
}
|
||||
|
||||
// ButtonStyle for button appearance
|
||||
pub enum ButtonStyle {
|
||||
default
|
||||
primary
|
||||
success
|
||||
warning
|
||||
danger
|
||||
link
|
||||
}
|
||||
|
||||
// Card for rich card content
|
||||
pub struct Card {
|
||||
pub mut:
|
||||
title string
|
||||
subtitle string
|
||||
text string
|
||||
image_url string
|
||||
actions []ActionButton
|
||||
facts []CardFact
|
||||
}
|
||||
|
||||
// CardFact for key-value pairs in cards
|
||||
pub struct CardFact {
|
||||
pub mut:
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
// Poll for interactive polls
|
||||
pub struct Poll {
|
||||
pub mut:
|
||||
id int
|
||||
question string
|
||||
options []PollOption
|
||||
multiple_choice bool
|
||||
anonymous bool
|
||||
expires_at time.Time
|
||||
created_by int
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
// PollOption for poll choices
|
||||
pub struct PollOption {
|
||||
pub mut:
|
||||
id int
|
||||
text string
|
||||
votes []PollVote
|
||||
vote_count int
|
||||
}
|
||||
|
||||
// PollVote for tracking poll votes
|
||||
pub struct PollVote {
|
||||
pub mut:
|
||||
user_id int
|
||||
option_id int
|
||||
voted_at time.Time
|
||||
}
|
||||
|
||||
// ChatSettings for chat configuration
|
||||
pub struct ChatSettings {
|
||||
pub mut:
|
||||
allow_guests bool
|
||||
require_approval bool
|
||||
message_retention_days int
|
||||
file_retention_days int
|
||||
max_members int
|
||||
slow_mode_seconds int
|
||||
profanity_filter bool
|
||||
link_preview bool
|
||||
emoji_reactions bool
|
||||
threading bool
|
||||
message_editing bool
|
||||
message_deletion bool
|
||||
file_uploads bool
|
||||
external_sharing bool
|
||||
read_receipts bool
|
||||
typing_indicators bool
|
||||
welcome_message string
|
||||
rules []string
|
||||
auto_moderation AutoModerationSettings
|
||||
}
|
||||
|
||||
// AutoModerationSettings for automated moderation
|
||||
pub struct AutoModerationSettings {
|
||||
pub mut:
|
||||
enabled bool
|
||||
spam_detection bool
|
||||
profanity_filter bool
|
||||
link_filtering bool
|
||||
caps_limit int
|
||||
rate_limit_messages int
|
||||
rate_limit_seconds int
|
||||
auto_timeout_duration int
|
||||
escalation_threshold int
|
||||
}
|
||||
|
||||
// NotificationSettings for member notification preferences
|
||||
pub struct NotificationSettings {
|
||||
pub mut:
|
||||
all_messages bool
|
||||
mentions_only bool
|
||||
direct_messages bool
|
||||
keywords []string
|
||||
mute_until time.Time
|
||||
email_notifications bool
|
||||
push_notifications bool
|
||||
desktop_notifications bool
|
||||
sound_enabled bool
|
||||
vibration_enabled bool
|
||||
}
|
||||
|
||||
// ChatIntegration for external service integrations
|
||||
pub struct ChatIntegration {
|
||||
pub mut:
|
||||
id int
|
||||
chat_id int
|
||||
integration_type IntegrationType
|
||||
name string
|
||||
description string
|
||||
webhook_url string
|
||||
api_key string
|
||||
settings map[string]string
|
||||
enabled bool
|
||||
created_by int
|
||||
created_at time.Time
|
||||
last_used time.Time
|
||||
error_count int
|
||||
last_error string
|
||||
}
|
||||
|
||||
// IntegrationType for different integrations
|
||||
pub enum IntegrationType {
|
||||
webhook
|
||||
slack
|
||||
teams
|
||||
discord
|
||||
telegram
|
||||
email
|
||||
sms
|
||||
jira
|
||||
github
|
||||
gitlab
|
||||
jenkins
|
||||
monitoring
|
||||
custom
|
||||
}
|
||||
|
||||
// get_unread_count returns unread message count for a user
|
||||
pub fn (c Chat) get_unread_count(user_id int) int {
|
||||
// Find member's last read message
|
||||
mut last_read_id := 0
|
||||
for member in c.members {
|
||||
if member.user_id == user_id {
|
||||
last_read_id = member.last_read_message_id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Count messages after last read
|
||||
return c.messages.filter(it.id > last_read_id && !it.system_message).len
|
||||
}
|
||||
|
||||
// is_member checks if a user is a member of the chat
|
||||
pub fn (c Chat) is_member(user_id int) bool {
|
||||
for member in c.members {
|
||||
if member.user_id == user_id && member.status == .active {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// has_permission checks if a user has a specific permission
|
||||
pub fn (c Chat) has_permission(user_id int, permission ChatPermission) bool {
|
||||
for member in c.members {
|
||||
if member.user_id == user_id && member.status == .active {
|
||||
return permission in member.permissions
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// get_member_role returns a user's role in the chat
|
||||
pub fn (c Chat) get_member_role(user_id int) ?ChatRole {
|
||||
for member in c.members {
|
||||
if member.user_id == user_id {
|
||||
return member.role
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// add_member adds a member to the chat
|
||||
pub fn (mut c Chat) add_member(user_id int, role ChatRole, permissions []ChatPermission, invited_by int, by_user_id int) {
|
||||
// Check if member already exists
|
||||
for i, member in c.members {
|
||||
if member.user_id == user_id {
|
||||
// Update existing member
|
||||
c.members[i].role = role
|
||||
c.members[i].permissions = permissions
|
||||
c.members[i].status = .active
|
||||
c.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new member
|
||||
c.members << ChatMember{
|
||||
user_id: user_id
|
||||
chat_id: c.id
|
||||
role: role
|
||||
permissions: permissions
|
||||
joined_at: time.now()
|
||||
invited_by: invited_by
|
||||
status: .active
|
||||
notification_settings: NotificationSettings{
|
||||
all_messages: true
|
||||
mentions_only: false
|
||||
direct_messages: true
|
||||
email_notifications: true
|
||||
push_notifications: true
|
||||
}
|
||||
}
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_member removes a member from the chat
|
||||
pub fn (mut c Chat) remove_member(user_id int, by_user_id int) {
|
||||
for i, member in c.members {
|
||||
if member.user_id == user_id {
|
||||
c.members[i].status = .inactive
|
||||
c.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send_message sends a message to the chat
|
||||
pub fn (mut c Chat) send_message(sender_id int, content string, message_type MessageType, thread_id int, reply_to_id int, mentions []int, attachments []Attachment, by_user_id int) int {
|
||||
message := Message{
|
||||
id: c.messages.len + 1
|
||||
chat_id: c.id
|
||||
sender_id: sender_id
|
||||
content: content
|
||||
message_type: message_type
|
||||
thread_id: thread_id
|
||||
reply_to_id: reply_to_id
|
||||
mentions: mentions
|
||||
attachments: attachments
|
||||
delivery_status: .sent
|
||||
priority: .normal
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
|
||||
c.messages << message
|
||||
c.message_count++
|
||||
c.last_activity = time.now()
|
||||
c.update_timestamp(by_user_id)
|
||||
|
||||
return message.id
|
||||
}
|
||||
|
||||
// edit_message edits an existing message
|
||||
pub fn (mut c Chat) edit_message(message_id int, new_content string, by_user_id int) bool {
|
||||
for i, mut message in c.messages {
|
||||
if message.id == message_id {
|
||||
c.messages[i].content = new_content
|
||||
c.messages[i].edited_at = time.now()
|
||||
c.messages[i].edited_by = by_user_id
|
||||
c.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// delete_message deletes a message
|
||||
pub fn (mut c Chat) delete_message(message_id int, by_user_id int) bool {
|
||||
for i, mut message in c.messages {
|
||||
if message.id == message_id {
|
||||
c.messages[i].deleted_at = time.now()
|
||||
c.messages[i].deleted_by = by_user_id
|
||||
c.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pin_message pins a message
|
||||
pub fn (mut c Chat) pin_message(message_id int, by_user_id int) bool {
|
||||
for i, mut message in c.messages {
|
||||
if message.id == message_id {
|
||||
c.messages[i].pinned = true
|
||||
c.messages[i].pinned_at = time.now()
|
||||
c.messages[i].pinned_by = by_user_id
|
||||
if message_id !in c.pinned_messages {
|
||||
c.pinned_messages << message_id
|
||||
}
|
||||
c.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_reaction adds a reaction to a message
|
||||
pub fn (mut c Chat) add_reaction(message_id int, user_id int, emoji string, by_user_id int) {
|
||||
for i, mut message in c.messages {
|
||||
if message.id == message_id {
|
||||
// Check if user already reacted with this emoji
|
||||
for reaction in message.reactions {
|
||||
if reaction.user_id == user_id && reaction.emoji == emoji {
|
||||
return // Already reacted
|
||||
}
|
||||
}
|
||||
|
||||
c.messages[i].reactions << Reaction{
|
||||
id: message.reactions.len + 1
|
||||
message_id: message_id
|
||||
user_id: user_id
|
||||
emoji: emoji
|
||||
created_at: time.now()
|
||||
}
|
||||
c.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark_as_read marks messages as read for a user
|
||||
pub fn (mut c Chat) mark_as_read(user_id int, message_id int, by_user_id int) {
|
||||
// Update member's last read message
|
||||
for i, mut member in c.members {
|
||||
if member.user_id == user_id {
|
||||
c.members[i].last_read_at = time.now()
|
||||
c.members[i].last_read_message_id = message_id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add read receipt to message
|
||||
for i, mut message in c.messages {
|
||||
if message.id == message_id {
|
||||
// Check if already marked as read
|
||||
for read in message.read_by {
|
||||
if read.user_id == user_id {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.messages[i].read_by << MessageRead{
|
||||
user_id: user_id
|
||||
message_id: message_id
|
||||
read_at: time.now()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// mute_chat mutes the chat for a user
|
||||
pub fn (mut c Chat) mute_chat(user_id int, until time.Time, by_user_id int) {
|
||||
for i, mut member in c.members {
|
||||
if member.user_id == user_id {
|
||||
c.members[i].muted = true
|
||||
c.members[i].muted_until = until
|
||||
c.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// archive_chat archives the chat
|
||||
pub fn (mut c Chat) archive_chat(by_user_id int) {
|
||||
c.status = .archived
|
||||
c.archived_at = time.now()
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_integration adds an external integration
|
||||
pub fn (mut c Chat) add_integration(integration_type IntegrationType, name string, webhook_url string, settings map[string]string, by_user_id int) {
|
||||
c.integrations << ChatIntegration{
|
||||
id: c.integrations.len + 1
|
||||
chat_id: c.id
|
||||
integration_type: integration_type
|
||||
name: name
|
||||
webhook_url: webhook_url
|
||||
settings: settings
|
||||
enabled: true
|
||||
created_by: by_user_id
|
||||
created_at: time.now()
|
||||
}
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// get_activity_level returns chat activity level
|
||||
pub fn (c Chat) get_activity_level() string {
|
||||
if c.messages.len == 0 {
|
||||
return 'Inactive'
|
||||
}
|
||||
|
||||
// Messages in last 24 hours
|
||||
day_ago := time.now().unix - 86400
|
||||
recent_messages := c.messages.filter(it.created_at.unix > day_ago).len
|
||||
|
||||
if recent_messages > 50 {
|
||||
return 'Very Active'
|
||||
} else if recent_messages > 20 {
|
||||
return 'Active'
|
||||
} else if recent_messages > 5 {
|
||||
return 'Moderate'
|
||||
} else if recent_messages > 0 {
|
||||
return 'Low'
|
||||
} else {
|
||||
return 'Inactive'
|
||||
}
|
||||
}
|
||||
|
||||
// get_engagement_score calculates engagement score
|
||||
pub fn (c Chat) get_engagement_score() f32 {
|
||||
if c.members.len == 0 || c.messages.len == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate unique participants in last 7 days
|
||||
week_ago := time.now().unix - (86400 * 7)
|
||||
recent_messages := c.messages.filter(it.created_at.unix > week_ago)
|
||||
|
||||
mut unique_senders := map[int]bool{}
|
||||
for message in recent_messages {
|
||||
unique_senders[message.sender_id] = true
|
||||
}
|
||||
|
||||
participation_rate := f32(unique_senders.len) / f32(c.members.len)
|
||||
|
||||
// Calculate message frequency
|
||||
messages_per_day := f32(recent_messages.len) / 7.0
|
||||
frequency_score := if messages_per_day > 10 { 1.0 } else { messages_per_day / 10.0 }
|
||||
|
||||
// Calculate reaction engagement
|
||||
mut total_reactions := 0
|
||||
for message in recent_messages {
|
||||
total_reactions += message.reactions.len
|
||||
}
|
||||
reaction_rate := if recent_messages.len > 0 { f32(total_reactions) / f32(recent_messages.len) } else { 0 }
|
||||
reaction_score := if reaction_rate > 2 { 1.0 } else { reaction_rate / 2.0 }
|
||||
|
||||
// Weighted average
|
||||
return (participation_rate * 0.5) + (frequency_score * 0.3) + (reaction_score * 0.2)
|
||||
}
|
||||
223
lib/biz/planner/models/customer.v
Normal file
223
lib/biz/planner/models/customer.v
Normal file
@@ -0,0 +1,223 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Customer represents a client or prospect in the CRM system
|
||||
pub struct Customer {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
type CustomerType
|
||||
status CustomerStatus
|
||||
industry string
|
||||
website string
|
||||
description string
|
||||
contacts []Contact
|
||||
addresses []Address
|
||||
projects []int // Project IDs associated with this customer
|
||||
total_value f64 // Total contract value
|
||||
annual_value f64 // Annual recurring revenue
|
||||
payment_terms string
|
||||
tax_id string
|
||||
account_manager_id int // User ID of account manager
|
||||
lead_source string
|
||||
acquisition_date time.Time
|
||||
last_contact_date time.Time
|
||||
next_followup_date time.Time
|
||||
credit_limit f64
|
||||
payment_method string
|
||||
billing_cycle string // monthly, quarterly, annually
|
||||
notes string
|
||||
logo_url string
|
||||
social_media map[string]string // platform -> URL
|
||||
custom_fields map[string]string // Flexible custom data
|
||||
}
|
||||
|
||||
// get_primary_contact returns the primary contact for this customer
|
||||
pub fn (c Customer) get_primary_contact() ?Contact {
|
||||
for contact in c.contacts {
|
||||
if contact.is_primary {
|
||||
return contact
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// get_primary_address returns the primary address for this customer
|
||||
pub fn (c Customer) get_primary_address() ?Address {
|
||||
for address in c.addresses {
|
||||
if address.is_primary {
|
||||
return address
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// add_contact adds a new contact to the customer
|
||||
pub fn (mut c Customer) add_contact(contact Contact) {
|
||||
// If this is the first contact, make it primary
|
||||
if c.contacts.len == 0 {
|
||||
mut new_contact := contact
|
||||
new_contact.is_primary = true
|
||||
c.contacts << new_contact
|
||||
} else {
|
||||
c.contacts << contact
|
||||
}
|
||||
}
|
||||
|
||||
// update_contact updates an existing contact
|
||||
pub fn (mut c Customer) update_contact(contact_id int, updated_contact Contact) bool {
|
||||
for i, mut contact in c.contacts {
|
||||
if contact.id == contact_id {
|
||||
c.contacts[i] = updated_contact
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// remove_contact removes a contact by ID
|
||||
pub fn (mut c Customer) remove_contact(contact_id int) bool {
|
||||
for i, contact in c.contacts {
|
||||
if contact.id == contact_id {
|
||||
c.contacts.delete(i)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_address adds a new address to the customer
|
||||
pub fn (mut c Customer) add_address(address Address) {
|
||||
// If this is the first address, make it primary
|
||||
if c.addresses.len == 0 {
|
||||
mut new_address := address
|
||||
new_address.is_primary = true
|
||||
c.addresses << new_address
|
||||
} else {
|
||||
c.addresses << address
|
||||
}
|
||||
}
|
||||
|
||||
// update_address updates an existing address
|
||||
pub fn (mut c Customer) update_address(address_id int, updated_address Address) bool {
|
||||
for i, mut address in c.addresses {
|
||||
if address.id == address_id {
|
||||
c.addresses[i] = updated_address
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// remove_address removes an address by ID
|
||||
pub fn (mut c Customer) remove_address(address_id int) bool {
|
||||
for i, address in c.addresses {
|
||||
if address.id == address_id {
|
||||
c.addresses.delete(i)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_project associates a project with this customer
|
||||
pub fn (mut c Customer) add_project(project_id int) {
|
||||
if project_id !in c.projects {
|
||||
c.projects << project_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_project removes a project association
|
||||
pub fn (mut c Customer) remove_project(project_id int) {
|
||||
c.projects = c.projects.filter(it != project_id)
|
||||
}
|
||||
|
||||
// has_project checks if a project is associated with this customer
|
||||
pub fn (c Customer) has_project(project_id int) bool {
|
||||
return project_id in c.projects
|
||||
}
|
||||
|
||||
// is_active_customer checks if the customer is currently active
|
||||
pub fn (c Customer) is_active_customer() bool {
|
||||
return c.status == .active && c.is_active
|
||||
}
|
||||
|
||||
// is_prospect checks if the customer is still a prospect
|
||||
pub fn (c Customer) is_prospect() bool {
|
||||
return c.status in [.prospect, .lead, .qualified]
|
||||
}
|
||||
|
||||
// convert_to_customer converts a prospect to an active customer
|
||||
pub fn (mut c Customer) convert_to_customer(by_user_id int) {
|
||||
c.status = .active
|
||||
c.acquisition_date = time.now()
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_last_contact updates the last contact date
|
||||
pub fn (mut c Customer) update_last_contact(by_user_id int) {
|
||||
c.last_contact_date = time.now()
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// set_next_followup sets the next followup date
|
||||
pub fn (mut c Customer) set_next_followup(followup_date time.Time, by_user_id int) {
|
||||
c.next_followup_date = followup_date
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// is_followup_due checks if a followup is due
|
||||
pub fn (c Customer) is_followup_due() bool {
|
||||
if c.next_followup_date.unix == 0 {
|
||||
return false
|
||||
}
|
||||
return time.now() >= c.next_followup_date
|
||||
}
|
||||
|
||||
// calculate_lifetime_value calculates the total value from all projects
|
||||
pub fn (c Customer) calculate_lifetime_value(projects []Project) f64 {
|
||||
mut total := f64(0)
|
||||
for project in projects {
|
||||
if project.customer_id == c.id {
|
||||
total += project.budget
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// get_contact_by_type returns contacts of a specific type
|
||||
pub fn (c Customer) get_contact_by_type(contact_type ContactType) []Contact {
|
||||
return c.contacts.filter(it.type == contact_type)
|
||||
}
|
||||
|
||||
// get_address_by_type returns addresses of a specific type
|
||||
pub fn (c Customer) get_address_by_type(address_type AddressType) []Address {
|
||||
return c.addresses.filter(it.type == address_type)
|
||||
}
|
||||
|
||||
// set_account_manager assigns an account manager to this customer
|
||||
pub fn (mut c Customer) set_account_manager(user_id int, by_user_id int) {
|
||||
c.account_manager_id = user_id
|
||||
c.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_social_media adds a social media link
|
||||
pub fn (mut c Customer) add_social_media(platform string, url string) {
|
||||
c.social_media[platform] = url
|
||||
}
|
||||
|
||||
// get_social_media gets a social media URL by platform
|
||||
pub fn (c Customer) get_social_media(platform string) ?string {
|
||||
return c.social_media[platform] or { none }
|
||||
}
|
||||
|
||||
// set_custom_field sets a custom field value
|
||||
pub fn (mut c Customer) set_custom_field(field string, value string) {
|
||||
c.custom_fields[field] = value
|
||||
}
|
||||
|
||||
// get_custom_field gets a custom field value
|
||||
pub fn (c Customer) get_custom_field(field string) ?string {
|
||||
return c.custom_fields[field] or { none }
|
||||
}
|
||||
198
lib/biz/planner/models/enums.v
Normal file
198
lib/biz/planner/models/enums.v
Normal file
@@ -0,0 +1,198 @@
|
||||
module models
|
||||
|
||||
// Priority levels used across tasks, issues, and projects
|
||||
pub enum Priority {
|
||||
lowest
|
||||
low
|
||||
medium
|
||||
high
|
||||
highest
|
||||
}
|
||||
|
||||
// Status for projects
|
||||
pub enum ProjectStatus {
|
||||
planning
|
||||
active
|
||||
on_hold
|
||||
completed
|
||||
cancelled
|
||||
}
|
||||
|
||||
// Status for tasks
|
||||
pub enum TaskStatus {
|
||||
todo
|
||||
in_progress
|
||||
in_review
|
||||
testing
|
||||
done
|
||||
blocked
|
||||
}
|
||||
|
||||
// Task types for different kinds of work items
|
||||
pub enum TaskType {
|
||||
story
|
||||
bug
|
||||
epic
|
||||
spike
|
||||
task
|
||||
feature
|
||||
}
|
||||
|
||||
// Issue status for problem tracking
|
||||
pub enum IssueStatus {
|
||||
open
|
||||
in_progress
|
||||
resolved
|
||||
closed
|
||||
reopened
|
||||
}
|
||||
|
||||
// Issue severity levels
|
||||
pub enum IssueSeverity {
|
||||
low
|
||||
medium
|
||||
high
|
||||
critical
|
||||
}
|
||||
|
||||
// Issue types
|
||||
pub enum IssueType {
|
||||
bug
|
||||
feature_request
|
||||
improvement
|
||||
question
|
||||
documentation
|
||||
support
|
||||
}
|
||||
|
||||
// Sprint status for Scrum methodology
|
||||
pub enum SprintStatus {
|
||||
planning
|
||||
active
|
||||
completed
|
||||
cancelled
|
||||
}
|
||||
|
||||
// Milestone status
|
||||
pub enum MilestoneStatus {
|
||||
not_started
|
||||
in_progress
|
||||
completed
|
||||
overdue
|
||||
}
|
||||
|
||||
// Condition status for milestone requirements
|
||||
pub enum ConditionStatus {
|
||||
pending
|
||||
in_progress
|
||||
verified
|
||||
failed
|
||||
}
|
||||
|
||||
// Customer types for CRM
|
||||
pub enum CustomerType {
|
||||
individual
|
||||
company
|
||||
government
|
||||
nonprofit
|
||||
partner
|
||||
}
|
||||
|
||||
// Customer status in the sales pipeline
|
||||
pub enum CustomerStatus {
|
||||
prospect
|
||||
lead
|
||||
qualified
|
||||
active
|
||||
inactive
|
||||
archived
|
||||
}
|
||||
|
||||
// User roles in the system
|
||||
pub enum UserRole {
|
||||
admin
|
||||
project_manager
|
||||
developer
|
||||
designer
|
||||
tester
|
||||
analyst
|
||||
client
|
||||
viewer
|
||||
}
|
||||
|
||||
// User status
|
||||
pub enum UserStatus {
|
||||
active
|
||||
inactive
|
||||
suspended
|
||||
pending
|
||||
}
|
||||
|
||||
// Agenda/Calendar event types
|
||||
pub enum AgendaType {
|
||||
meeting
|
||||
deadline
|
||||
milestone
|
||||
personal
|
||||
project_review
|
||||
sprint_planning
|
||||
retrospective
|
||||
standup
|
||||
}
|
||||
|
||||
// Agenda status
|
||||
pub enum AgendaStatus {
|
||||
scheduled
|
||||
in_progress
|
||||
completed
|
||||
cancelled
|
||||
postponed
|
||||
}
|
||||
|
||||
// Chat types
|
||||
pub enum ChatType {
|
||||
direct
|
||||
group
|
||||
project
|
||||
team
|
||||
support
|
||||
}
|
||||
|
||||
// Message types in chat
|
||||
pub enum MessageType {
|
||||
text
|
||||
file
|
||||
image
|
||||
system
|
||||
notification
|
||||
mention
|
||||
}
|
||||
|
||||
// Address types
|
||||
pub enum AddressType {
|
||||
billing
|
||||
shipping
|
||||
office
|
||||
home
|
||||
other
|
||||
}
|
||||
|
||||
// Contact types
|
||||
pub enum ContactType {
|
||||
primary
|
||||
technical
|
||||
billing
|
||||
support
|
||||
other
|
||||
}
|
||||
|
||||
// Time entry types
|
||||
pub enum TimeEntryType {
|
||||
development
|
||||
testing
|
||||
meeting
|
||||
documentation
|
||||
support
|
||||
training
|
||||
other
|
||||
}
|
||||
583
lib/biz/planner/models/issue.v
Normal file
583
lib/biz/planner/models/issue.v
Normal file
@@ -0,0 +1,583 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Issue represents a problem, bug, or concern in the system
|
||||
pub struct Issue {
|
||||
BaseModel
|
||||
pub mut:
|
||||
title string @[required]
|
||||
description string
|
||||
project_id int // Links to Project
|
||||
task_id int // Links to Task (optional)
|
||||
sprint_id int // Links to Sprint (optional)
|
||||
reporter_id int // User who reported the issue
|
||||
assignee_id int // User assigned to resolve the issue
|
||||
status IssueStatus
|
||||
priority Priority
|
||||
severity Severity
|
||||
issue_type IssueType
|
||||
category IssueCategory
|
||||
resolution IssueResolution
|
||||
resolution_description string
|
||||
environment string // Environment where issue occurred
|
||||
version string // Version where issue was found
|
||||
fixed_version string // Version where issue was fixed
|
||||
component string // Component/module affected
|
||||
labels []int // Label IDs
|
||||
affects_versions []string
|
||||
fix_versions []string
|
||||
due_date time.Time
|
||||
resolved_date time.Time
|
||||
closed_date time.Time
|
||||
estimated_hours f32
|
||||
actual_hours f32
|
||||
story_points int // For estimation
|
||||
watchers []int // User IDs watching this issue
|
||||
linked_issues []IssueLink
|
||||
duplicates []int // Issue IDs that are duplicates of this
|
||||
duplicated_by int // Issue ID that this duplicates
|
||||
parent_issue_id int // For sub-issues
|
||||
sub_issues []int // Sub-issue IDs
|
||||
time_entries []TimeEntry
|
||||
comments []Comment
|
||||
attachments []Attachment
|
||||
workarounds []Workaround
|
||||
test_cases []TestCase
|
||||
steps_to_reproduce []string
|
||||
expected_behavior string
|
||||
actual_behavior string
|
||||
additional_info string
|
||||
browser string
|
||||
operating_system string
|
||||
device_info string
|
||||
network_info string
|
||||
user_agent string
|
||||
screen_resolution string
|
||||
logs []LogEntry
|
||||
stack_trace string
|
||||
error_message string
|
||||
frequency IssueFrequency
|
||||
impact_users int // Number of users affected
|
||||
business_impact string
|
||||
technical_debt bool // Is this technical debt?
|
||||
security_issue bool // Is this a security issue?
|
||||
performance_issue bool // Is this a performance issue?
|
||||
accessibility_issue bool // Is this an accessibility issue?
|
||||
regression bool // Is this a regression?
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// IssueType for categorizing issues
|
||||
pub enum IssueType {
|
||||
bug
|
||||
feature_request
|
||||
improvement
|
||||
task
|
||||
epic
|
||||
story
|
||||
sub_task
|
||||
incident
|
||||
change_request
|
||||
question
|
||||
documentation
|
||||
test
|
||||
}
|
||||
|
||||
// IssueCategory for further categorization
|
||||
pub enum IssueCategory {
|
||||
frontend
|
||||
backend
|
||||
database
|
||||
api
|
||||
ui_ux
|
||||
performance
|
||||
security
|
||||
infrastructure
|
||||
deployment
|
||||
configuration
|
||||
integration
|
||||
documentation
|
||||
testing
|
||||
accessibility
|
||||
mobile
|
||||
desktop
|
||||
web
|
||||
}
|
||||
|
||||
// IssueResolution for tracking how issues were resolved
|
||||
pub enum IssueResolution {
|
||||
unresolved
|
||||
fixed
|
||||
wont_fix
|
||||
duplicate
|
||||
invalid
|
||||
works_as_designed
|
||||
cannot_reproduce
|
||||
incomplete
|
||||
moved
|
||||
deferred
|
||||
}
|
||||
|
||||
// IssueFrequency for tracking how often an issue occurs
|
||||
pub enum IssueFrequency {
|
||||
always
|
||||
often
|
||||
sometimes
|
||||
rarely
|
||||
once
|
||||
unknown
|
||||
}
|
||||
|
||||
// IssueLink represents a relationship between issues
|
||||
pub struct IssueLink {
|
||||
pub mut:
|
||||
issue_id int
|
||||
linked_issue_id int
|
||||
link_type IssueLinkType
|
||||
created_at time.Time
|
||||
created_by int
|
||||
description string
|
||||
}
|
||||
|
||||
// IssueLinkType for different types of issue relationships
|
||||
pub enum IssueLinkType {
|
||||
blocks
|
||||
blocked_by
|
||||
relates_to
|
||||
duplicates
|
||||
duplicated_by
|
||||
causes
|
||||
caused_by
|
||||
parent_of
|
||||
child_of
|
||||
depends_on
|
||||
depended_by
|
||||
follows
|
||||
followed_by
|
||||
}
|
||||
|
||||
// Workaround represents a temporary solution for an issue
|
||||
pub struct Workaround {
|
||||
pub mut:
|
||||
id int
|
||||
issue_id int
|
||||
title string
|
||||
description string
|
||||
steps []string
|
||||
effectiveness f32 // 0.0 to 1.0 scale
|
||||
complexity WorkaroundComplexity
|
||||
temporary bool // Is this a temporary workaround?
|
||||
created_at time.Time
|
||||
created_by int
|
||||
tested_by []int // User IDs who tested this workaround
|
||||
success_rate f32 // Success rate from testing
|
||||
}
|
||||
|
||||
// WorkaroundComplexity for rating workaround complexity
|
||||
pub enum WorkaroundComplexity {
|
||||
simple
|
||||
moderate
|
||||
complex
|
||||
expert_only
|
||||
}
|
||||
|
||||
// TestCase represents a test case related to an issue
|
||||
pub struct TestCase {
|
||||
pub mut:
|
||||
id int
|
||||
issue_id int
|
||||
title string
|
||||
description string
|
||||
preconditions []string
|
||||
steps []string
|
||||
expected_result string
|
||||
test_data string
|
||||
test_type TestType
|
||||
automated bool
|
||||
created_at time.Time
|
||||
created_by int
|
||||
last_executed time.Time
|
||||
last_result TestResult
|
||||
}
|
||||
|
||||
// TestType for categorizing test cases
|
||||
pub enum TestType {
|
||||
unit
|
||||
integration
|
||||
system
|
||||
acceptance
|
||||
regression
|
||||
performance
|
||||
security
|
||||
usability
|
||||
compatibility
|
||||
}
|
||||
|
||||
// TestResult for test case results
|
||||
pub enum TestResult {
|
||||
not_executed
|
||||
passed
|
||||
failed
|
||||
blocked
|
||||
skipped
|
||||
}
|
||||
|
||||
// LogEntry represents a log entry related to an issue
|
||||
pub struct LogEntry {
|
||||
pub mut:
|
||||
timestamp time.Time
|
||||
level LogLevel
|
||||
message string
|
||||
source string
|
||||
thread string
|
||||
user_id int
|
||||
session_id string
|
||||
request_id string
|
||||
additional_data map[string]string
|
||||
}
|
||||
|
||||
// LogLevel for log entry severity
|
||||
pub enum LogLevel {
|
||||
trace
|
||||
debug
|
||||
info
|
||||
warn
|
||||
error
|
||||
fatal
|
||||
}
|
||||
|
||||
// is_overdue checks if the issue is past its due date
|
||||
pub fn (i Issue) is_overdue() bool {
|
||||
if i.due_date.unix == 0 || i.status in [.resolved, .closed, .cancelled] {
|
||||
return false
|
||||
}
|
||||
return time.now() > i.due_date
|
||||
}
|
||||
|
||||
// is_open checks if the issue is in an open state
|
||||
pub fn (i Issue) is_open() bool {
|
||||
return i.status !in [.resolved, .closed, .cancelled]
|
||||
}
|
||||
|
||||
// is_critical checks if the issue is critical
|
||||
pub fn (i Issue) is_critical() bool {
|
||||
return i.priority == .critical || i.severity == .blocker
|
||||
}
|
||||
|
||||
// get_age returns the age of the issue in days
|
||||
pub fn (i Issue) get_age() int {
|
||||
return int((time.now().unix - i.created_at.unix) / 86400)
|
||||
}
|
||||
|
||||
// get_resolution_time returns the time to resolve in hours
|
||||
pub fn (i Issue) get_resolution_time() f32 {
|
||||
if i.resolved_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32((i.resolved_date.unix - i.created_at.unix) / 3600)
|
||||
}
|
||||
|
||||
// get_time_to_close returns the time to close in hours
|
||||
pub fn (i Issue) get_time_to_close() f32 {
|
||||
if i.closed_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32((i.closed_date.unix - i.created_at.unix) / 3600)
|
||||
}
|
||||
|
||||
// assign_to assigns the issue to a user
|
||||
pub fn (mut i Issue) assign_to(user_id int, by_user_id int) {
|
||||
i.assignee_id = user_id
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// unassign removes the assignee from the issue
|
||||
pub fn (mut i Issue) unassign(by_user_id int) {
|
||||
i.assignee_id = 0
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_watcher adds a user to watch this issue
|
||||
pub fn (mut i Issue) add_watcher(user_id int, by_user_id int) {
|
||||
if user_id !in i.watchers {
|
||||
i.watchers << user_id
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// remove_watcher removes a user from watching this issue
|
||||
pub fn (mut i Issue) remove_watcher(user_id int, by_user_id int) {
|
||||
i.watchers = i.watchers.filter(it != user_id)
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// start_work starts work on the issue
|
||||
pub fn (mut i Issue) start_work(by_user_id int) {
|
||||
i.status = .in_progress
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// resolve_issue resolves the issue
|
||||
pub fn (mut i Issue) resolve_issue(resolution IssueResolution, resolution_description string, fixed_version string, by_user_id int) {
|
||||
i.status = .resolved
|
||||
i.resolution = resolution
|
||||
i.resolution_description = resolution_description
|
||||
i.fixed_version = fixed_version
|
||||
i.resolved_date = time.now()
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// close_issue closes the issue
|
||||
pub fn (mut i Issue) close_issue(by_user_id int) {
|
||||
i.status = .closed
|
||||
i.closed_date = time.now()
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// reopen_issue reopens a resolved/closed issue
|
||||
pub fn (mut i Issue) reopen_issue(by_user_id int) {
|
||||
i.status = .open
|
||||
i.resolution = .unresolved
|
||||
i.resolution_description = ''
|
||||
i.resolved_date = time.Time{}
|
||||
i.closed_date = time.Time{}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// cancel_issue cancels the issue
|
||||
pub fn (mut i Issue) cancel_issue(by_user_id int) {
|
||||
i.status = .cancelled
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_link adds a link to another issue
|
||||
pub fn (mut i Issue) add_link(linked_issue_id int, link_type IssueLinkType, description string, by_user_id int) {
|
||||
// Check if link already exists
|
||||
for link in i.linked_issues {
|
||||
if link.linked_issue_id == linked_issue_id && link.link_type == link_type {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i.linked_issues << IssueLink{
|
||||
issue_id: i.id
|
||||
linked_issue_id: linked_issue_id
|
||||
link_type: link_type
|
||||
description: description
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_link removes a link to another issue
|
||||
pub fn (mut i Issue) remove_link(linked_issue_id int, link_type IssueLinkType, by_user_id int) {
|
||||
for idx, link in i.linked_issues {
|
||||
if link.linked_issue_id == linked_issue_id && link.link_type == link_type {
|
||||
i.linked_issues.delete(idx)
|
||||
i.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark_as_duplicate marks this issue as a duplicate of another
|
||||
pub fn (mut i Issue) mark_as_duplicate(original_issue_id int, by_user_id int) {
|
||||
i.duplicated_by = original_issue_id
|
||||
i.resolution = .duplicate
|
||||
i.status = .resolved
|
||||
i.resolved_date = time.now()
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_duplicate adds an issue as a duplicate of this one
|
||||
pub fn (mut i Issue) add_duplicate(duplicate_issue_id int, by_user_id int) {
|
||||
if duplicate_issue_id !in i.duplicates {
|
||||
i.duplicates << duplicate_issue_id
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// log_time adds a time entry to the issue
|
||||
pub fn (mut i Issue) log_time(user_id int, hours f32, description string, date time.Time, by_user_id int) {
|
||||
i.time_entries << TimeEntry{
|
||||
user_id: user_id
|
||||
hours: hours
|
||||
description: description
|
||||
date: date
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
i.actual_hours += hours
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_comment adds a comment to the issue
|
||||
pub fn (mut i Issue) add_comment(user_id int, content string, by_user_id int) {
|
||||
i.comments << Comment{
|
||||
user_id: user_id
|
||||
content: content
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_attachment adds an attachment to the issue
|
||||
pub fn (mut i Issue) add_attachment(filename string, file_path string, file_size int, mime_type string, by_user_id int) {
|
||||
i.attachments << Attachment{
|
||||
filename: filename
|
||||
file_path: file_path
|
||||
file_size: file_size
|
||||
mime_type: mime_type
|
||||
uploaded_at: time.now()
|
||||
uploaded_by: by_user_id
|
||||
}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_workaround adds a workaround for the issue
|
||||
pub fn (mut i Issue) add_workaround(title string, description string, steps []string, effectiveness f32, complexity WorkaroundComplexity, temporary bool, by_user_id int) {
|
||||
i.workarounds << Workaround{
|
||||
id: i.workarounds.len + 1
|
||||
issue_id: i.id
|
||||
title: title
|
||||
description: description
|
||||
steps: steps
|
||||
effectiveness: effectiveness
|
||||
complexity: complexity
|
||||
temporary: temporary
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_test_case adds a test case for the issue
|
||||
pub fn (mut i Issue) add_test_case(title string, description string, preconditions []string, steps []string, expected_result string, test_type TestType, automated bool, by_user_id int) {
|
||||
i.test_cases << TestCase{
|
||||
id: i.test_cases.len + 1
|
||||
issue_id: i.id
|
||||
title: title
|
||||
description: description
|
||||
preconditions: preconditions
|
||||
steps: steps
|
||||
expected_result: expected_result
|
||||
test_type: test_type
|
||||
automated: automated
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_log_entry adds a log entry to the issue
|
||||
pub fn (mut i Issue) add_log_entry(timestamp time.Time, level LogLevel, message string, source string, thread string, user_id int, session_id string, request_id string, additional_data map[string]string) {
|
||||
i.logs << LogEntry{
|
||||
timestamp: timestamp
|
||||
level: level
|
||||
message: message
|
||||
source: source
|
||||
thread: thread
|
||||
user_id: user_id
|
||||
session_id: session_id
|
||||
request_id: request_id
|
||||
additional_data: additional_data
|
||||
}
|
||||
}
|
||||
|
||||
// set_due_date sets the due date for the issue
|
||||
pub fn (mut i Issue) set_due_date(due_date time.Time, by_user_id int) {
|
||||
i.due_date = due_date
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// escalate escalates the issue priority
|
||||
pub fn (mut i Issue) escalate(new_priority Priority, by_user_id int) {
|
||||
i.priority = new_priority
|
||||
i.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_priority_score calculates a priority score based on various factors
|
||||
pub fn (i Issue) calculate_priority_score() f32 {
|
||||
mut score := f32(0)
|
||||
|
||||
// Base priority score
|
||||
match i.priority {
|
||||
.critical { score += 100 }
|
||||
.high { score += 75 }
|
||||
.medium { score += 50 }
|
||||
.low { score += 25 }
|
||||
}
|
||||
|
||||
// Severity modifier
|
||||
match i.severity {
|
||||
.blocker { score += 50 }
|
||||
.critical { score += 40 }
|
||||
.major { score += 30 }
|
||||
.minor { score += 10 }
|
||||
.trivial { score += 0 }
|
||||
}
|
||||
|
||||
// Age factor (older issues get higher priority)
|
||||
age := i.get_age()
|
||||
if age > 30 {
|
||||
score += 20
|
||||
} else if age > 14 {
|
||||
score += 10
|
||||
} else if age > 7 {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// User impact factor
|
||||
if i.impact_users > 1000 {
|
||||
score += 30
|
||||
} else if i.impact_users > 100 {
|
||||
score += 20
|
||||
} else if i.impact_users > 10 {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Special issue type modifiers
|
||||
if i.security_issue {
|
||||
score += 25
|
||||
}
|
||||
if i.performance_issue {
|
||||
score += 15
|
||||
}
|
||||
if i.regression {
|
||||
score += 20
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// get_sla_status returns SLA compliance status
|
||||
pub fn (i Issue) get_sla_status() string {
|
||||
age := i.get_age()
|
||||
|
||||
// Define SLA based on priority
|
||||
mut sla_days := 0
|
||||
match i.priority {
|
||||
.critical { sla_days = 1 }
|
||||
.high { sla_days = 3 }
|
||||
.medium { sla_days = 7 }
|
||||
.low { sla_days = 14 }
|
||||
}
|
||||
|
||||
if i.status in [.resolved, .closed] {
|
||||
resolution_days := int(i.get_resolution_time() / 24)
|
||||
if resolution_days <= sla_days {
|
||||
return 'Met'
|
||||
} else {
|
||||
return 'Missed'
|
||||
}
|
||||
} else {
|
||||
if age <= sla_days {
|
||||
return 'On Track'
|
||||
} else {
|
||||
return 'Breached'
|
||||
}
|
||||
}
|
||||
}
|
||||
577
lib/biz/planner/models/milestone.v
Normal file
577
lib/biz/planner/models/milestone.v
Normal file
@@ -0,0 +1,577 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Milestone represents a significant project goal or deliverable
|
||||
pub struct Milestone {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
description string
|
||||
project_id int // Links to Project
|
||||
status MilestoneStatus
|
||||
priority Priority
|
||||
milestone_type MilestoneType
|
||||
due_date time.Time
|
||||
completed_date time.Time
|
||||
progress f32 // 0.0 to 1.0
|
||||
owner_id int // User responsible for this milestone
|
||||
stakeholders []int // User IDs of stakeholders
|
||||
conditions []Condition // Conditions that must be met
|
||||
deliverables []Deliverable
|
||||
dependencies []MilestoneDependency
|
||||
tasks []int // Task IDs associated with this milestone
|
||||
budget f64 // Budget allocated to this milestone
|
||||
actual_cost f64 // Actual cost incurred
|
||||
estimated_hours f32 // Estimated effort in hours
|
||||
actual_hours f32 // Actual effort spent
|
||||
acceptance_criteria []string
|
||||
success_metrics []SuccessMetric
|
||||
risks []Risk
|
||||
approvals []Approval
|
||||
communications []Communication
|
||||
review_notes string
|
||||
lessons_learned string
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// MilestoneStatus for milestone lifecycle
|
||||
pub enum MilestoneStatus {
|
||||
planning
|
||||
in_progress
|
||||
review
|
||||
completed
|
||||
cancelled
|
||||
on_hold
|
||||
}
|
||||
|
||||
// MilestoneType for categorizing milestones
|
||||
pub enum MilestoneType {
|
||||
deliverable
|
||||
decision_point
|
||||
review
|
||||
release
|
||||
contract
|
||||
regulatory
|
||||
internal
|
||||
external
|
||||
}
|
||||
|
||||
// Condition represents a condition that must be met for milestone completion
|
||||
pub struct Condition {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
title string
|
||||
description string
|
||||
condition_type ConditionType
|
||||
status ConditionStatus
|
||||
required bool // Is this condition mandatory?
|
||||
weight f32 // Weight in milestone completion (0.0 to 1.0)
|
||||
assigned_to int // User responsible for this condition
|
||||
due_date time.Time
|
||||
completed_date time.Time
|
||||
verification_method string
|
||||
evidence []string // URLs, file paths, or descriptions of evidence
|
||||
notes string
|
||||
created_at time.Time
|
||||
created_by int
|
||||
}
|
||||
|
||||
// ConditionType for categorizing conditions
|
||||
pub enum ConditionType {
|
||||
deliverable
|
||||
approval
|
||||
test_passed
|
||||
documentation
|
||||
training
|
||||
compliance
|
||||
quality_gate
|
||||
performance
|
||||
security
|
||||
legal
|
||||
}
|
||||
|
||||
// ConditionStatus for condition tracking
|
||||
pub enum ConditionStatus {
|
||||
not_started
|
||||
in_progress
|
||||
pending_review
|
||||
completed
|
||||
failed
|
||||
waived
|
||||
}
|
||||
|
||||
// Deliverable represents a specific deliverable for a milestone
|
||||
pub struct Deliverable {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
name string
|
||||
description string
|
||||
deliverable_type DeliverableType
|
||||
status DeliverableStatus
|
||||
assigned_to int
|
||||
due_date time.Time
|
||||
completed_date time.Time
|
||||
file_path string
|
||||
url string
|
||||
size_estimate string
|
||||
quality_criteria []string
|
||||
acceptance_criteria []string
|
||||
review_status ReviewStatus
|
||||
reviewer_id int
|
||||
review_notes string
|
||||
version string
|
||||
created_at time.Time
|
||||
created_by int
|
||||
}
|
||||
|
||||
// DeliverableType for categorizing deliverables
|
||||
pub enum DeliverableType {
|
||||
document
|
||||
software
|
||||
design
|
||||
report
|
||||
presentation
|
||||
training_material
|
||||
process
|
||||
template
|
||||
specification
|
||||
test_plan
|
||||
}
|
||||
|
||||
// DeliverableStatus for deliverable tracking
|
||||
pub enum DeliverableStatus {
|
||||
not_started
|
||||
in_progress
|
||||
draft
|
||||
review
|
||||
approved
|
||||
delivered
|
||||
rejected
|
||||
}
|
||||
|
||||
// ReviewStatus for deliverable reviews
|
||||
pub enum ReviewStatus {
|
||||
not_reviewed
|
||||
under_review
|
||||
approved
|
||||
rejected
|
||||
needs_revision
|
||||
}
|
||||
|
||||
// MilestoneDependency represents dependencies between milestones
|
||||
pub struct MilestoneDependency {
|
||||
pub mut:
|
||||
milestone_id int
|
||||
depends_on_milestone_id int
|
||||
dependency_type DependencyType
|
||||
created_at time.Time
|
||||
created_by int
|
||||
}
|
||||
|
||||
// SuccessMetric for measuring milestone success
|
||||
pub struct SuccessMetric {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
name string
|
||||
description string
|
||||
metric_type MetricType
|
||||
target_value f64
|
||||
actual_value f64
|
||||
unit string
|
||||
measurement_method string
|
||||
status MetricStatus
|
||||
measured_at time.Time
|
||||
measured_by int
|
||||
}
|
||||
|
||||
// MetricType for categorizing success metrics
|
||||
pub enum MetricType {
|
||||
performance
|
||||
quality
|
||||
cost
|
||||
time
|
||||
satisfaction
|
||||
adoption
|
||||
revenue
|
||||
efficiency
|
||||
}
|
||||
|
||||
// MetricStatus for metric tracking
|
||||
pub enum MetricStatus {
|
||||
not_measured
|
||||
measuring
|
||||
target_met
|
||||
target_exceeded
|
||||
target_missed
|
||||
}
|
||||
|
||||
// Risk represents a risk associated with a milestone
|
||||
pub struct Risk {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
title string
|
||||
description string
|
||||
risk_type RiskType
|
||||
probability f32 // 0.0 to 1.0
|
||||
impact f32 // 0.0 to 1.0
|
||||
risk_score f32 // probability * impact
|
||||
status RiskStatus
|
||||
owner_id int
|
||||
mitigation_plan string
|
||||
contingency_plan string
|
||||
identified_at time.Time
|
||||
identified_by int
|
||||
reviewed_at time.Time
|
||||
reviewed_by int
|
||||
}
|
||||
|
||||
// RiskType for categorizing risks
|
||||
pub enum RiskType {
|
||||
technical
|
||||
schedule
|
||||
budget
|
||||
resource
|
||||
quality
|
||||
external
|
||||
regulatory
|
||||
market
|
||||
}
|
||||
|
||||
// RiskStatus for risk tracking
|
||||
pub enum RiskStatus {
|
||||
identified
|
||||
analyzing
|
||||
mitigating
|
||||
monitoring
|
||||
closed
|
||||
realized
|
||||
}
|
||||
|
||||
// Approval represents an approval required for milestone completion
|
||||
pub struct Approval {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
title string
|
||||
description string
|
||||
approver_id int
|
||||
approval_type ApprovalType
|
||||
status ApprovalStatus
|
||||
requested_at time.Time
|
||||
requested_by int
|
||||
responded_at time.Time
|
||||
comments string
|
||||
conditions string
|
||||
expires_at time.Time
|
||||
}
|
||||
|
||||
// ApprovalType for categorizing approvals
|
||||
pub enum ApprovalType {
|
||||
technical
|
||||
business
|
||||
legal
|
||||
financial
|
||||
quality
|
||||
security
|
||||
regulatory
|
||||
}
|
||||
|
||||
// ApprovalStatus for approval tracking
|
||||
pub enum ApprovalStatus {
|
||||
pending
|
||||
approved
|
||||
rejected
|
||||
conditional
|
||||
expired
|
||||
}
|
||||
|
||||
// Communication represents communication about the milestone
|
||||
pub struct Communication {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int
|
||||
title string
|
||||
message string
|
||||
communication_type CommunicationType
|
||||
sender_id int
|
||||
recipients []int
|
||||
sent_at time.Time
|
||||
channel string
|
||||
priority Priority
|
||||
read_by []int // User IDs who have read this communication
|
||||
}
|
||||
|
||||
// CommunicationType for categorizing communications
|
||||
pub enum CommunicationType {
|
||||
update
|
||||
alert
|
||||
reminder
|
||||
announcement
|
||||
request
|
||||
escalation
|
||||
}
|
||||
|
||||
// is_overdue checks if the milestone is past its due date
|
||||
pub fn (m Milestone) is_overdue() bool {
|
||||
if m.due_date.unix == 0 || m.status in [.completed, .cancelled] {
|
||||
return false
|
||||
}
|
||||
return time.now() > m.due_date
|
||||
}
|
||||
|
||||
// get_completion_percentage calculates completion based on conditions
|
||||
pub fn (m Milestone) get_completion_percentage() f32 {
|
||||
if m.conditions.len == 0 {
|
||||
return m.progress * 100
|
||||
}
|
||||
|
||||
mut total_weight := f32(0)
|
||||
mut completed_weight := f32(0)
|
||||
|
||||
for condition in m.conditions {
|
||||
weight := if condition.weight > 0 { condition.weight } else { 1.0 }
|
||||
total_weight += weight
|
||||
|
||||
if condition.status == .completed {
|
||||
completed_weight += weight
|
||||
} else if condition.status == .waived {
|
||||
completed_weight += weight * 0.5 // Waived conditions count as half
|
||||
}
|
||||
}
|
||||
|
||||
if total_weight == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (completed_weight / total_weight) * 100
|
||||
}
|
||||
|
||||
// get_days_until_due returns days until due date
|
||||
pub fn (m Milestone) get_days_until_due() int {
|
||||
if m.due_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.now()
|
||||
if now > m.due_date {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int((m.due_date.unix - now.unix) / 86400)
|
||||
}
|
||||
|
||||
// get_budget_variance returns budget variance
|
||||
pub fn (m Milestone) get_budget_variance() f64 {
|
||||
return m.budget - m.actual_cost
|
||||
}
|
||||
|
||||
// is_over_budget checks if milestone is over budget
|
||||
pub fn (m Milestone) is_over_budget() bool {
|
||||
return m.budget > 0 && m.actual_cost > m.budget
|
||||
}
|
||||
|
||||
// add_condition adds a condition to the milestone
|
||||
pub fn (mut m Milestone) add_condition(title string, description string, condition_type ConditionType, required bool, weight f32, assigned_to int, due_date time.Time, by_user_id int) {
|
||||
m.conditions << Condition{
|
||||
id: m.conditions.len + 1
|
||||
milestone_id: m.id
|
||||
title: title
|
||||
description: description
|
||||
condition_type: condition_type
|
||||
status: .not_started
|
||||
required: required
|
||||
weight: weight
|
||||
assigned_to: assigned_to
|
||||
due_date: due_date
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_condition marks a condition as completed
|
||||
pub fn (mut m Milestone) complete_condition(condition_id int, evidence []string, notes string, by_user_id int) bool {
|
||||
for i, mut condition in m.conditions {
|
||||
if condition.id == condition_id {
|
||||
m.conditions[i].status = .completed
|
||||
m.conditions[i].completed_date = time.now()
|
||||
m.conditions[i].evidence = evidence
|
||||
m.conditions[i].notes = notes
|
||||
m.update_timestamp(by_user_id)
|
||||
|
||||
// Update milestone progress
|
||||
m.progress = m.get_completion_percentage() / 100
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_deliverable adds a deliverable to the milestone
|
||||
pub fn (mut m Milestone) add_deliverable(name string, description string, deliverable_type DeliverableType, assigned_to int, due_date time.Time, by_user_id int) {
|
||||
m.deliverables << Deliverable{
|
||||
id: m.deliverables.len + 1
|
||||
milestone_id: m.id
|
||||
name: name
|
||||
description: description
|
||||
deliverable_type: deliverable_type
|
||||
status: .not_started
|
||||
assigned_to: assigned_to
|
||||
due_date: due_date
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_deliverable marks a deliverable as completed
|
||||
pub fn (mut m Milestone) complete_deliverable(deliverable_id int, file_path string, url string, version string, by_user_id int) bool {
|
||||
for i, mut deliverable in m.deliverables {
|
||||
if deliverable.id == deliverable_id {
|
||||
m.deliverables[i].status = .delivered
|
||||
m.deliverables[i].completed_date = time.now()
|
||||
m.deliverables[i].file_path = file_path
|
||||
m.deliverables[i].url = url
|
||||
m.deliverables[i].version = version
|
||||
m.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_dependency adds a dependency to this milestone
|
||||
pub fn (mut m Milestone) add_dependency(depends_on_milestone_id int, dep_type DependencyType, by_user_id int) {
|
||||
// Check if dependency already exists
|
||||
for dep in m.dependencies {
|
||||
if dep.depends_on_milestone_id == depends_on_milestone_id {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.dependencies << MilestoneDependency{
|
||||
milestone_id: m.id
|
||||
depends_on_milestone_id: depends_on_milestone_id
|
||||
dependency_type: dep_type
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_stakeholder adds a stakeholder to the milestone
|
||||
pub fn (mut m Milestone) add_stakeholder(user_id int, by_user_id int) {
|
||||
if user_id !in m.stakeholders {
|
||||
m.stakeholders << user_id
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// request_approval requests an approval for the milestone
|
||||
pub fn (mut m Milestone) request_approval(title string, description string, approver_id int, approval_type ApprovalType, expires_at time.Time, by_user_id int) {
|
||||
m.approvals << Approval{
|
||||
id: m.approvals.len + 1
|
||||
milestone_id: m.id
|
||||
title: title
|
||||
description: description
|
||||
approver_id: approver_id
|
||||
approval_type: approval_type
|
||||
status: .pending
|
||||
requested_at: time.now()
|
||||
requested_by: by_user_id
|
||||
expires_at: expires_at
|
||||
}
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// approve grants an approval
|
||||
pub fn (mut m Milestone) approve(approval_id int, comments string, conditions string, by_user_id int) bool {
|
||||
for i, mut approval in m.approvals {
|
||||
if approval.id == approval_id && approval.approver_id == by_user_id {
|
||||
m.approvals[i].status = if conditions.len > 0 { .conditional } else { .approved }
|
||||
m.approvals[i].responded_at = time.now()
|
||||
m.approvals[i].comments = comments
|
||||
m.approvals[i].conditions = conditions
|
||||
m.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// start_milestone starts work on the milestone
|
||||
pub fn (mut m Milestone) start_milestone(by_user_id int) {
|
||||
m.status = .in_progress
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_milestone marks the milestone as completed
|
||||
pub fn (mut m Milestone) complete_milestone(by_user_id int) {
|
||||
m.status = .completed
|
||||
m.completed_date = time.now()
|
||||
m.progress = 1.0
|
||||
m.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_health returns a health score for the milestone
|
||||
pub fn (m Milestone) calculate_health() f32 {
|
||||
mut score := f32(1.0)
|
||||
|
||||
// Progress health (30% weight)
|
||||
if m.progress < 0.8 && m.status == .in_progress {
|
||||
score -= 0.3 * (0.8 - m.progress)
|
||||
}
|
||||
|
||||
// Schedule health (25% weight)
|
||||
if m.is_overdue() {
|
||||
score -= 0.25
|
||||
} else {
|
||||
days_until_due := m.get_days_until_due()
|
||||
if days_until_due < 7 && m.progress < 0.9 {
|
||||
score -= 0.125
|
||||
}
|
||||
}
|
||||
|
||||
// Budget health (20% weight)
|
||||
if m.is_over_budget() {
|
||||
variance_pct := (m.actual_cost - m.budget) / m.budget
|
||||
score -= 0.2 * variance_pct
|
||||
}
|
||||
|
||||
// Conditions health (15% weight)
|
||||
overdue_conditions := m.conditions.filter(it.due_date.unix > 0 && time.now() > it.due_date && it.status !in [.completed, .waived]).len
|
||||
if overdue_conditions > 0 {
|
||||
score -= 0.15 * f32(overdue_conditions) / f32(m.conditions.len)
|
||||
}
|
||||
|
||||
// Approvals health (10% weight)
|
||||
pending_approvals := m.approvals.filter(it.status == .pending).len
|
||||
if pending_approvals > 0 {
|
||||
score -= 0.1 * f32(pending_approvals) / f32(m.approvals.len)
|
||||
}
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// get_health_status returns a human-readable health status
|
||||
pub fn (m Milestone) get_health_status() string {
|
||||
health := m.calculate_health()
|
||||
if health >= 0.8 {
|
||||
return 'Excellent'
|
||||
} else if health >= 0.6 {
|
||||
return 'Good'
|
||||
} else if health >= 0.4 {
|
||||
return 'At Risk'
|
||||
} else {
|
||||
return 'Critical'
|
||||
}
|
||||
}
|
||||
291
lib/biz/planner/models/mod.v
Normal file
291
lib/biz/planner/models/mod.v
Normal file
@@ -0,0 +1,291 @@
|
||||
module models
|
||||
|
||||
// Task/Project Management System with Integrated CRM
|
||||
//
|
||||
// This module provides a comprehensive task and project management system
|
||||
// with integrated CRM capabilities, built using V language best practices.
|
||||
//
|
||||
// Architecture:
|
||||
// - Root objects stored as JSON in database tables with matching names
|
||||
// - Each root object has incremental IDs for efficient querying
|
||||
// - Additional indexing for searchable properties
|
||||
// - V structs with public fields and base class inheritance
|
||||
// - Support for SQLite and PostgreSQL via V's relational ORM
|
||||
//
|
||||
// Key Features:
|
||||
// - Scrum methodology support with sprints, story points, velocity tracking
|
||||
// - Milestone management with conditions and deliverables
|
||||
// - Comprehensive task management with dependencies and time tracking
|
||||
// - Issue tracking with severity levels and resolution workflow
|
||||
// - Team management with capacity planning and skill tracking
|
||||
// - Customer relationship management (CRM) integration
|
||||
// - Calendar/agenda management with recurrence and reminders
|
||||
// - Real-time chat and communication system
|
||||
// - Rich metadata support with tags, custom fields, and comments
|
||||
// - Audit trail with created/updated timestamps and user tracking
|
||||
// - Health scoring and analytics for projects, sprints, and teams
|
||||
|
||||
// Re-export all model types for easy importing
|
||||
pub use base { BaseModel }
|
||||
pub use enums {
|
||||
Priority, ProjectStatus, TaskStatus, TaskType, IssueStatus,
|
||||
IssueType, SprintStatus, MilestoneStatus, TeamStatus,
|
||||
AgendaStatus, ChatStatus, UserRole, CustomerStatus,
|
||||
SkillLevel, NotificationChannel, ReminderType
|
||||
}
|
||||
pub use subobjects {
|
||||
Contact, Address, TimeEntry, Comment, Attachment,
|
||||
Condition, Notification, Reaction, Reminder,
|
||||
UserPreferences, ProjectRole, Label
|
||||
}
|
||||
pub use user { User }
|
||||
pub use customer { Customer }
|
||||
pub use project { Project, ProjectBillingType, RiskLevel, ProjectMethodology }
|
||||
pub use task { Task, TaskDependency, DependencyType, Severity }
|
||||
pub use sprint {
|
||||
Sprint, SprintMember, SprintRetrospective, ActionItem,
|
||||
BurndownPoint, DailyStandup, Impediment
|
||||
}
|
||||
pub use milestone {
|
||||
Milestone, Condition as MilestoneCondition, Deliverable,
|
||||
MilestoneDependency, SuccessMetric, Risk, Approval, Communication
|
||||
}
|
||||
pub use issue {
|
||||
Issue, IssueLink, Workaround, TestCase, LogEntry,
|
||||
IssueFrequency, WorkaroundComplexity, TestType, LogLevel
|
||||
}
|
||||
pub use team {
|
||||
Team, TeamMember, TeamSkill, TeamCapacity, WorkingHours,
|
||||
Holiday, TeamRitual, TeamGoal, TeamMetric, TeamTool
|
||||
}
|
||||
pub use agenda {
|
||||
Agenda, Attendee, Resource, Recurrence, AgendaItem, Decision,
|
||||
AttendanceType, ResponseStatus, ResourceType, RecurrencePattern
|
||||
}
|
||||
pub use chat {
|
||||
Chat, ChatMember, Message, Reaction, RichContent, Poll,
|
||||
ChatSettings, NotificationSettings, ChatIntegration
|
||||
}
|
||||
|
||||
// System Overview:
|
||||
//
|
||||
// ROOT OBJECTS (stored as JSON with incremental IDs):
|
||||
// 1. User - System users with roles, skills, and preferences
|
||||
// 2. Customer - CRM entities with contacts and project relationships
|
||||
// 3. Project - Main project containers with budgets and timelines
|
||||
// 4. Task - Work items with dependencies and time tracking
|
||||
// 5. Sprint - Scrum sprints with velocity and burndown tracking
|
||||
// 6. Milestone - Project goals with conditions and deliverables
|
||||
// 7. Issue - Problem tracking with severity and resolution workflow
|
||||
// 8. Team - Groups with capacity planning and skill management
|
||||
// 9. Agenda - Calendar events with recurrence and attendee management
|
||||
// 10. Chat - Communication channels with threading and integrations
|
||||
//
|
||||
// RELATIONSHIPS:
|
||||
// - Projects belong to Customers and contain Tasks, Sprints, Milestones, Issues
|
||||
// - Tasks can be assigned to Sprints and Milestones, have dependencies
|
||||
// - Users are members of Teams and can be assigned to Projects/Tasks
|
||||
// - Sprints contain Tasks and track team velocity
|
||||
// - Milestones have Conditions that must be met for completion
|
||||
// - Issues can be linked to Projects, Tasks, or other Issues
|
||||
// - Agenda items can be linked to any entity for meeting context
|
||||
// - Chats can be associated with Projects, Teams, or specific entities
|
||||
//
|
||||
// DATA STORAGE:
|
||||
// - Each root object stored as JSON in database table matching struct name
|
||||
// - Incremental integer IDs for efficient querying and relationships
|
||||
// - Additional indexes on searchable fields (status, assignee, dates, etc.)
|
||||
// - Soft delete support via BaseModel.deleted_at field
|
||||
// - Full audit trail with created_at, updated_at, created_by, updated_by
|
||||
//
|
||||
// EXTENSIBILITY:
|
||||
// - Custom fields support via map[string]string on all root objects
|
||||
// - Tag system for flexible categorization
|
||||
// - Metadata field for additional structured data
|
||||
// - Plugin architecture via integrations (webhooks, external APIs)
|
||||
//
|
||||
// PERFORMANCE CONSIDERATIONS:
|
||||
// - JSON storage allows flexible schema evolution
|
||||
// - Targeted indexing on frequently queried fields
|
||||
// - Pagination support for large datasets
|
||||
// - Caching layer for frequently accessed data
|
||||
// - Async processing for heavy operations (notifications, reports)
|
||||
|
||||
// Database table names (matching struct names in lowercase)
|
||||
pub const table_names = [
|
||||
'user',
|
||||
'customer',
|
||||
'project',
|
||||
'task',
|
||||
'sprint',
|
||||
'milestone',
|
||||
'issue',
|
||||
'team',
|
||||
'agenda',
|
||||
'chat'
|
||||
]
|
||||
|
||||
// Searchable fields that should be indexed
|
||||
pub const indexed_fields = {
|
||||
'user': ['email', 'username', 'status', 'role']
|
||||
'customer': ['name', 'email', 'status', 'type']
|
||||
'project': ['name', 'status', 'priority', 'customer_id', 'project_manager_id']
|
||||
'task': ['title', 'status', 'priority', 'assignee_id', 'project_id', 'sprint_id']
|
||||
'sprint': ['name', 'status', 'project_id', 'start_date', 'end_date']
|
||||
'milestone': ['name', 'status', 'priority', 'project_id', 'due_date']
|
||||
'issue': ['title', 'status', 'priority', 'severity', 'assignee_id', 'project_id']
|
||||
'team': ['name', 'status', 'team_type', 'manager_id']
|
||||
'agenda': ['title', 'status', 'start_time', 'organizer_id', 'project_id']
|
||||
'chat': ['name', 'chat_type', 'status', 'owner_id', 'project_id', 'team_id']
|
||||
}
|
||||
|
||||
// Common query patterns for efficient database access
|
||||
pub const common_queries = {
|
||||
'active_projects': 'status IN ("planning", "active")'
|
||||
'overdue_tasks': 'due_date < NOW() AND status NOT IN ("done", "cancelled")'
|
||||
'current_sprints': 'status = "active" AND start_date <= NOW() AND end_date >= NOW()'
|
||||
'pending_milestones': 'status IN ("planning", "in_progress") AND due_date IS NOT NULL'
|
||||
'open_issues': 'status NOT IN ("resolved", "closed", "cancelled")'
|
||||
'active_teams': 'status = "performing"'
|
||||
'upcoming_meetings': 'start_time > NOW() AND status = "scheduled"'
|
||||
'active_chats': 'status = "active" AND last_activity > DATE_SUB(NOW(), INTERVAL 30 DAY)'
|
||||
}
|
||||
|
||||
// System-wide constants
|
||||
pub const (
|
||||
max_file_size = 100 * 1024 * 1024 // 100MB
|
||||
max_message_length = 10000
|
||||
max_comment_length = 5000
|
||||
max_description_length = 10000
|
||||
default_page_size = 50
|
||||
max_page_size = 1000
|
||||
session_timeout_hours = 24
|
||||
password_min_length = 8
|
||||
username_min_length = 3
|
||||
team_max_members = 100
|
||||
project_max_tasks = 10000
|
||||
sprint_max_duration_days = 30
|
||||
chat_max_members = 1000
|
||||
)
|
||||
|
||||
// Validation helpers
|
||||
pub fn validate_email(email string) bool {
|
||||
// Simple email validation - in production use proper regex
|
||||
return email.contains('@') && email.contains('.')
|
||||
}
|
||||
|
||||
pub fn validate_username(username string) bool {
|
||||
return username.len >= username_min_length && username.is_alnum()
|
||||
}
|
||||
|
||||
pub fn validate_password(password string) bool {
|
||||
return password.len >= password_min_length
|
||||
}
|
||||
|
||||
// Utility functions for common operations
|
||||
pub fn generate_slug(text string) string {
|
||||
return text.to_lower().replace(' ', '-').replace_each(['/', '\\', '?', '#'], '-')
|
||||
}
|
||||
|
||||
pub fn truncate_text(text string, max_length int) string {
|
||||
if text.len <= max_length {
|
||||
return text
|
||||
}
|
||||
return text[..max_length-3] + '...'
|
||||
}
|
||||
|
||||
pub fn format_duration(minutes int) string {
|
||||
if minutes < 60 {
|
||||
return '${minutes}m'
|
||||
}
|
||||
hours := minutes / 60
|
||||
remaining_minutes := minutes % 60
|
||||
if remaining_minutes == 0 {
|
||||
return '${hours}h'
|
||||
}
|
||||
return '${hours}h ${remaining_minutes}m'
|
||||
}
|
||||
|
||||
pub fn calculate_business_days(start_date time.Time, end_date time.Time) int {
|
||||
mut days := 0
|
||||
mut current := start_date
|
||||
|
||||
for current.unix <= end_date.unix {
|
||||
weekday := current.weekday()
|
||||
if weekday != 0 && weekday != 6 { // Not Sunday (0) or Saturday (6)
|
||||
days++
|
||||
}
|
||||
current = time.Time{unix: current.unix + 86400} // Add one day
|
||||
}
|
||||
|
||||
return days
|
||||
}
|
||||
|
||||
// Health scoring weights for different metrics
|
||||
pub const health_weights = {
|
||||
'project': {
|
||||
'budget': 0.25
|
||||
'schedule': 0.25
|
||||
'progress': 0.25
|
||||
'risk': 0.25
|
||||
}
|
||||
'sprint': {
|
||||
'completion': 0.4
|
||||
'utilization': 0.3
|
||||
'impediments': 0.2
|
||||
'schedule': 0.1
|
||||
}
|
||||
'team': {
|
||||
'utilization': 0.25
|
||||
'velocity': 0.25
|
||||
'goals': 0.25
|
||||
'stability': 0.25
|
||||
}
|
||||
'milestone': {
|
||||
'progress': 0.3
|
||||
'schedule': 0.25
|
||||
'budget': 0.2
|
||||
'conditions': 0.15
|
||||
'approvals': 0.1
|
||||
}
|
||||
}
|
||||
|
||||
// Default notification settings
|
||||
pub const default_notifications = {
|
||||
'task_assigned': true
|
||||
'task_due_soon': true
|
||||
'task_overdue': true
|
||||
'project_milestone': true
|
||||
'sprint_started': true
|
||||
'sprint_ended': true
|
||||
'meeting_reminder': true
|
||||
'chat_mention': true
|
||||
'issue_assigned': true
|
||||
'approval_requested': true
|
||||
}
|
||||
|
||||
// System roles and their default permissions
|
||||
pub const role_permissions = {
|
||||
'admin': ['*'] // All permissions
|
||||
'manager': [
|
||||
'create_project', 'edit_project', 'delete_project',
|
||||
'create_team', 'edit_team', 'manage_team_members',
|
||||
'create_milestone', 'edit_milestone',
|
||||
'view_reports', 'export_data'
|
||||
]
|
||||
'lead': [
|
||||
'create_task', 'edit_task', 'assign_task',
|
||||
'create_sprint', 'edit_sprint',
|
||||
'create_issue', 'edit_issue',
|
||||
'schedule_meeting', 'create_chat'
|
||||
]
|
||||
'member': [
|
||||
'view_project', 'create_task', 'edit_own_task',
|
||||
'create_issue', 'comment', 'upload_file',
|
||||
'join_meeting', 'send_message'
|
||||
]
|
||||
'viewer': [
|
||||
'view_project', 'view_task', 'view_issue',
|
||||
'view_meeting', 'read_message'
|
||||
]
|
||||
}
|
||||
349
lib/biz/planner/models/project.v
Normal file
349
lib/biz/planner/models/project.v
Normal file
@@ -0,0 +1,349 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Project represents a project in the system
|
||||
pub struct Project {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
description string
|
||||
customer_id int // Links to Customer
|
||||
status ProjectStatus
|
||||
priority Priority
|
||||
start_date time.Time
|
||||
end_date time.Time
|
||||
actual_start_date time.Time
|
||||
actual_end_date time.Time
|
||||
budget f64
|
||||
actual_cost f64
|
||||
estimated_hours f32
|
||||
actual_hours f32
|
||||
progress f32 // 0.0 to 1.0
|
||||
milestones []int // Milestone IDs
|
||||
sprints []int // Sprint IDs
|
||||
tasks []int // Task IDs
|
||||
issues []int // Issue IDs
|
||||
team_members []ProjectRole // Users and their roles in this project
|
||||
project_manager_id int // User ID of project manager
|
||||
client_contact_id int // Contact ID from customer
|
||||
billing_type ProjectBillingType
|
||||
hourly_rate f64 // Default hourly rate for this project
|
||||
currency string = 'USD'
|
||||
risk_level RiskLevel
|
||||
methodology ProjectMethodology
|
||||
repository_url string
|
||||
documentation_url string
|
||||
slack_channel string
|
||||
custom_fields map[string]string
|
||||
labels []int // Label IDs
|
||||
}
|
||||
|
||||
// ProjectBillingType for different billing models
|
||||
pub enum ProjectBillingType {
|
||||
fixed_price
|
||||
time_and_materials
|
||||
retainer
|
||||
milestone_based
|
||||
}
|
||||
|
||||
// RiskLevel for project risk assessment
|
||||
pub enum RiskLevel {
|
||||
low
|
||||
medium
|
||||
high
|
||||
critical
|
||||
}
|
||||
|
||||
// ProjectMethodology for project management approach
|
||||
pub enum ProjectMethodology {
|
||||
agile
|
||||
scrum
|
||||
kanban
|
||||
waterfall
|
||||
hybrid
|
||||
}
|
||||
|
||||
// get_duration returns the planned duration in days
|
||||
pub fn (p Project) get_duration() int {
|
||||
if p.start_date.unix == 0 || p.end_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((p.end_date.unix - p.start_date.unix) / 86400) // 86400 seconds in a day
|
||||
}
|
||||
|
||||
// get_actual_duration returns the actual duration in days
|
||||
pub fn (p Project) get_actual_duration() int {
|
||||
if p.actual_start_date.unix == 0 || p.actual_end_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((p.actual_end_date.unix - p.actual_start_date.unix) / 86400)
|
||||
}
|
||||
|
||||
// is_overdue checks if the project is past its end date
|
||||
pub fn (p Project) is_overdue() bool {
|
||||
if p.end_date.unix == 0 || p.status in [.completed, .cancelled] {
|
||||
return false
|
||||
}
|
||||
return time.now() > p.end_date
|
||||
}
|
||||
|
||||
// is_over_budget checks if the project is over budget
|
||||
pub fn (p Project) is_over_budget() bool {
|
||||
return p.budget > 0 && p.actual_cost > p.budget
|
||||
}
|
||||
|
||||
// get_budget_variance returns the budget variance (positive = under budget, negative = over budget)
|
||||
pub fn (p Project) get_budget_variance() f64 {
|
||||
return p.budget - p.actual_cost
|
||||
}
|
||||
|
||||
// get_budget_variance_percentage returns the budget variance as a percentage
|
||||
pub fn (p Project) get_budget_variance_percentage() f64 {
|
||||
if p.budget == 0 {
|
||||
return 0
|
||||
}
|
||||
return (p.get_budget_variance() / p.budget) * 100
|
||||
}
|
||||
|
||||
// get_schedule_variance returns schedule variance in days
|
||||
pub fn (p Project) get_schedule_variance() int {
|
||||
planned_duration := p.get_duration()
|
||||
if planned_duration == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if p.status == .completed {
|
||||
actual_duration := p.get_actual_duration()
|
||||
return planned_duration - actual_duration
|
||||
}
|
||||
|
||||
// For ongoing projects, calculate based on current date
|
||||
if p.start_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
days_elapsed := int((time.now().unix - p.start_date.unix) / 86400)
|
||||
expected_progress := f32(days_elapsed) / f32(planned_duration)
|
||||
|
||||
if expected_progress == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
schedule_performance := p.progress / expected_progress
|
||||
return int(f32(planned_duration) * (schedule_performance - 1))
|
||||
}
|
||||
|
||||
// add_team_member adds a user to the project with a specific role
|
||||
pub fn (mut p Project) add_team_member(user_id int, role string, permissions []string) {
|
||||
// Check if user is already in the project
|
||||
for i, member in p.team_members {
|
||||
if member.user_id == user_id {
|
||||
// Update existing member
|
||||
p.team_members[i].role = role
|
||||
p.team_members[i].permissions = permissions
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new member
|
||||
p.team_members << ProjectRole{
|
||||
user_id: user_id
|
||||
project_id: p.id
|
||||
role: role
|
||||
permissions: permissions
|
||||
assigned_at: time.now()
|
||||
}
|
||||
}
|
||||
|
||||
// remove_team_member removes a user from the project
|
||||
pub fn (mut p Project) remove_team_member(user_id int) bool {
|
||||
for i, member in p.team_members {
|
||||
if member.user_id == user_id {
|
||||
p.team_members.delete(i)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// has_team_member checks if a user is a team member
|
||||
pub fn (p Project) has_team_member(user_id int) bool {
|
||||
for member in p.team_members {
|
||||
if member.user_id == user_id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// get_team_member_role returns the role of a team member
|
||||
pub fn (p Project) get_team_member_role(user_id int) ?string {
|
||||
for member in p.team_members {
|
||||
if member.user_id == user_id {
|
||||
return member.role
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// add_milestone adds a milestone to the project
|
||||
pub fn (mut p Project) add_milestone(milestone_id int) {
|
||||
if milestone_id !in p.milestones {
|
||||
p.milestones << milestone_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_milestone removes a milestone from the project
|
||||
pub fn (mut p Project) remove_milestone(milestone_id int) {
|
||||
p.milestones = p.milestones.filter(it != milestone_id)
|
||||
}
|
||||
|
||||
// add_sprint adds a sprint to the project
|
||||
pub fn (mut p Project) add_sprint(sprint_id int) {
|
||||
if sprint_id !in p.sprints {
|
||||
p.sprints << sprint_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_sprint removes a sprint from the project
|
||||
pub fn (mut p Project) remove_sprint(sprint_id int) {
|
||||
p.sprints = p.sprints.filter(it != sprint_id)
|
||||
}
|
||||
|
||||
// add_task adds a task to the project
|
||||
pub fn (mut p Project) add_task(task_id int) {
|
||||
if task_id !in p.tasks {
|
||||
p.tasks << task_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_task removes a task from the project
|
||||
pub fn (mut p Project) remove_task(task_id int) {
|
||||
p.tasks = p.tasks.filter(it != task_id)
|
||||
}
|
||||
|
||||
// add_issue adds an issue to the project
|
||||
pub fn (mut p Project) add_issue(issue_id int) {
|
||||
if issue_id !in p.issues {
|
||||
p.issues << issue_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_issue removes an issue from the project
|
||||
pub fn (mut p Project) remove_issue(issue_id int) {
|
||||
p.issues = p.issues.filter(it != issue_id)
|
||||
}
|
||||
|
||||
// start_project marks the project as started
|
||||
pub fn (mut p Project) start_project(by_user_id int) {
|
||||
p.status = .active
|
||||
p.actual_start_date = time.now()
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_project marks the project as completed
|
||||
pub fn (mut p Project) complete_project(by_user_id int) {
|
||||
p.status = .completed
|
||||
p.actual_end_date = time.now()
|
||||
p.progress = 1.0
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// cancel_project marks the project as cancelled
|
||||
pub fn (mut p Project) cancel_project(by_user_id int) {
|
||||
p.status = .cancelled
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// put_on_hold puts the project on hold
|
||||
pub fn (mut p Project) put_on_hold(by_user_id int) {
|
||||
p.status = .on_hold
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_progress updates the project progress
|
||||
pub fn (mut p Project) update_progress(progress f32, by_user_id int) {
|
||||
if progress < 0 {
|
||||
p.progress = 0
|
||||
} else if progress > 1 {
|
||||
p.progress = 1
|
||||
} else {
|
||||
p.progress = progress
|
||||
}
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_cost adds to the actual cost
|
||||
pub fn (mut p Project) add_cost(amount f64, by_user_id int) {
|
||||
p.actual_cost += amount
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_hours adds to the actual hours
|
||||
pub fn (mut p Project) add_hours(hours f32, by_user_id int) {
|
||||
p.actual_hours += hours
|
||||
p.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_health returns a project health score based on various factors
|
||||
pub fn (p Project) calculate_health() f32 {
|
||||
mut score := f32(1.0)
|
||||
|
||||
// Budget health (25% weight)
|
||||
if p.budget > 0 {
|
||||
budget_ratio := p.actual_cost / p.budget
|
||||
if budget_ratio > 1.2 {
|
||||
score -= 0.25
|
||||
} else if budget_ratio > 1.0 {
|
||||
score -= 0.125
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule health (25% weight)
|
||||
schedule_var := p.get_schedule_variance()
|
||||
if schedule_var < -7 { // More than a week behind
|
||||
score -= 0.25
|
||||
} else if schedule_var < 0 {
|
||||
score -= 0.125
|
||||
}
|
||||
|
||||
// Progress health (25% weight)
|
||||
if p.progress < 0.5 && p.status == .active {
|
||||
days_elapsed := int((time.now().unix - p.start_date.unix) / 86400)
|
||||
total_days := p.get_duration()
|
||||
if total_days > 0 {
|
||||
expected_progress := f32(days_elapsed) / f32(total_days)
|
||||
if p.progress < expected_progress * 0.8 {
|
||||
score -= 0.25
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Risk level (25% weight)
|
||||
match p.risk_level {
|
||||
.critical { score -= 0.25 }
|
||||
.high { score -= 0.125 }
|
||||
else {}
|
||||
}
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// get_health_status returns a human-readable health status
|
||||
pub fn (p Project) get_health_status() string {
|
||||
health := p.calculate_health()
|
||||
if health >= 0.8 {
|
||||
return 'Excellent'
|
||||
} else if health >= 0.6 {
|
||||
return 'Good'
|
||||
} else if health >= 0.4 {
|
||||
return 'At Risk'
|
||||
} else {
|
||||
return 'Critical'
|
||||
}
|
||||
}
|
||||
431
lib/biz/planner/models/sprint.v
Normal file
431
lib/biz/planner/models/sprint.v
Normal file
@@ -0,0 +1,431 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Sprint represents a Scrum sprint
|
||||
pub struct Sprint {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
description string
|
||||
project_id int // Links to Project
|
||||
sprint_number int // Sequential number within project
|
||||
status SprintStatus
|
||||
start_date time.Time
|
||||
end_date time.Time
|
||||
goal string // Sprint goal
|
||||
capacity f32 // Team capacity in hours
|
||||
commitment int // Story points committed
|
||||
completed int // Story points completed
|
||||
velocity f32 // Actual velocity (story points / sprint duration)
|
||||
tasks []int // Task IDs in this sprint
|
||||
team_members []SprintMember // Team members and their capacity
|
||||
retrospective SprintRetrospective
|
||||
review_notes string
|
||||
demo_url string
|
||||
burndown_data []BurndownPoint
|
||||
daily_standups []DailyStandup
|
||||
impediments []Impediment
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// SprintStatus for sprint lifecycle
|
||||
pub enum SprintStatus {
|
||||
planning
|
||||
active
|
||||
completed
|
||||
cancelled
|
||||
}
|
||||
|
||||
// SprintMember represents a team member's participation in a sprint
|
||||
pub struct SprintMember {
|
||||
pub mut:
|
||||
user_id int
|
||||
sprint_id int
|
||||
capacity_hours f32 // Available hours for this sprint
|
||||
allocated_hours f32 // Hours allocated to tasks
|
||||
actual_hours f32 // Hours actually worked
|
||||
availability f32 // Percentage availability (0.0 to 1.0)
|
||||
role string
|
||||
joined_at time.Time
|
||||
}
|
||||
|
||||
// SprintRetrospective for sprint retrospective data
|
||||
pub struct SprintRetrospective {
|
||||
pub mut:
|
||||
conducted_at time.Time
|
||||
facilitator_id int
|
||||
participants []int // User IDs
|
||||
what_went_well []string
|
||||
what_went_wrong []string
|
||||
action_items []ActionItem
|
||||
team_mood f32 // 1.0 to 5.0 scale
|
||||
notes string
|
||||
}
|
||||
|
||||
// ActionItem for retrospective action items
|
||||
pub struct ActionItem {
|
||||
pub mut:
|
||||
description string
|
||||
assignee_id int
|
||||
due_date time.Time
|
||||
status ActionItemStatus
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
// ActionItemStatus for action item tracking
|
||||
pub enum ActionItemStatus {
|
||||
open
|
||||
in_progress
|
||||
completed
|
||||
cancelled
|
||||
}
|
||||
|
||||
// BurndownPoint for burndown chart data
|
||||
pub struct BurndownPoint {
|
||||
pub mut:
|
||||
date time.Time
|
||||
remaining_points int
|
||||
remaining_hours f32
|
||||
completed_points int
|
||||
added_points int // Points added during sprint
|
||||
removed_points int // Points removed during sprint
|
||||
}
|
||||
|
||||
// DailyStandup for daily standup meeting data
|
||||
pub struct DailyStandup {
|
||||
pub mut:
|
||||
date time.Time
|
||||
facilitator_id int
|
||||
participants []int // User IDs
|
||||
updates []StandupUpdate
|
||||
impediments []int // Impediment IDs discussed
|
||||
duration_minutes int
|
||||
notes string
|
||||
}
|
||||
|
||||
// StandupUpdate for individual team member updates
|
||||
pub struct StandupUpdate {
|
||||
pub mut:
|
||||
user_id int
|
||||
yesterday string // What did you do yesterday?
|
||||
today string // What will you do today?
|
||||
blockers string // Any blockers or impediments?
|
||||
mood f32 // 1.0 to 5.0 scale
|
||||
}
|
||||
|
||||
// Impediment for tracking sprint impediments
|
||||
pub struct Impediment {
|
||||
pub mut:
|
||||
id int
|
||||
sprint_id int
|
||||
title string
|
||||
description string
|
||||
reported_by int
|
||||
assigned_to int
|
||||
status ImpedimentStatus
|
||||
severity Priority
|
||||
reported_at time.Time
|
||||
resolved_at time.Time
|
||||
resolution string
|
||||
}
|
||||
|
||||
// ImpedimentStatus for impediment tracking
|
||||
pub enum ImpedimentStatus {
|
||||
open
|
||||
in_progress
|
||||
resolved
|
||||
cancelled
|
||||
}
|
||||
|
||||
// get_duration returns the sprint duration in days
|
||||
pub fn (s Sprint) get_duration() int {
|
||||
if s.start_date.unix == 0 || s.end_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return int((s.end_date.unix - s.start_date.unix) / 86400)
|
||||
}
|
||||
|
||||
// get_days_remaining returns the number of days remaining in the sprint
|
||||
pub fn (s Sprint) get_days_remaining() int {
|
||||
if s.end_date.unix == 0 || s.status != .active {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.now()
|
||||
if now > s.end_date {
|
||||
return 0
|
||||
}
|
||||
|
||||
return int((s.end_date.unix - now.unix) / 86400)
|
||||
}
|
||||
|
||||
// get_days_elapsed returns the number of days elapsed in the sprint
|
||||
pub fn (s Sprint) get_days_elapsed() int {
|
||||
if s.start_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.now()
|
||||
if now < s.start_date {
|
||||
return 0
|
||||
}
|
||||
|
||||
end_time := if s.status == .completed && s.end_date.unix > 0 { s.end_date } else { now }
|
||||
return int((end_time.unix - s.start_date.unix) / 86400)
|
||||
}
|
||||
|
||||
// is_active checks if the sprint is currently active
|
||||
pub fn (s Sprint) is_active() bool {
|
||||
return s.status == .active
|
||||
}
|
||||
|
||||
// is_overdue checks if the sprint has passed its end date
|
||||
pub fn (s Sprint) is_overdue() bool {
|
||||
return s.status == .active && time.now() > s.end_date
|
||||
}
|
||||
|
||||
// get_completion_percentage returns the completion percentage based on story points
|
||||
pub fn (s Sprint) get_completion_percentage() f32 {
|
||||
if s.commitment == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32(s.completed) / f32(s.commitment) * 100
|
||||
}
|
||||
|
||||
// get_velocity calculates the actual velocity for the sprint
|
||||
pub fn (s Sprint) get_velocity() f32 {
|
||||
duration := s.get_duration()
|
||||
if duration == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32(s.completed) / f32(duration)
|
||||
}
|
||||
|
||||
// get_team_capacity returns the total team capacity in hours
|
||||
pub fn (s Sprint) get_team_capacity() f32 {
|
||||
mut total := f32(0)
|
||||
for member in s.team_members {
|
||||
total += member.capacity_hours
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// get_team_utilization returns the team utilization percentage
|
||||
pub fn (s Sprint) get_team_utilization() f32 {
|
||||
capacity := s.get_team_capacity()
|
||||
if capacity == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
mut actual := f32(0)
|
||||
for member in s.team_members {
|
||||
actual += member.actual_hours
|
||||
}
|
||||
|
||||
return (actual / capacity) * 100
|
||||
}
|
||||
|
||||
// add_task adds a task to the sprint
|
||||
pub fn (mut s Sprint) add_task(task_id int, by_user_id int) {
|
||||
if task_id !in s.tasks {
|
||||
s.tasks << task_id
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// remove_task removes a task from the sprint
|
||||
pub fn (mut s Sprint) remove_task(task_id int, by_user_id int) {
|
||||
s.tasks = s.tasks.filter(it != task_id)
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_team_member adds a team member to the sprint
|
||||
pub fn (mut s Sprint) add_team_member(user_id int, capacity_hours f32, availability f32, role string, by_user_id int) {
|
||||
// Check if member already exists
|
||||
for i, member in s.team_members {
|
||||
if member.user_id == user_id {
|
||||
// Update existing member
|
||||
s.team_members[i].capacity_hours = capacity_hours
|
||||
s.team_members[i].availability = availability
|
||||
s.team_members[i].role = role
|
||||
s.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new member
|
||||
s.team_members << SprintMember{
|
||||
user_id: user_id
|
||||
sprint_id: s.id
|
||||
capacity_hours: capacity_hours
|
||||
availability: availability
|
||||
role: role
|
||||
joined_at: time.now()
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_team_member removes a team member from the sprint
|
||||
pub fn (mut s Sprint) remove_team_member(user_id int, by_user_id int) {
|
||||
for i, member in s.team_members {
|
||||
if member.user_id == user_id {
|
||||
s.team_members.delete(i)
|
||||
s.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start_sprint starts the sprint
|
||||
pub fn (mut s Sprint) start_sprint(by_user_id int) {
|
||||
s.status = .active
|
||||
if s.start_date.unix == 0 {
|
||||
s.start_date = time.now()
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_sprint completes the sprint
|
||||
pub fn (mut s Sprint) complete_sprint(by_user_id int) {
|
||||
s.status = .completed
|
||||
s.velocity = s.get_velocity()
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// cancel_sprint cancels the sprint
|
||||
pub fn (mut s Sprint) cancel_sprint(by_user_id int) {
|
||||
s.status = .cancelled
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_commitment updates the story points commitment
|
||||
pub fn (mut s Sprint) update_commitment(points int, by_user_id int) {
|
||||
s.commitment = points
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_completed updates the completed story points
|
||||
pub fn (mut s Sprint) update_completed(points int, by_user_id int) {
|
||||
s.completed = points
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_burndown_point adds a burndown chart data point
|
||||
pub fn (mut s Sprint) add_burndown_point(remaining_points int, remaining_hours f32, completed_points int, by_user_id int) {
|
||||
s.burndown_data << BurndownPoint{
|
||||
date: time.now()
|
||||
remaining_points: remaining_points
|
||||
remaining_hours: remaining_hours
|
||||
completed_points: completed_points
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_daily_standup adds a daily standup record
|
||||
pub fn (mut s Sprint) add_daily_standup(facilitator_id int, participants []int, updates []StandupUpdate, duration_minutes int, notes string, by_user_id int) {
|
||||
s.daily_standups << DailyStandup{
|
||||
date: time.now()
|
||||
facilitator_id: facilitator_id
|
||||
participants: participants
|
||||
updates: updates
|
||||
duration_minutes: duration_minutes
|
||||
notes: notes
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_impediment adds an impediment to the sprint
|
||||
pub fn (mut s Sprint) add_impediment(title string, description string, reported_by int, severity Priority, by_user_id int) {
|
||||
s.impediments << Impediment{
|
||||
id: s.impediments.len + 1
|
||||
sprint_id: s.id
|
||||
title: title
|
||||
description: description
|
||||
reported_by: reported_by
|
||||
status: .open
|
||||
severity: severity
|
||||
reported_at: time.now()
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// resolve_impediment resolves an impediment
|
||||
pub fn (mut s Sprint) resolve_impediment(impediment_id int, resolution string, by_user_id int) {
|
||||
for i, mut impediment in s.impediments {
|
||||
if impediment.id == impediment_id {
|
||||
s.impediments[i].status = .resolved
|
||||
s.impediments[i].resolved_at = time.now()
|
||||
s.impediments[i].resolution = resolution
|
||||
s.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// conduct_retrospective conducts a sprint retrospective
|
||||
pub fn (mut s Sprint) conduct_retrospective(facilitator_id int, participants []int, went_well []string, went_wrong []string, action_items []ActionItem, team_mood f32, notes string, by_user_id int) {
|
||||
s.retrospective = SprintRetrospective{
|
||||
conducted_at: time.now()
|
||||
facilitator_id: facilitator_id
|
||||
participants: participants
|
||||
what_went_well: went_well
|
||||
what_went_wrong: went_wrong
|
||||
action_items: action_items
|
||||
team_mood: team_mood
|
||||
notes: notes
|
||||
}
|
||||
s.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// get_health_score calculates a health score for the sprint
|
||||
pub fn (s Sprint) get_health_score() f32 {
|
||||
mut score := f32(1.0)
|
||||
|
||||
// Completion rate (40% weight)
|
||||
completion := s.get_completion_percentage()
|
||||
if completion < 70 {
|
||||
score -= 0.4 * (70 - completion) / 70
|
||||
}
|
||||
|
||||
// Team utilization (30% weight)
|
||||
utilization := s.get_team_utilization()
|
||||
if utilization < 80 || utilization > 120 {
|
||||
if utilization < 80 {
|
||||
score -= 0.3 * (80 - utilization) / 80
|
||||
} else {
|
||||
score -= 0.3 * (utilization - 120) / 120
|
||||
}
|
||||
}
|
||||
|
||||
// Impediments (20% weight)
|
||||
open_impediments := s.impediments.filter(it.status == .open).len
|
||||
if open_impediments > 0 {
|
||||
score -= 0.2 * f32(open_impediments) / 5 // Assume 5+ impediments is very bad
|
||||
}
|
||||
|
||||
// Schedule adherence (10% weight)
|
||||
if s.is_overdue() {
|
||||
score -= 0.1
|
||||
}
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// get_health_status returns a human-readable health status
|
||||
pub fn (s Sprint) get_health_status() string {
|
||||
health := s.get_health_score()
|
||||
if health >= 0.8 {
|
||||
return 'Excellent'
|
||||
} else if health >= 0.6 {
|
||||
return 'Good'
|
||||
} else if health >= 0.4 {
|
||||
return 'At Risk'
|
||||
} else {
|
||||
return 'Critical'
|
||||
}
|
||||
}
|
||||
238
lib/biz/planner/models/subobjects.v
Normal file
238
lib/biz/planner/models/subobjects.v
Normal file
@@ -0,0 +1,238 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Contact represents a person associated with a customer or organization
|
||||
pub struct Contact {
|
||||
pub mut:
|
||||
id int
|
||||
name string @[required]
|
||||
email string
|
||||
phone string
|
||||
mobile string
|
||||
role string
|
||||
department string
|
||||
type ContactType
|
||||
is_primary bool
|
||||
notes string
|
||||
created_at time.Time
|
||||
updated_at time.Time
|
||||
}
|
||||
|
||||
// Address represents a physical address
|
||||
pub struct Address {
|
||||
pub mut:
|
||||
id int
|
||||
type AddressType
|
||||
label string // e.g., "Main Office", "Warehouse"
|
||||
street string
|
||||
street2 string // Additional address line
|
||||
city string
|
||||
state string
|
||||
postal_code string
|
||||
country string
|
||||
is_primary bool
|
||||
created_at time.Time
|
||||
updated_at time.Time
|
||||
}
|
||||
|
||||
// TimeEntry represents time spent on tasks or projects
|
||||
pub struct TimeEntry {
|
||||
pub mut:
|
||||
id int
|
||||
user_id int @[required]
|
||||
task_id int
|
||||
project_id int
|
||||
start_time time.Time
|
||||
end_time time.Time
|
||||
duration f32 // Hours (calculated or manual)
|
||||
description string
|
||||
type TimeEntryType
|
||||
billable bool
|
||||
hourly_rate f64
|
||||
created_at time.Time
|
||||
updated_at time.Time
|
||||
}
|
||||
|
||||
// Comment represents a comment on tasks, issues, or other entities
|
||||
pub struct Comment {
|
||||
pub mut:
|
||||
id int
|
||||
author_id int @[required]
|
||||
content string @[required]
|
||||
timestamp time.Time
|
||||
is_internal bool // Internal comments not visible to clients
|
||||
is_edited bool
|
||||
edited_at time.Time
|
||||
parent_id int // For threaded comments
|
||||
}
|
||||
|
||||
// Attachment represents a file attached to an entity
|
||||
pub struct Attachment {
|
||||
pub mut:
|
||||
id int
|
||||
filename string @[required]
|
||||
original_name string
|
||||
file_path string @[required]
|
||||
file_size i64
|
||||
mime_type string
|
||||
uploaded_by int // User ID
|
||||
uploaded_at time.Time
|
||||
description string
|
||||
is_public bool // Whether clients can see this attachment
|
||||
}
|
||||
|
||||
// Condition represents a requirement that must be met for a milestone
|
||||
pub struct Condition {
|
||||
pub mut:
|
||||
id int
|
||||
milestone_id int @[required]
|
||||
description string @[required]
|
||||
status ConditionStatus
|
||||
verification string // How to verify this condition is met
|
||||
responsible_id int // User ID responsible for this condition
|
||||
due_date time.Time
|
||||
completed_at time.Time
|
||||
notes string
|
||||
created_at time.Time
|
||||
updated_at time.Time
|
||||
}
|
||||
|
||||
// Message represents a chat message
|
||||
pub struct Message {
|
||||
pub mut:
|
||||
id int
|
||||
chat_id int @[required]
|
||||
sender_id int @[required]
|
||||
content string @[required]
|
||||
timestamp time.Time
|
||||
message_type MessageType
|
||||
attachments []Attachment
|
||||
reactions []Reaction
|
||||
thread_id int // For threaded conversations
|
||||
is_edited bool
|
||||
edited_at time.Time
|
||||
mentions []int // User IDs mentioned in the message
|
||||
}
|
||||
|
||||
// Reaction represents an emoji reaction to a message
|
||||
pub struct Reaction {
|
||||
pub mut:
|
||||
id int
|
||||
message_id int @[required]
|
||||
user_id int @[required]
|
||||
emoji string @[required]
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// Notification represents a system notification
|
||||
pub struct Notification {
|
||||
pub mut:
|
||||
id int
|
||||
user_id int @[required]
|
||||
title string @[required]
|
||||
message string @[required]
|
||||
type NotificationType
|
||||
entity_type string // e.g., "task", "project", "issue"
|
||||
entity_id int
|
||||
is_read bool
|
||||
created_at time.Time
|
||||
read_at time.Time
|
||||
}
|
||||
|
||||
// NotificationType for different kinds of notifications
|
||||
pub enum NotificationType {
|
||||
info
|
||||
warning
|
||||
error
|
||||
success
|
||||
task_assigned
|
||||
task_completed
|
||||
deadline_approaching
|
||||
milestone_reached
|
||||
comment_added
|
||||
mention
|
||||
}
|
||||
|
||||
// Reminder for agenda items
|
||||
pub struct Reminder {
|
||||
pub mut:
|
||||
id int
|
||||
agenda_id int @[required]
|
||||
user_id int @[required]
|
||||
remind_at time.Time
|
||||
message string
|
||||
is_sent bool
|
||||
sent_at time.Time
|
||||
}
|
||||
|
||||
// RecurrenceRule for recurring agenda items
|
||||
pub struct RecurrenceRule {
|
||||
pub mut:
|
||||
frequency RecurrenceFrequency
|
||||
interval int = 1 // Every N frequency units
|
||||
end_date time.Time
|
||||
count int // Number of occurrences
|
||||
days_of_week []int // 0=Sunday, 1=Monday, etc.
|
||||
day_of_month int
|
||||
}
|
||||
|
||||
// RecurrenceFrequency for agenda recurrence
|
||||
pub enum RecurrenceFrequency {
|
||||
none
|
||||
daily
|
||||
weekly
|
||||
monthly
|
||||
yearly
|
||||
}
|
||||
|
||||
// UserPreferences for user-specific settings
|
||||
pub struct UserPreferences {
|
||||
pub mut:
|
||||
timezone string = 'UTC'
|
||||
date_format string = 'YYYY-MM-DD'
|
||||
time_format string = '24h'
|
||||
language string = 'en'
|
||||
theme string = 'light'
|
||||
notifications_email bool = true
|
||||
notifications_push bool = true
|
||||
default_view string = 'kanban'
|
||||
}
|
||||
|
||||
// ProjectRole represents a user's role in a specific project
|
||||
pub struct ProjectRole {
|
||||
pub mut:
|
||||
user_id int @[required]
|
||||
project_id int @[required]
|
||||
role string @[required] // e.g., "lead", "developer", "tester"
|
||||
permissions []string // Specific permissions for this project
|
||||
assigned_at time.Time
|
||||
}
|
||||
|
||||
// TaskDependency represents dependencies between tasks
|
||||
pub struct TaskDependency {
|
||||
pub mut:
|
||||
id int
|
||||
task_id int @[required] // The dependent task
|
||||
depends_on_id int @[required] // The task it depends on
|
||||
dependency_type DependencyType
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
// DependencyType for task dependencies
|
||||
pub enum DependencyType {
|
||||
finish_to_start // Most common: predecessor must finish before successor starts
|
||||
start_to_start // Both tasks start at the same time
|
||||
finish_to_finish // Both tasks finish at the same time
|
||||
start_to_finish // Successor can't finish until predecessor starts
|
||||
}
|
||||
|
||||
// Label for flexible categorization
|
||||
pub struct Label {
|
||||
pub mut:
|
||||
id int
|
||||
name string @[required]
|
||||
color string // Hex color code
|
||||
description string
|
||||
created_at time.Time
|
||||
}
|
||||
364
lib/biz/planner/models/task.v
Normal file
364
lib/biz/planner/models/task.v
Normal file
@@ -0,0 +1,364 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Task represents a work item in the system
|
||||
pub struct Task {
|
||||
BaseModel
|
||||
pub mut:
|
||||
title string @[required]
|
||||
description string
|
||||
project_id int // Links to Project
|
||||
sprint_id int // Links to Sprint (optional)
|
||||
milestone_id int // Links to Milestone (optional)
|
||||
parent_task_id int // For subtasks
|
||||
assignee_id int // User ID of assignee
|
||||
reporter_id int // User ID who created the task
|
||||
status TaskStatus
|
||||
priority Priority
|
||||
task_type TaskType
|
||||
story_points int // For Scrum estimation
|
||||
estimated_hours f32
|
||||
actual_hours f32
|
||||
remaining_hours f32
|
||||
start_date time.Time
|
||||
due_date time.Time
|
||||
completed_date time.Time
|
||||
dependencies []TaskDependency // Tasks this depends on
|
||||
blocked_by []int // Task IDs that block this task
|
||||
blocks []int // Task IDs that this task blocks
|
||||
subtasks []int // Subtask IDs
|
||||
watchers []int // User IDs watching this task
|
||||
time_entries []TimeEntry
|
||||
comments []Comment
|
||||
attachments []Attachment
|
||||
acceptance_criteria []string
|
||||
definition_of_done []string
|
||||
labels []int // Label IDs
|
||||
epic_id int // Links to Epic (if applicable)
|
||||
component string // Component/module this task relates to
|
||||
version string // Target version/release
|
||||
environment string // Environment (dev, staging, prod)
|
||||
severity Severity // For bug tasks
|
||||
reproducible bool // For bug tasks
|
||||
steps_to_reproduce []string // For bug tasks
|
||||
expected_result string // For bug tasks
|
||||
actual_result string // For bug tasks
|
||||
browser string // For web-related tasks
|
||||
os string // Operating system
|
||||
device string // Device type
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// TaskDependency represents a dependency relationship between tasks
|
||||
pub struct TaskDependency {
|
||||
pub mut:
|
||||
task_id int
|
||||
depends_on_task_id int
|
||||
dependency_type DependencyType
|
||||
created_at time.Time
|
||||
created_by int
|
||||
}
|
||||
|
||||
// DependencyType for task dependencies
|
||||
pub enum DependencyType {
|
||||
finish_to_start // Task B cannot start until Task A finishes
|
||||
start_to_start // Task B cannot start until Task A starts
|
||||
finish_to_finish // Task B cannot finish until Task A finishes
|
||||
start_to_finish // Task B cannot finish until Task A starts
|
||||
}
|
||||
|
||||
// Severity for bug tasks
|
||||
pub enum Severity {
|
||||
trivial
|
||||
minor
|
||||
major
|
||||
critical
|
||||
blocker
|
||||
}
|
||||
|
||||
// is_overdue checks if the task is past its due date
|
||||
pub fn (t Task) is_overdue() bool {
|
||||
if t.due_date.unix == 0 || t.status in [.done, .cancelled] {
|
||||
return false
|
||||
}
|
||||
return time.now() > t.due_date
|
||||
}
|
||||
|
||||
// is_blocked checks if the task is blocked by other tasks
|
||||
pub fn (t Task) is_blocked() bool {
|
||||
return t.blocked_by.len > 0
|
||||
}
|
||||
|
||||
// get_duration returns the planned duration in hours
|
||||
pub fn (t Task) get_duration() f32 {
|
||||
if t.start_date.unix == 0 || t.due_date.unix == 0 {
|
||||
return t.estimated_hours
|
||||
}
|
||||
hours := f32((t.due_date.unix - t.start_date.unix) / 3600) // 3600 seconds in an hour
|
||||
return hours
|
||||
}
|
||||
|
||||
// get_actual_duration returns the actual duration in hours
|
||||
pub fn (t Task) get_actual_duration() f32 {
|
||||
return t.actual_hours
|
||||
}
|
||||
|
||||
// get_progress returns the task progress as a percentage (0.0 to 1.0)
|
||||
pub fn (t Task) get_progress() f32 {
|
||||
if t.estimated_hours == 0 {
|
||||
match t.status {
|
||||
.done { return 1.0 }
|
||||
.in_progress { return 0.5 }
|
||||
else { return 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
if t.actual_hours >= t.estimated_hours {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
return t.actual_hours / t.estimated_hours
|
||||
}
|
||||
|
||||
// get_remaining_work returns the estimated remaining work in hours
|
||||
pub fn (t Task) get_remaining_work() f32 {
|
||||
if t.remaining_hours > 0 {
|
||||
return t.remaining_hours
|
||||
}
|
||||
|
||||
if t.estimated_hours > t.actual_hours {
|
||||
return t.estimated_hours - t.actual_hours
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// add_dependency adds a dependency to this task
|
||||
pub fn (mut t Task) add_dependency(depends_on_task_id int, dep_type DependencyType, by_user_id int) {
|
||||
// Check if dependency already exists
|
||||
for dep in t.dependencies {
|
||||
if dep.depends_on_task_id == depends_on_task_id {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.dependencies << TaskDependency{
|
||||
task_id: t.id
|
||||
depends_on_task_id: depends_on_task_id
|
||||
dependency_type: dep_type
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_dependency removes a dependency from this task
|
||||
pub fn (mut t Task) remove_dependency(depends_on_task_id int, by_user_id int) bool {
|
||||
for i, dep in t.dependencies {
|
||||
if dep.depends_on_task_id == depends_on_task_id {
|
||||
t.dependencies.delete(i)
|
||||
t.update_timestamp(by_user_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// add_blocker adds a task that blocks this task
|
||||
pub fn (mut t Task) add_blocker(blocker_task_id int, by_user_id int) {
|
||||
if blocker_task_id !in t.blocked_by {
|
||||
t.blocked_by << blocker_task_id
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// remove_blocker removes a blocking task
|
||||
pub fn (mut t Task) remove_blocker(blocker_task_id int, by_user_id int) {
|
||||
t.blocked_by = t.blocked_by.filter(it != blocker_task_id)
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_subtask adds a subtask to this task
|
||||
pub fn (mut t Task) add_subtask(subtask_id int, by_user_id int) {
|
||||
if subtask_id !in t.subtasks {
|
||||
t.subtasks << subtask_id
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// remove_subtask removes a subtask from this task
|
||||
pub fn (mut t Task) remove_subtask(subtask_id int, by_user_id int) {
|
||||
t.subtasks = t.subtasks.filter(it != subtask_id)
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// assign_to assigns the task to a user
|
||||
pub fn (mut t Task) assign_to(user_id int, by_user_id int) {
|
||||
t.assignee_id = user_id
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// unassign removes the assignee from the task
|
||||
pub fn (mut t Task) unassign(by_user_id int) {
|
||||
t.assignee_id = 0
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_watcher adds a user to watch this task
|
||||
pub fn (mut t Task) add_watcher(user_id int, by_user_id int) {
|
||||
if user_id !in t.watchers {
|
||||
t.watchers << user_id
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// remove_watcher removes a user from watching this task
|
||||
pub fn (mut t Task) remove_watcher(user_id int, by_user_id int) {
|
||||
t.watchers = t.watchers.filter(it != user_id)
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// start_work starts work on the task
|
||||
pub fn (mut t Task) start_work(by_user_id int) {
|
||||
t.status = .in_progress
|
||||
if t.start_date.unix == 0 {
|
||||
t.start_date = time.now()
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// complete_task marks the task as completed
|
||||
pub fn (mut t Task) complete_task(by_user_id int) {
|
||||
t.status = .done
|
||||
t.completed_date = time.now()
|
||||
t.remaining_hours = 0
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// reopen_task reopens a completed task
|
||||
pub fn (mut t Task) reopen_task(by_user_id int) {
|
||||
t.status = .todo
|
||||
t.completed_date = time.Time{}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// cancel_task cancels the task
|
||||
pub fn (mut t Task) cancel_task(by_user_id int) {
|
||||
t.status = .cancelled
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// log_time adds a time entry to the task
|
||||
pub fn (mut t Task) log_time(user_id int, hours f32, description string, date time.Time, by_user_id int) {
|
||||
t.time_entries << TimeEntry{
|
||||
user_id: user_id
|
||||
hours: hours
|
||||
description: description
|
||||
date: date
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
t.actual_hours += hours
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_remaining_hours updates the remaining work estimate
|
||||
pub fn (mut t Task) update_remaining_hours(hours f32, by_user_id int) {
|
||||
t.remaining_hours = hours
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_comment adds a comment to the task
|
||||
pub fn (mut t Task) add_comment(user_id int, content string, by_user_id int) {
|
||||
t.comments << Comment{
|
||||
user_id: user_id
|
||||
content: content
|
||||
created_at: time.now()
|
||||
created_by: by_user_id
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_attachment adds an attachment to the task
|
||||
pub fn (mut t Task) add_attachment(filename string, file_path string, file_size int, mime_type string, by_user_id int) {
|
||||
t.attachments << Attachment{
|
||||
filename: filename
|
||||
file_path: file_path
|
||||
file_size: file_size
|
||||
mime_type: mime_type
|
||||
uploaded_at: time.now()
|
||||
uploaded_by: by_user_id
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_acceptance_criteria adds acceptance criteria to the task
|
||||
pub fn (mut t Task) add_acceptance_criteria(criteria string, by_user_id int) {
|
||||
t.acceptance_criteria << criteria
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_acceptance_criteria removes acceptance criteria from the task
|
||||
pub fn (mut t Task) remove_acceptance_criteria(index int, by_user_id int) {
|
||||
if index >= 0 && index < t.acceptance_criteria.len {
|
||||
t.acceptance_criteria.delete(index)
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
// set_story_points sets the story points for the task
|
||||
pub fn (mut t Task) set_story_points(points int, by_user_id int) {
|
||||
t.story_points = points
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// set_due_date sets the due date for the task
|
||||
pub fn (mut t Task) set_due_date(due_date time.Time, by_user_id int) {
|
||||
t.due_date = due_date
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_velocity returns the velocity (story points / actual hours)
|
||||
pub fn (t Task) calculate_velocity() f32 {
|
||||
if t.actual_hours == 0 || t.story_points == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32(t.story_points) / t.actual_hours
|
||||
}
|
||||
|
||||
// is_bug checks if the task is a bug
|
||||
pub fn (t Task) is_bug() bool {
|
||||
return t.task_type == .bug
|
||||
}
|
||||
|
||||
// is_story checks if the task is a user story
|
||||
pub fn (t Task) is_story() bool {
|
||||
return t.task_type == .story
|
||||
}
|
||||
|
||||
// is_epic checks if the task is an epic
|
||||
pub fn (t Task) is_epic() bool {
|
||||
return t.task_type == .epic
|
||||
}
|
||||
|
||||
// get_age returns the age of the task in days
|
||||
pub fn (t Task) get_age() int {
|
||||
return int((time.now().unix - t.created_at.unix) / 86400)
|
||||
}
|
||||
|
||||
// get_cycle_time returns the cycle time (time from start to completion) in hours
|
||||
pub fn (t Task) get_cycle_time() f32 {
|
||||
if t.start_date.unix == 0 || t.completed_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32((t.completed_date.unix - t.start_date.unix) / 3600)
|
||||
}
|
||||
|
||||
// get_lead_time returns the lead time (time from creation to completion) in hours
|
||||
pub fn (t Task) get_lead_time() f32 {
|
||||
if t.completed_date.unix == 0 {
|
||||
return 0
|
||||
}
|
||||
return f32((t.completed_date.unix - t.created_at.unix) / 3600)
|
||||
}
|
||||
636
lib/biz/planner/models/team.v
Normal file
636
lib/biz/planner/models/team.v
Normal file
@@ -0,0 +1,636 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// Team represents a group of users working together
|
||||
pub struct Team {
|
||||
BaseModel
|
||||
pub mut:
|
||||
name string @[required]
|
||||
description string
|
||||
team_type TeamType
|
||||
status TeamStatus
|
||||
manager_id int // Team manager/lead
|
||||
members []TeamMember
|
||||
projects []int // Project IDs this team works on
|
||||
skills []TeamSkill // Skills available in this team
|
||||
capacity TeamCapacity
|
||||
location string
|
||||
time_zone string
|
||||
working_hours WorkingHours
|
||||
holidays []Holiday
|
||||
rituals []TeamRitual
|
||||
goals []TeamGoal
|
||||
metrics []TeamMetric
|
||||
budget f64 // Team budget
|
||||
cost_per_hour f64 // Average cost per hour
|
||||
utilization_target f32 // Target utilization percentage
|
||||
velocity_target int // Target velocity (story points per sprint)
|
||||
slack_channel string
|
||||
email_list string
|
||||
wiki_url string
|
||||
repository_urls []string
|
||||
tools []TeamTool
|
||||
custom_fields map[string]string
|
||||
}
|
||||
|
||||
// TeamType for categorizing teams
|
||||
pub enum TeamType {
|
||||
development
|
||||
qa
|
||||
design
|
||||
product
|
||||
marketing
|
||||
sales
|
||||
support
|
||||
operations
|
||||
security
|
||||
data
|
||||
research
|
||||
management
|
||||
cross_functional
|
||||
}
|
||||
|
||||
// TeamStatus for team lifecycle
|
||||
pub enum TeamStatus {
|
||||
forming
|
||||
storming
|
||||
norming
|
||||
performing
|
||||
adjourning
|
||||
disbanded
|
||||
}
|
||||
|
||||
// TeamMember represents a user's membership in a team
|
||||
pub struct TeamMember {
|
||||
pub mut:
|
||||
user_id int
|
||||
team_id int
|
||||
role string
|
||||
permissions []string
|
||||
capacity_hours f32 // Weekly capacity in hours
|
||||
allocation f32 // Percentage allocation to this team (0.0 to 1.0)
|
||||
hourly_rate f64 // Member's hourly rate
|
||||
start_date time.Time
|
||||
end_date time.Time // For temporary members
|
||||
status MemberStatus
|
||||
skills []int // Skill IDs
|
||||
certifications []string
|
||||
seniority_level SeniorityLevel
|
||||
performance_rating f32 // 1.0 to 5.0 scale
|
||||
last_review time.Time
|
||||
notes string
|
||||
}
|
||||
|
||||
// MemberStatus for team member status
|
||||
pub enum MemberStatus {
|
||||
active
|
||||
inactive
|
||||
on_leave
|
||||
temporary
|
||||
contractor
|
||||
intern
|
||||
}
|
||||
|
||||
// SeniorityLevel for team member experience
|
||||
pub enum SeniorityLevel {
|
||||
intern
|
||||
junior
|
||||
mid_level
|
||||
senior
|
||||
lead
|
||||
principal
|
||||
architect
|
||||
}
|
||||
|
||||
// TeamSkill represents a skill available in the team
|
||||
pub struct TeamSkill {
|
||||
pub mut:
|
||||
skill_id int
|
||||
team_id int
|
||||
skill_name string
|
||||
category string
|
||||
proficiency_levels map[int]SkillLevel // user_id -> proficiency level
|
||||
demand f32 // How much this skill is needed (0.0 to 1.0)
|
||||
supply f32 // How much this skill is available (0.0 to 1.0)
|
||||
gap f32 // Skill gap (demand - supply)
|
||||
training_plan string
|
||||
}
|
||||
|
||||
// TeamCapacity represents team capacity planning
|
||||
pub struct TeamCapacity {
|
||||
pub mut:
|
||||
total_hours_per_week f32
|
||||
available_hours_per_week f32
|
||||
committed_hours_per_week f32
|
||||
utilization_percentage f32
|
||||
velocity_last_sprint int
|
||||
velocity_average int
|
||||
velocity_trend f32 // Positive = improving, negative = declining
|
||||
capacity_by_skill map[string]f32 // skill -> available hours
|
||||
capacity_forecast []CapacityForecast
|
||||
}
|
||||
|
||||
// CapacityForecast for future capacity planning
|
||||
pub struct CapacityForecast {
|
||||
pub mut:
|
||||
period_start time.Time
|
||||
period_end time.Time
|
||||
forecast_type ForecastType
|
||||
total_capacity f32
|
||||
available_capacity f32
|
||||
planned_allocation f32
|
||||
confidence_level f32 // 0.0 to 1.0
|
||||
assumptions []string
|
||||
risks []string
|
||||
}
|
||||
|
||||
// ForecastType for capacity forecasting
|
||||
pub enum ForecastType {
|
||||
weekly
|
||||
monthly
|
||||
quarterly
|
||||
yearly
|
||||
}
|
||||
|
||||
// WorkingHours represents team working schedule
|
||||
pub struct WorkingHours {
|
||||
pub mut:
|
||||
monday_start string // "09:00"
|
||||
monday_end string // "17:00"
|
||||
tuesday_start string
|
||||
tuesday_end string
|
||||
wednesday_start string
|
||||
wednesday_end string
|
||||
thursday_start string
|
||||
thursday_end string
|
||||
friday_start string
|
||||
friday_end string
|
||||
saturday_start string
|
||||
saturday_end string
|
||||
sunday_start string
|
||||
sunday_end string
|
||||
break_duration int // Minutes
|
||||
lunch_duration int // Minutes
|
||||
flexible_hours bool
|
||||
core_hours_start string
|
||||
core_hours_end string
|
||||
}
|
||||
|
||||
// Holiday represents team holidays and time off
|
||||
pub struct Holiday {
|
||||
pub mut:
|
||||
name string
|
||||
date time.Time
|
||||
end_date time.Time // For multi-day holidays
|
||||
holiday_type HolidayType
|
||||
affects_members []int // User IDs affected (empty = all)
|
||||
description string
|
||||
}
|
||||
|
||||
// HolidayType for categorizing holidays
|
||||
pub enum HolidayType {
|
||||
public
|
||||
company
|
||||
team
|
||||
personal
|
||||
sick_leave
|
||||
vacation
|
||||
training
|
||||
conference
|
||||
}
|
||||
|
||||
// TeamRitual represents recurring team activities
|
||||
pub struct TeamRitual {
|
||||
pub mut:
|
||||
id int
|
||||
team_id int
|
||||
name string
|
||||
description string
|
||||
ritual_type RitualType
|
||||
frequency RitualFrequency
|
||||
duration_minutes int
|
||||
participants []int // User IDs
|
||||
facilitator_id int
|
||||
location string
|
||||
virtual_link string
|
||||
agenda string
|
||||
outcomes []string
|
||||
next_occurrence time.Time
|
||||
last_occurrence time.Time
|
||||
active bool
|
||||
}
|
||||
|
||||
// RitualType for categorizing team rituals
|
||||
pub enum RitualType {
|
||||
standup
|
||||
retrospective
|
||||
planning
|
||||
review
|
||||
one_on_one
|
||||
team_meeting
|
||||
training
|
||||
social
|
||||
demo
|
||||
sync
|
||||
}
|
||||
|
||||
// RitualFrequency for ritual scheduling
|
||||
pub enum RitualFrequency {
|
||||
daily
|
||||
weekly
|
||||
biweekly
|
||||
monthly
|
||||
quarterly
|
||||
ad_hoc
|
||||
}
|
||||
|
||||
// TeamGoal represents team objectives
|
||||
pub struct TeamGoal {
|
||||
pub mut:
|
||||
id int
|
||||
team_id int
|
||||
title string
|
||||
description string
|
||||
goal_type GoalType
|
||||
target_value f64
|
||||
current_value f64
|
||||
unit string
|
||||
start_date time.Time
|
||||
target_date time.Time
|
||||
status GoalStatus
|
||||
owner_id int
|
||||
progress f32 // 0.0 to 1.0
|
||||
milestones []GoalMilestone
|
||||
success_criteria []string
|
||||
}
|
||||
|
||||
// GoalType for categorizing team goals
|
||||
pub enum GoalType {
|
||||
performance
|
||||
quality
|
||||
delivery
|
||||
learning
|
||||
process
|
||||
culture
|
||||
business
|
||||
technical
|
||||
}
|
||||
|
||||
// GoalStatus for goal tracking
|
||||
pub enum GoalStatus {
|
||||
draft
|
||||
active
|
||||
achieved
|
||||
missed
|
||||
cancelled
|
||||
deferred
|
||||
}
|
||||
|
||||
// GoalMilestone represents milestones within team goals
|
||||
pub struct GoalMilestone {
|
||||
pub mut:
|
||||
title string
|
||||
target_date time.Time
|
||||
target_value f64
|
||||
achieved bool
|
||||
achieved_date time.Time
|
||||
achieved_value f64
|
||||
}
|
||||
|
||||
// TeamMetric represents team performance metrics
|
||||
pub struct TeamMetric {
|
||||
pub mut:
|
||||
id int
|
||||
team_id int
|
||||
name string
|
||||
description string
|
||||
metric_type MetricType
|
||||
current_value f64
|
||||
target_value f64
|
||||
unit string
|
||||
trend f32 // Positive = improving
|
||||
last_updated time.Time
|
||||
history []MetricDataPoint
|
||||
benchmark f64 // Industry/company benchmark
|
||||
}
|
||||
|
||||
// MetricDataPoint for metric history
|
||||
pub struct MetricDataPoint {
|
||||
pub mut:
|
||||
timestamp time.Time
|
||||
value f64
|
||||
period string // "2024-Q1", "2024-01", etc.
|
||||
}
|
||||
|
||||
// TeamTool represents tools used by the team
|
||||
pub struct TeamTool {
|
||||
pub mut:
|
||||
name string
|
||||
category ToolCategory
|
||||
url string
|
||||
description string
|
||||
cost_per_month f64
|
||||
licenses int
|
||||
admin_contact string
|
||||
renewal_date time.Time
|
||||
satisfaction_rating f32 // 1.0 to 5.0
|
||||
}
|
||||
|
||||
// ToolCategory for categorizing team tools
|
||||
pub enum ToolCategory {
|
||||
development
|
||||
testing
|
||||
design
|
||||
communication
|
||||
project_management
|
||||
documentation
|
||||
monitoring
|
||||
deployment
|
||||
security
|
||||
analytics
|
||||
}
|
||||
|
||||
// get_total_capacity returns total team capacity in hours per week
|
||||
pub fn (t Team) get_total_capacity() f32 {
|
||||
mut total := f32(0)
|
||||
for member in t.members {
|
||||
if member.status == .active {
|
||||
total += member.capacity_hours * member.allocation
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// get_available_capacity returns available capacity considering current commitments
|
||||
pub fn (t Team) get_available_capacity() f32 {
|
||||
total := t.get_total_capacity()
|
||||
return total - t.capacity.committed_hours_per_week
|
||||
}
|
||||
|
||||
// get_utilization returns current team utilization percentage
|
||||
pub fn (t Team) get_utilization() f32 {
|
||||
total := t.get_total_capacity()
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return (t.capacity.committed_hours_per_week / total) * 100
|
||||
}
|
||||
|
||||
// get_member_count returns the number of active team members
|
||||
pub fn (t Team) get_member_count() int {
|
||||
return t.members.filter(it.status == .active).len
|
||||
}
|
||||
|
||||
// get_average_seniority returns the average seniority level
|
||||
pub fn (t Team) get_average_seniority() f32 {
|
||||
active_members := t.members.filter(it.status == .active)
|
||||
if active_members.len == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
mut total := f32(0)
|
||||
for member in active_members {
|
||||
match member.seniority_level {
|
||||
.intern { total += 1 }
|
||||
.junior { total += 2 }
|
||||
.mid_level { total += 3 }
|
||||
.senior { total += 4 }
|
||||
.lead { total += 5 }
|
||||
.principal { total += 6 }
|
||||
.architect { total += 7 }
|
||||
}
|
||||
}
|
||||
|
||||
return total / f32(active_members.len)
|
||||
}
|
||||
|
||||
// add_member adds a member to the team
|
||||
pub fn (mut t Team) add_member(user_id int, role string, capacity_hours f32, allocation f32, hourly_rate f64, seniority_level SeniorityLevel, by_user_id int) {
|
||||
// Check if member already exists
|
||||
for i, member in t.members {
|
||||
if member.user_id == user_id {
|
||||
// Update existing member
|
||||
t.members[i].role = role
|
||||
t.members[i].capacity_hours = capacity_hours
|
||||
t.members[i].allocation = allocation
|
||||
t.members[i].hourly_rate = hourly_rate
|
||||
t.members[i].seniority_level = seniority_level
|
||||
t.members[i].status = .active
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new member
|
||||
t.members << TeamMember{
|
||||
user_id: user_id
|
||||
team_id: t.id
|
||||
role: role
|
||||
capacity_hours: capacity_hours
|
||||
allocation: allocation
|
||||
hourly_rate: hourly_rate
|
||||
start_date: time.now()
|
||||
status: .active
|
||||
seniority_level: seniority_level
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// remove_member removes a member from the team
|
||||
pub fn (mut t Team) remove_member(user_id int, by_user_id int) {
|
||||
for i, member in t.members {
|
||||
if member.user_id == user_id {
|
||||
t.members[i].status = .inactive
|
||||
t.members[i].end_date = time.now()
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update_member_capacity updates a member's capacity
|
||||
pub fn (mut t Team) update_member_capacity(user_id int, capacity_hours f32, allocation f32, by_user_id int) {
|
||||
for i, member in t.members {
|
||||
if member.user_id == user_id {
|
||||
t.members[i].capacity_hours = capacity_hours
|
||||
t.members[i].allocation = allocation
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add_skill adds a skill to the team
|
||||
pub fn (mut t Team) add_skill(skill_id int, skill_name string, category string, demand f32, by_user_id int) {
|
||||
// Check if skill already exists
|
||||
for i, skill in t.skills {
|
||||
if skill.skill_id == skill_id {
|
||||
t.skills[i].demand = demand
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.skills << TeamSkill{
|
||||
skill_id: skill_id
|
||||
team_id: t.id
|
||||
skill_name: skill_name
|
||||
category: category
|
||||
demand: demand
|
||||
proficiency_levels: map[int]SkillLevel{}
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_skill_proficiency updates a member's proficiency in a skill
|
||||
pub fn (mut t Team) update_skill_proficiency(skill_id int, user_id int, level SkillLevel, by_user_id int) {
|
||||
for i, mut skill in t.skills {
|
||||
if skill.skill_id == skill_id {
|
||||
t.skills[i].proficiency_levels[user_id] = level
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add_goal adds a goal to the team
|
||||
pub fn (mut t Team) add_goal(title string, description string, goal_type GoalType, target_value f64, unit string, target_date time.Time, owner_id int, by_user_id int) {
|
||||
t.goals << TeamGoal{
|
||||
id: t.goals.len + 1
|
||||
team_id: t.id
|
||||
title: title
|
||||
description: description
|
||||
goal_type: goal_type
|
||||
target_value: target_value
|
||||
unit: unit
|
||||
start_date: time.now()
|
||||
target_date: target_date
|
||||
status: .active
|
||||
owner_id: owner_id
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// update_goal_progress updates progress on a team goal
|
||||
pub fn (mut t Team) update_goal_progress(goal_id int, current_value f64, by_user_id int) {
|
||||
for i, mut goal in t.goals {
|
||||
if goal.id == goal_id {
|
||||
t.goals[i].current_value = current_value
|
||||
if goal.target_value > 0 {
|
||||
t.goals[i].progress = f32(current_value / goal.target_value)
|
||||
if t.goals[i].progress >= 1.0 {
|
||||
t.goals[i].status = .achieved
|
||||
}
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add_ritual adds a recurring ritual to the team
|
||||
pub fn (mut t Team) add_ritual(name string, description string, ritual_type RitualType, frequency RitualFrequency, duration_minutes int, facilitator_id int, by_user_id int) {
|
||||
t.rituals << TeamRitual{
|
||||
id: t.rituals.len + 1
|
||||
team_id: t.id
|
||||
name: name
|
||||
description: description
|
||||
ritual_type: ritual_type
|
||||
frequency: frequency
|
||||
duration_minutes: duration_minutes
|
||||
facilitator_id: facilitator_id
|
||||
active: true
|
||||
}
|
||||
t.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// calculate_team_health returns a team health score
|
||||
pub fn (t Team) calculate_team_health() f32 {
|
||||
mut score := f32(1.0)
|
||||
|
||||
// Utilization health (25% weight)
|
||||
utilization := t.get_utilization()
|
||||
if utilization < 70 || utilization > 90 {
|
||||
if utilization < 70 {
|
||||
score -= 0.25 * (70 - utilization) / 70
|
||||
} else {
|
||||
score -= 0.25 * (utilization - 90) / 90
|
||||
}
|
||||
}
|
||||
|
||||
// Velocity trend (25% weight)
|
||||
if t.capacity.velocity_trend < -0.1 {
|
||||
score -= 0.25 * (-t.capacity.velocity_trend)
|
||||
}
|
||||
|
||||
// Goal achievement (25% weight)
|
||||
active_goals := t.goals.filter(it.status == .active)
|
||||
if active_goals.len > 0 {
|
||||
mut avg_progress := f32(0)
|
||||
for goal in active_goals {
|
||||
avg_progress += goal.progress
|
||||
}
|
||||
avg_progress /= f32(active_goals.len)
|
||||
if avg_progress < 0.7 {
|
||||
score -= 0.25 * (0.7 - avg_progress)
|
||||
}
|
||||
}
|
||||
|
||||
// Team stability (25% weight)
|
||||
active_members := t.members.filter(it.status == .active)
|
||||
if active_members.len < 3 {
|
||||
score -= 0.25 * (3 - f32(active_members.len)) / 3
|
||||
}
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// get_health_status returns a human-readable health status
|
||||
pub fn (t Team) get_health_status() string {
|
||||
health := t.calculate_team_health()
|
||||
if health >= 0.8 {
|
||||
return 'Excellent'
|
||||
} else if health >= 0.6 {
|
||||
return 'Good'
|
||||
} else if health >= 0.4 {
|
||||
return 'At Risk'
|
||||
} else {
|
||||
return 'Critical'
|
||||
}
|
||||
}
|
||||
|
||||
// get_cost_per_week returns the team's cost per week
|
||||
pub fn (t Team) get_cost_per_week() f64 {
|
||||
mut total_cost := f64(0)
|
||||
for member in t.members {
|
||||
if member.status == .active {
|
||||
weekly_hours := member.capacity_hours * member.allocation
|
||||
total_cost += f64(weekly_hours) * member.hourly_rate
|
||||
}
|
||||
}
|
||||
return total_cost
|
||||
}
|
||||
|
||||
// forecast_capacity forecasts team capacity for a future period
|
||||
pub fn (t Team) forecast_capacity(start_date time.Time, end_date time.Time, forecast_type ForecastType) CapacityForecast {
|
||||
current_capacity := t.get_total_capacity()
|
||||
|
||||
// Simple forecast based on current capacity
|
||||
// In a real implementation, this would consider planned hires, departures, etc.
|
||||
return CapacityForecast{
|
||||
period_start: start_date
|
||||
period_end: end_date
|
||||
forecast_type: forecast_type
|
||||
total_capacity: current_capacity
|
||||
available_capacity: t.get_available_capacity()
|
||||
planned_allocation: t.capacity.committed_hours_per_week
|
||||
confidence_level: 0.8
|
||||
assumptions: ['Current team composition remains stable', 'No major holidays or time off']
|
||||
risks: ['Team member departures', 'Increased project demands']
|
||||
}
|
||||
}
|
||||
167
lib/biz/planner/models/user.v
Normal file
167
lib/biz/planner/models/user.v
Normal file
@@ -0,0 +1,167 @@
|
||||
module models
|
||||
|
||||
import time
|
||||
|
||||
// User represents a system user (employees, clients, etc.)
|
||||
pub struct User {
|
||||
BaseModel
|
||||
pub mut:
|
||||
username string @[required; unique]
|
||||
email string @[required; unique]
|
||||
first_name string @[required]
|
||||
last_name string @[required]
|
||||
display_name string
|
||||
avatar_url string
|
||||
role UserRole
|
||||
status UserStatus
|
||||
timezone string = 'UTC'
|
||||
preferences UserPreferences
|
||||
teams []int // Team IDs this user belongs to
|
||||
skills []string
|
||||
hourly_rate f64
|
||||
hire_date time.Time
|
||||
last_login time.Time
|
||||
password_hash string // For authentication
|
||||
phone string
|
||||
mobile string
|
||||
department string
|
||||
job_title string
|
||||
manager_id int // User ID of manager
|
||||
reports []int // User IDs of direct reports
|
||||
}
|
||||
|
||||
// get_full_name returns the user's full name
|
||||
pub fn (u User) get_full_name() string {
|
||||
return '${u.first_name} ${u.last_name}'
|
||||
}
|
||||
|
||||
// get_display_name returns the display name or full name if display name is empty
|
||||
pub fn (u User) get_display_name() string {
|
||||
if u.display_name.len > 0 {
|
||||
return u.display_name
|
||||
}
|
||||
return u.get_full_name()
|
||||
}
|
||||
|
||||
// is_admin checks if the user has admin role
|
||||
pub fn (u User) is_admin() bool {
|
||||
return u.role == .admin
|
||||
}
|
||||
|
||||
// is_project_manager checks if the user can manage projects
|
||||
pub fn (u User) is_project_manager() bool {
|
||||
return u.role in [.admin, .project_manager]
|
||||
}
|
||||
|
||||
// can_manage_users checks if the user can manage other users
|
||||
pub fn (u User) can_manage_users() bool {
|
||||
return u.role == .admin
|
||||
}
|
||||
|
||||
// add_skill adds a skill if it doesn't already exist
|
||||
pub fn (mut u User) add_skill(skill string) {
|
||||
if skill !in u.skills {
|
||||
u.skills << skill
|
||||
}
|
||||
}
|
||||
|
||||
// remove_skill removes a skill if it exists
|
||||
pub fn (mut u User) remove_skill(skill string) {
|
||||
u.skills = u.skills.filter(it != skill)
|
||||
}
|
||||
|
||||
// has_skill checks if the user has a specific skill
|
||||
pub fn (u User) has_skill(skill string) bool {
|
||||
return skill in u.skills
|
||||
}
|
||||
|
||||
// add_to_team adds the user to a team
|
||||
pub fn (mut u User) add_to_team(team_id int) {
|
||||
if team_id !in u.teams {
|
||||
u.teams << team_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_from_team removes the user from a team
|
||||
pub fn (mut u User) remove_from_team(team_id int) {
|
||||
u.teams = u.teams.filter(it != team_id)
|
||||
}
|
||||
|
||||
// is_in_team checks if the user is in a specific team
|
||||
pub fn (u User) is_in_team(team_id int) bool {
|
||||
return team_id in u.teams
|
||||
}
|
||||
|
||||
// update_last_login updates the last login timestamp
|
||||
pub fn (mut u User) update_last_login() {
|
||||
u.last_login = time.now()
|
||||
}
|
||||
|
||||
// is_active checks if the user is active and not suspended
|
||||
pub fn (u User) is_active() bool {
|
||||
return u.status == .active && u.is_active
|
||||
}
|
||||
|
||||
// suspend suspends the user account
|
||||
pub fn (mut u User) suspend(by_user_id int) {
|
||||
u.status = .suspended
|
||||
u.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// activate activates the user account
|
||||
pub fn (mut u User) activate(by_user_id int) {
|
||||
u.status = .active
|
||||
u.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// set_manager sets the user's manager
|
||||
pub fn (mut u User) set_manager(manager_id int, by_user_id int) {
|
||||
u.manager_id = manager_id
|
||||
u.update_timestamp(by_user_id)
|
||||
}
|
||||
|
||||
// add_report adds a direct report
|
||||
pub fn (mut u User) add_report(report_id int) {
|
||||
if report_id !in u.reports {
|
||||
u.reports << report_id
|
||||
}
|
||||
}
|
||||
|
||||
// remove_report removes a direct report
|
||||
pub fn (mut u User) remove_report(report_id int) {
|
||||
u.reports = u.reports.filter(it != report_id)
|
||||
}
|
||||
|
||||
// get_initials returns the user's initials
|
||||
pub fn (u User) get_initials() string {
|
||||
mut initials := ''
|
||||
if u.first_name.len > 0 {
|
||||
initials += u.first_name[0].ascii_str()
|
||||
}
|
||||
if u.last_name.len > 0 {
|
||||
initials += u.last_name[0].ascii_str()
|
||||
}
|
||||
return initials.to_upper()
|
||||
}
|
||||
|
||||
// calculate_total_hours calculates total hours worked in a time period
|
||||
pub fn (u User) calculate_total_hours(start_date time.Time, end_date time.Time, time_entries []TimeEntry) f32 {
|
||||
mut total := f32(0)
|
||||
for entry in time_entries {
|
||||
if entry.user_id == u.id && entry.start_time >= start_date && entry.end_time <= end_date {
|
||||
total += entry.duration
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// calculate_billable_hours calculates billable hours in a time period
|
||||
pub fn (u User) calculate_billable_hours(start_date time.Time, end_date time.Time, time_entries []TimeEntry) f32 {
|
||||
mut total := f32(0)
|
||||
for entry in time_entries {
|
||||
if entry.user_id == u.id && entry.billable && entry.start_time >= start_date && entry.end_time <= end_date {
|
||||
total += entry.duration
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
Reference in New Issue
Block a user