diff --git a/doc.vsh b/doc.vsh index 099a1c52..acceea21 100755 --- a/doc.vsh +++ b/doc.vsh @@ -1,4 +1,4 @@ -#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run import os diff --git a/lib/biz/planner/.gitignore b/lib/biz/planner/.gitignore new file mode 100644 index 00000000..98e6ef67 --- /dev/null +++ b/lib/biz/planner/.gitignore @@ -0,0 +1 @@ +*.db diff --git a/lib/biz/planner/README.md b/lib/biz/planner/README.md new file mode 100644 index 00000000..92eae417 --- /dev/null +++ b/lib/biz/planner/README.md @@ -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 \ No newline at end of file diff --git a/lib/biz/planner/examples/chat_orm_example.vsh b/lib/biz/planner/examples/chat_orm_example.vsh new file mode 100755 index 00000000..75adef9b --- /dev/null +++ b/lib/biz/planner/examples/chat_orm_example.vsh @@ -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) } \ No newline at end of file diff --git a/lib/biz/planner/examples/orm_instructions.md b/lib/biz/planner/examples/orm_instructions.md new file mode 100644 index 00000000..2561b741 --- /dev/null +++ b/lib/biz/planner/examples/orm_instructions.md @@ -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 [Function Call API](https://modules.vlang.io/orm.html#function-call-api). + +Here are a couple example structs showing most of the features outlined above. + + +``` +import time + +@[table: 'foos'] +struct Foo { + id int @[primary; sql: serial] + name string + created_at time.Time @[default: 'CURRENT_TIME'] + updated_at ?string @[sql_type: 'TIMESTAMP'] + deleted_at ?time.Time + children []Child @[fkey: 'parent_id'] +} + +struct Child { + id int @[primary; sql: serial] + parent_id int + name string +} +``` + + +To use the ORM, there is a special interface that lets you use the structs and V itself in queries. This interface takes the database instance as an argument. + + +``` +import db.sqlite + +db := sqlite.connect(':memory:')! + +sql db { + // query; see below +}! +``` + + +When you need to reference the table, simply pass the struct itself. + + +``` +import models.Foo + +struct Bar { + id int @[primary; sql: serial] +} + +sql db { + create table models.Foo + create table Bar +}! +``` + + + +### **Create & Drop Tables** + +You can create and drop tables by passing the struct to `create table` and `drop table`. + + +``` +import models.Foo + +struct Bar { + id int @[primary; sql: serial] +} + +sql db { + create table models.Foo + drop table Bar +}! +``` + + + +### **Insert Records** + +To insert a record, create a struct and pass the variable to the query. Again, reference the struct as the table. + + +``` +foo := Foo{ + name: 'abc' + created_at: time.now() + // updated_at defaults to none + // deleted_at defaults to none + children: [ + Child{ + name: 'abc' + }, + Child{ + name: 'def' + }, + ] +} + +foo_id := sql db { + insert foo into Foo +}! +``` + + +If the `id` field is marked as `sql: serial` and `primary`, the insert expression returns the database ID of the newly added object. Getting an ID of a newly added DB row is often useful. + +When inserting, `[sql: serial]` fields, and fields with a `[default: 'raw_sql']` attribute, are not sent to the database when the value being sent is the default for the V struct field (e.g., 0 int, or an empty string). This allows the database to insert default values for auto-increment fields and where you have specified a default. + + +### **Select** + +You can select rows from the database by passing the struct as the table, and use V syntax and functions for expressions. Selecting returns an array of the results. + + +``` +result := sql db { + select from Foo where id == 1 +}! + +foo := result.first() +result := sql db { + select from Foo where id > 1 && name != 'lasanha' limit 5 +}! +result := sql db { + select from Foo where id > 1 order by id +}! +``` + + + +### **Update** + +You can update fields in a row using V syntax and functions. Again, pass the struct as the table. + + +``` +sql db { + update Foo set updated_at = time.now() where name == 'abc' && updated_at is none +}! +``` + + +Note that `is none` and `!is none` can be used to select for NULL fields. + + +### **Delete** + +You can delete rows using V syntax and functions. Again, pass the struct as the table. + + +``` +sql db { + delete from Foo where id > 10 +}! +``` + + + +### **time.Time Fields** + +It's definitely useful to cast a field as `time.Time` so you can use V's built-in time functions; however, this is handled a bit differently than expected in the ORM. `time.Time` fields are created as integer columns in the database. Because of this, the usual time functions (`current_timestamp`, `NOW()`, etc) in SQL do not work as defaults. + + +## **Example** + + +``` +import db.pg + +struct Member { + id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid'] + name string + created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP'] +} + +fn main() { + db := pg.connect(pg.Config{ + host: 'localhost' + port: 5432 + user: 'user' + password: 'password' + dbname: 'dbname' + })! + + defer { + db.close() + } + + sql db { + create table Member + }! + + new_member := Member{ + name: 'John Doe' + } + + sql db { + insert new_member into Member + }! + + selected_members := sql db { + select from Member where name == 'John Doe' limit 1 + }! + john_doe := selected_members.first() + + sql db { + update Member set name = 'Hitalo' where id == john_doe.id + }! +} +``` + + + +## **Function Call API** + +You can utilize the `Function Call API` to work with `ORM`. It provides the capability to dynamically construct SQL statements. The Function Call API supports common operations such as `Create Table`/`Drop Table`/`Insert`/`Delete`/`Update`/`Select`, and offers convenient yet powerful features for constructing `WHERE` clauses, `SET` clauses, `SELECT` clauses, and more. + +A complete example is available [here](https://github.com/vlang/v/blob/master/vlib/orm/orm_func_test.v). + +Below, we illustrate its usage through several examples. + +​​1. Define your struct​​ with the same method definitions as before: + + +``` +@[table: 'sys_users'] +struct User { + id int @[primary;serial] + name string + age int + role string + status int + salary int + title string + score int + created_at ?time.Time @[sql_type: 'TIMESTAMP'] +} +``` + + +​​2. Create a database connection​​: + + +``` + mut db := sqlite.connect(':memory:')! + defer { db.close() or {} } + +``` + + + +1. Create a `QueryBuilder`​​ (which also completes struct mapping): + + +``` + mut qb := orm.new_query[User](db) + +``` + + + +1. Create a database table​​: + + +``` + qb.create()! + +``` + + + +1. Insert multiple records​​ into the table: + + +``` + qb.insert_many(users)! + +``` + + + +1. Delete records​​ (note: `delete()` must follow `where()`): + + +``` + qb.where('name = ?','John')!.delete()! + +``` + + + +1. Query records​​ (you can specify fields of interest via `select`): + + +``` +// Returns []User with only 'name' populated; other fields are zero values. + only_names := qb.select('name')!.query()! + +``` + + + +1. Update records​​ (note: `update()` must be placed last): + + +``` + qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()! + +``` + + + +1. Drop the table​​: + + +``` + qb.drop()! + +``` + + + +1. Chainable method calls​​: Most Function Call API support chainable calls, allowing easy method chaining: + + +``` + final_users := + qb + .drop()! + .create()! + .insert_many(users)! + .set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()! + .where('age >= ?', 30)!.delete()! + .query()! + +``` + + + +1. Writing complex nested `WHERE` clauses​​: The API includes a built-in parser to handle intricate `WHERE` clause conditions. For example: + + +``` + where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))', 2000, 30, '%employee%')! +``` + + +Note the use of placeholders `?`. The conditional expressions support logical operators including `AND`, `OR`, `||`, and `&&`. + + +## Constants [#](https://modules.vlang.io/orm.html#Constants) + + +## fn new_query [#](https://modules.vlang.io/orm.html#new_query) + +new_query create a new query object for struct `T` + + +## fn orm_select_gen [#](https://modules.vlang.io/orm.html#orm_select_gen) + +Generates an sql select stmt, from universal parameter orm - See SelectConfig q, num, qm, start_pos - see orm_stmt_gen where - See QueryData + + +## fn orm_stmt_gen [#](https://modules.vlang.io/orm.html#orm_stmt_gen) + +Generates an sql stmt, from universal parameter q - The quotes character, which can be different in every type, so it's variable num - Stmt uses nums at prepared statements (? or ?1) qm - Character for prepared statement (qm for question mark, as in sqlite) start_pos - When num is true, it's the start position of the counter + + +## fn orm_table_gen [#](https://modules.vlang.io/orm.html#orm_table_gen) + +Generates an sql table stmt, from universal parameter table - Table struct q - see orm_stmt_gen defaults - enables default values in stmt def_unique_len - sets default unique length for texts fields - See TableField sql_from_v - Function which maps type indices to sql type names alternative - Needed for msdb + + +## interface Connection [#](https://modules.vlang.io/orm.html#Connection) + +Interfaces gets called from the backend and can be implemented Since the orm supports arrays aswell, they have to be returned too. A row is represented as []Primitive, where the data is connected to the fields of the struct by their index. The indices are mapped with the SelectConfig.field array. This is the mapping for a struct. To have an array, there has to be an array of structs, basically [][]Primitive + +Every function without last_id() returns an optional, which returns an error if present last_id returns the last inserted id of the db + + +## type Primitive [#](https://modules.vlang.io/orm.html#Primitive) + + +## fn (QueryBuilder[T]) reset [#](https://modules.vlang.io/orm.html#QueryBuilder[T].reset) + +reset reset a query object, but keep the connection and table name + + +## fn (QueryBuilder[T]) where [#](https://modules.vlang.io/orm.html#QueryBuilder[T].where) + +where create a `where` clause, it will `AND` with previous `where` clause. valid token in the `condition` include: `field's names`, `operator`, `(`, `)`, `?`, `AND`, `OR`, `||`, `&&`, valid `operator` incldue: `=`, `!=`, `<>`, `>=`, `<=`, `>`, `<`, `LIKE`, `ILIKE`, `IS NULL`, `IS NOT NULL`, `IN`, `NOT IN` example: `where('(a > ? AND b <= ?) OR (c <> ? AND (x = ? OR y = ?))', a, b, c, x, y)` + + +## fn (QueryBuilder[T]) or_where [#](https://modules.vlang.io/orm.html#QueryBuilder[T].or_where) + +or_where create a `where` clause, it will `OR` with previous `where` clause. + + +## fn (QueryBuilder[T]) order [#](https://modules.vlang.io/orm.html#QueryBuilder[T].order) + +order create a `order` clause + + +## fn (QueryBuilder[T]) limit [#](https://modules.vlang.io/orm.html#QueryBuilder[T].limit) + +limit create a `limit` clause + + +## fn (QueryBuilder[T]) offset [#](https://modules.vlang.io/orm.html#QueryBuilder[T].offset) + +offset create a `offset` clause + + +## fn (QueryBuilder[T]) select [#](https://modules.vlang.io/orm.html#QueryBuilder[T].select) + +select create a `select` clause + + +## fn (QueryBuilder[T]) set [#](https://modules.vlang.io/orm.html#QueryBuilder[T].set) + +set create a `set` clause for `update` + + +## fn (QueryBuilder[T]) query [#](https://modules.vlang.io/orm.html#QueryBuilder[T].query) + +query start a query and return result in struct `T` + + +## fn (QueryBuilder[T]) count [#](https://modules.vlang.io/orm.html#QueryBuilder[T].count) + +count start a count query and return result + + +## fn (QueryBuilder[T]) insert [#](https://modules.vlang.io/orm.html#QueryBuilder[T].insert) + +insert insert a record into the database + + +## fn (QueryBuilder[T]) insert_many [#](https://modules.vlang.io/orm.html#QueryBuilder[T].insert_many) + +insert_many insert records into the database + + +## fn (QueryBuilder[T]) update [#](https://modules.vlang.io/orm.html#QueryBuilder[T].update) + +update update record(s) in the database + + +## fn (QueryBuilder[T]) delete [#](https://modules.vlang.io/orm.html#QueryBuilder[T].delete) + +delete delete record(s) in the database + + +## fn (QueryBuilder[T]) create [#](https://modules.vlang.io/orm.html#QueryBuilder[T].create) + +create create a table + + +## fn (QueryBuilder[T]) drop [#](https://modules.vlang.io/orm.html#QueryBuilder[T].drop) + +drop drop a table + + +## fn (QueryBuilder[T]) last_id [#](https://modules.vlang.io/orm.html#QueryBuilder[T].last_id) + +last_id returns the last inserted id of the db + + +## enum MathOperationKind [#](https://modules.vlang.io/orm.html#MathOperationKind) + + +## enum OperationKind [#](https://modules.vlang.io/orm.html#OperationKind) + + +## enum OrderType [#](https://modules.vlang.io/orm.html#OrderType) + + +## enum SQLDialect [#](https://modules.vlang.io/orm.html#SQLDialect) + + +## enum StmtKind [#](https://modules.vlang.io/orm.html#StmtKind) + + +## struct InfixType [#](https://modules.vlang.io/orm.html#InfixType) + + +## struct Null [#](https://modules.vlang.io/orm.html#Null) + + +## struct QueryBuilder [#](https://modules.vlang.io/orm.html#QueryBuilder) + + +``` +@[heap] +``` + + + +## struct QueryData [#](https://modules.vlang.io/orm.html#QueryData) + +Examples for QueryData in SQL: abc == 3 && b == 'test' => fields[abc, b]; data[3, 'test']; types[index of int, index of string]; kinds[.eq, .eq]; is_and[true]; Every field, data, type & kind of operation in the expr share the same index in the arrays is_and defines how they're addicted to each other either and or or parentheses defines which fields will be inside () auto_fields are indexes of fields where db should generate a value when absent in an insert + + +## struct SelectConfig [#](https://modules.vlang.io/orm.html#SelectConfig) + +table - Table struct is_count - Either the data will be returned or an integer with the count has_where - Select all or use a where expr has_order - Order the results order - Name of the column which will be ordered order_type - Type of order (asc, desc) has_limit - Limits the output data primary - Name of the primary field has_offset - Add an offset to the result fields - Fields to select types - Types to select + + +## struct Table [#](https://modules.vlang.io/orm.html#Table) + + +## struct TableField [#](https://modules.vlang.io/orm.html#TableField) diff --git a/lib/biz/planner/models/agenda.v b/lib/biz/planner/models/agenda.v new file mode 100644 index 00000000..509b5e18 --- /dev/null +++ b/lib/biz/planner/models/agenda.v @@ -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 +} \ No newline at end of file diff --git a/lib/biz/planner/models/base.v b/lib/biz/planner/models/base.v new file mode 100644 index 00000000..ad9f83ea --- /dev/null +++ b/lib/biz/planner/models/base.v @@ -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) +} \ No newline at end of file diff --git a/lib/biz/planner/models/chat.v b/lib/biz/planner/models/chat.v new file mode 100644 index 00000000..d6967bed --- /dev/null +++ b/lib/biz/planner/models/chat.v @@ -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) +} \ No newline at end of file diff --git a/lib/biz/planner/models/customer.v b/lib/biz/planner/models/customer.v new file mode 100644 index 00000000..c8d2c1e1 --- /dev/null +++ b/lib/biz/planner/models/customer.v @@ -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 } +} \ No newline at end of file diff --git a/lib/biz/planner/models/enums.v b/lib/biz/planner/models/enums.v new file mode 100644 index 00000000..8ee86205 --- /dev/null +++ b/lib/biz/planner/models/enums.v @@ -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 +} \ No newline at end of file diff --git a/lib/biz/planner/models/issue.v b/lib/biz/planner/models/issue.v new file mode 100644 index 00000000..22d4246d --- /dev/null +++ b/lib/biz/planner/models/issue.v @@ -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' + } + } +} \ No newline at end of file diff --git a/lib/biz/planner/models/milestone.v b/lib/biz/planner/models/milestone.v new file mode 100644 index 00000000..d952e398 --- /dev/null +++ b/lib/biz/planner/models/milestone.v @@ -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' + } +} \ No newline at end of file diff --git a/lib/biz/planner/models/mod.v b/lib/biz/planner/models/mod.v new file mode 100644 index 00000000..41c615ce --- /dev/null +++ b/lib/biz/planner/models/mod.v @@ -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' + ] +} \ No newline at end of file diff --git a/lib/biz/planner/models/project.v b/lib/biz/planner/models/project.v new file mode 100644 index 00000000..8f2f683c --- /dev/null +++ b/lib/biz/planner/models/project.v @@ -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' + } +} \ No newline at end of file diff --git a/lib/biz/planner/models/sprint.v b/lib/biz/planner/models/sprint.v new file mode 100644 index 00000000..25ae4d1b --- /dev/null +++ b/lib/biz/planner/models/sprint.v @@ -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' + } +} \ No newline at end of file diff --git a/lib/biz/planner/models/subobjects.v b/lib/biz/planner/models/subobjects.v new file mode 100644 index 00000000..608e6357 --- /dev/null +++ b/lib/biz/planner/models/subobjects.v @@ -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 +} \ No newline at end of file diff --git a/lib/biz/planner/models/task.v b/lib/biz/planner/models/task.v new file mode 100644 index 00000000..e9103a91 --- /dev/null +++ b/lib/biz/planner/models/task.v @@ -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) +} \ No newline at end of file diff --git a/lib/biz/planner/models/team.v b/lib/biz/planner/models/team.v new file mode 100644 index 00000000..0196f692 --- /dev/null +++ b/lib/biz/planner/models/team.v @@ -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'] + } +} \ No newline at end of file diff --git a/lib/biz/planner/models/user.v b/lib/biz/planner/models/user.v new file mode 100644 index 00000000..141cfdfe --- /dev/null +++ b/lib/biz/planner/models/user.v @@ -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 +} \ No newline at end of file