This commit is contained in:
2025-07-12 20:37:13 +04:00
parent 545fd75d71
commit 0662b37915
19 changed files with 7011 additions and 1 deletions

View File

@@ -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
View File

@@ -0,0 +1 @@
*.db

303
lib/biz/planner/README.md Normal file
View 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

View 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) }

View 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: `=`, `!=`, `&lt;>`, `>=`, `&lt;=`, `>`, `&lt;`, `LIKE`, `ILIKE`, `IS NULL`, `IS NOT NULL`, `IN`, `NOT IN` example: `where('(a > ? AND b &lt;= ?) OR (c &lt;> ? 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)

View 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
}

View 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)
}

View 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)
}

View 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 }
}

View 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
}

View 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'
}
}
}

View 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'
}
}

View 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'
]
}

View 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'
}
}

View 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'
}
}

View 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
}

View 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)
}

View 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']
}
}

View 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
}