feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules. Updates developer portal navigation and BYOEH page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
164
ai-compliance-sdk/internal/dsb/models.go
Normal file
164
ai-compliance-sdk/internal/dsb/models.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package dsb
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Core Models
|
||||
// ============================================================================
|
||||
|
||||
// Assignment represents a DSB-to-tenant assignment.
|
||||
type Assignment struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DSBUserID uuid.UUID `json:"dsb_user_id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
TenantName string `json:"tenant_name"` // populated via JOIN
|
||||
TenantSlug string `json:"tenant_slug"` // populated via JOIN
|
||||
Status string `json:"status"` // active, paused, terminated
|
||||
ContractStart time.Time `json:"contract_start"`
|
||||
ContractEnd *time.Time `json:"contract_end,omitempty"`
|
||||
MonthlyHoursBudget float64 `json:"monthly_hours_budget"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// HourEntry represents a DSB time tracking entry.
|
||||
type HourEntry struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
Date time.Time `json:"date"`
|
||||
Hours float64 `json:"hours"`
|
||||
Category string `json:"category"` // dsfa_review, consultation, audit, training, incident_response, documentation, meeting, other
|
||||
Description string `json:"description"`
|
||||
Billable bool `json:"billable"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Task represents a DSB task/work item.
|
||||
type Task struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"` // dsfa_review, dsr_response, incident_review, audit_preparation, policy_review, training, consultation, other
|
||||
Priority string `json:"priority"` // low, medium, high, urgent
|
||||
Status string `json:"status"` // open, in_progress, waiting, completed, cancelled
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Communication represents a DSB communication log entry.
|
||||
type Communication struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
Direction string `json:"direction"` // inbound, outbound
|
||||
Channel string `json:"channel"` // email, phone, meeting, portal, letter
|
||||
Subject string `json:"subject"`
|
||||
Content string `json:"content"`
|
||||
Participants string `json:"participants"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dashboard Models
|
||||
// ============================================================================
|
||||
|
||||
// DSBDashboard provides the aggregated overview for a DSB user.
|
||||
type DSBDashboard struct {
|
||||
Assignments []AssignmentOverview `json:"assignments"`
|
||||
TotalAssignments int `json:"total_assignments"`
|
||||
ActiveAssignments int `json:"active_assignments"`
|
||||
TotalHoursThisMonth float64 `json:"total_hours_this_month"`
|
||||
OpenTasks int `json:"open_tasks"`
|
||||
UrgentTasks int `json:"urgent_tasks"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
// AssignmentOverview enriches an Assignment with aggregated metrics.
|
||||
type AssignmentOverview struct {
|
||||
Assignment
|
||||
ComplianceScore int `json:"compliance_score"`
|
||||
HoursThisMonth float64 `json:"hours_this_month"`
|
||||
HoursBudget float64 `json:"hours_budget"`
|
||||
OpenTaskCount int `json:"open_task_count"`
|
||||
UrgentTaskCount int `json:"urgent_task_count"`
|
||||
NextDeadline *time.Time `json:"next_deadline,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request Models
|
||||
// ============================================================================
|
||||
|
||||
// CreateAssignmentRequest is the request body for creating an assignment.
|
||||
type CreateAssignmentRequest struct {
|
||||
DSBUserID uuid.UUID `json:"dsb_user_id" binding:"required"`
|
||||
TenantID uuid.UUID `json:"tenant_id" binding:"required"`
|
||||
Status string `json:"status"`
|
||||
ContractStart time.Time `json:"contract_start" binding:"required"`
|
||||
ContractEnd *time.Time `json:"contract_end,omitempty"`
|
||||
MonthlyHoursBudget float64 `json:"monthly_hours_budget"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// UpdateAssignmentRequest is the request body for updating an assignment.
|
||||
type UpdateAssignmentRequest struct {
|
||||
Status *string `json:"status,omitempty"`
|
||||
ContractEnd *time.Time `json:"contract_end,omitempty"`
|
||||
MonthlyHoursBudget *float64 `json:"monthly_hours_budget,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
// CreateHourEntryRequest is the request body for creating a time entry.
|
||||
type CreateHourEntryRequest struct {
|
||||
Date time.Time `json:"date" binding:"required"`
|
||||
Hours float64 `json:"hours" binding:"required"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
Billable *bool `json:"billable,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTaskRequest is the request body for creating a task.
|
||||
type CreateTaskRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
Priority string `json:"priority"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateTaskRequest is the request body for updating a task.
|
||||
type UpdateTaskRequest struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Category *string `json:"category,omitempty"`
|
||||
Priority *string `json:"priority,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
}
|
||||
|
||||
// CreateCommunicationRequest is the request body for creating a communication entry.
|
||||
type CreateCommunicationRequest struct {
|
||||
Direction string `json:"direction" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
Content string `json:"content"`
|
||||
Participants string `json:"participants"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Summary Models
|
||||
// ============================================================================
|
||||
|
||||
// HoursSummary provides aggregated hour statistics for an assignment.
|
||||
type HoursSummary struct {
|
||||
TotalHours float64 `json:"total_hours"`
|
||||
BillableHours float64 `json:"billable_hours"`
|
||||
ByCategory map[string]float64 `json:"by_category"`
|
||||
Period string `json:"period"` // YYYY-MM or "all"
|
||||
}
|
||||
510
ai-compliance-sdk/internal/dsb/store.go
Normal file
510
ai-compliance-sdk/internal/dsb/store.go
Normal file
@@ -0,0 +1,510 @@
|
||||
package dsb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/reporting"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Store provides database operations for the DSB portal.
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
reportingStore *reporting.Store
|
||||
}
|
||||
|
||||
// NewStore creates a new DSB store.
|
||||
func NewStore(pool *pgxpool.Pool, reportingStore *reporting.Store) *Store {
|
||||
return &Store{
|
||||
pool: pool,
|
||||
reportingStore: reportingStore,
|
||||
}
|
||||
}
|
||||
|
||||
// Pool returns the underlying connection pool for direct queries when needed.
|
||||
func (s *Store) Pool() *pgxpool.Pool {
|
||||
return s.pool
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dashboard
|
||||
// ============================================================================
|
||||
|
||||
// GetDashboard generates the aggregated DSB dashboard for a given DSB user.
|
||||
func (s *Store) GetDashboard(ctx context.Context, dsbUserID uuid.UUID) (*DSBDashboard, error) {
|
||||
assignments, err := s.ListAssignments(ctx, dsbUserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list assignments: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
currentMonth := now.Format("2006-01")
|
||||
|
||||
dashboard := &DSBDashboard{
|
||||
Assignments: make([]AssignmentOverview, 0, len(assignments)),
|
||||
GeneratedAt: now,
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
overview := AssignmentOverview{
|
||||
Assignment: a,
|
||||
HoursBudget: a.MonthlyHoursBudget,
|
||||
}
|
||||
|
||||
// Enrich with compliance score (error-tolerant)
|
||||
if s.reportingStore != nil {
|
||||
report, err := s.reportingStore.GenerateReport(ctx, a.TenantID)
|
||||
if err == nil && report != nil {
|
||||
overview.ComplianceScore = report.ComplianceScore
|
||||
}
|
||||
}
|
||||
|
||||
// Hours this month
|
||||
summary, err := s.GetHoursSummary(ctx, a.ID, currentMonth)
|
||||
if err == nil && summary != nil {
|
||||
overview.HoursThisMonth = summary.TotalHours
|
||||
}
|
||||
|
||||
// Open and urgent tasks
|
||||
openTasks, err := s.ListTasks(ctx, a.ID, "open")
|
||||
if err == nil {
|
||||
overview.OpenTaskCount = len(openTasks)
|
||||
for _, t := range openTasks {
|
||||
if t.Priority == "urgent" {
|
||||
overview.UrgentTaskCount++
|
||||
}
|
||||
if t.DueDate != nil && (overview.NextDeadline == nil || t.DueDate.Before(*overview.NextDeadline)) {
|
||||
overview.NextDeadline = t.DueDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also count in_progress tasks
|
||||
inProgressTasks, err := s.ListTasks(ctx, a.ID, "in_progress")
|
||||
if err == nil {
|
||||
overview.OpenTaskCount += len(inProgressTasks)
|
||||
for _, t := range inProgressTasks {
|
||||
if t.Priority == "urgent" {
|
||||
overview.UrgentTaskCount++
|
||||
}
|
||||
if t.DueDate != nil && (overview.NextDeadline == nil || t.DueDate.Before(*overview.NextDeadline)) {
|
||||
overview.NextDeadline = t.DueDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dashboard.Assignments = append(dashboard.Assignments, overview)
|
||||
dashboard.TotalAssignments++
|
||||
if a.Status == "active" {
|
||||
dashboard.ActiveAssignments++
|
||||
}
|
||||
dashboard.TotalHoursThisMonth += overview.HoursThisMonth
|
||||
dashboard.OpenTasks += overview.OpenTaskCount
|
||||
dashboard.UrgentTasks += overview.UrgentTaskCount
|
||||
}
|
||||
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Assignments
|
||||
// ============================================================================
|
||||
|
||||
// CreateAssignment inserts a new DSB assignment.
|
||||
func (s *Store) CreateAssignment(ctx context.Context, a *Assignment) error {
|
||||
a.ID = uuid.New()
|
||||
now := time.Now().UTC()
|
||||
a.CreatedAt = now
|
||||
a.UpdatedAt = now
|
||||
|
||||
if a.Status == "" {
|
||||
a.Status = "active"
|
||||
}
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO dsb_assignments (id, dsb_user_id, tenant_id, status, contract_start, contract_end, monthly_hours_budget, notes, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`, a.ID, a.DSBUserID, a.TenantID, a.Status, a.ContractStart, a.ContractEnd, a.MonthlyHoursBudget, a.Notes, a.CreatedAt, a.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert assignment: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAssignments returns all assignments for a given DSB user, joined with tenant info.
|
||||
func (s *Store) ListAssignments(ctx context.Context, dsbUserID uuid.UUID) ([]Assignment, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT a.id, a.dsb_user_id, a.tenant_id, ct.name, ct.slug,
|
||||
a.status, a.contract_start, a.contract_end,
|
||||
a.monthly_hours_budget, a.notes, a.created_at, a.updated_at
|
||||
FROM dsb_assignments a
|
||||
JOIN compliance_tenants ct ON ct.id = a.tenant_id
|
||||
WHERE a.dsb_user_id = $1
|
||||
ORDER BY a.created_at DESC
|
||||
`, dsbUserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query assignments: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var assignments []Assignment
|
||||
for rows.Next() {
|
||||
var a Assignment
|
||||
if err := rows.Scan(
|
||||
&a.ID, &a.DSBUserID, &a.TenantID, &a.TenantName, &a.TenantSlug,
|
||||
&a.Status, &a.ContractStart, &a.ContractEnd,
|
||||
&a.MonthlyHoursBudget, &a.Notes, &a.CreatedAt, &a.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan assignment: %w", err)
|
||||
}
|
||||
assignments = append(assignments, a)
|
||||
}
|
||||
|
||||
if assignments == nil {
|
||||
assignments = []Assignment{}
|
||||
}
|
||||
return assignments, nil
|
||||
}
|
||||
|
||||
// GetAssignment retrieves a single assignment by ID.
|
||||
func (s *Store) GetAssignment(ctx context.Context, id uuid.UUID) (*Assignment, error) {
|
||||
var a Assignment
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT a.id, a.dsb_user_id, a.tenant_id, ct.name, ct.slug,
|
||||
a.status, a.contract_start, a.contract_end,
|
||||
a.monthly_hours_budget, a.notes, a.created_at, a.updated_at
|
||||
FROM dsb_assignments a
|
||||
JOIN compliance_tenants ct ON ct.id = a.tenant_id
|
||||
WHERE a.id = $1
|
||||
`, id).Scan(
|
||||
&a.ID, &a.DSBUserID, &a.TenantID, &a.TenantName, &a.TenantSlug,
|
||||
&a.Status, &a.ContractStart, &a.ContractEnd,
|
||||
&a.MonthlyHoursBudget, &a.Notes, &a.CreatedAt, &a.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get assignment: %w", err)
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// UpdateAssignment updates an existing assignment.
|
||||
func (s *Store) UpdateAssignment(ctx context.Context, a *Assignment) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE dsb_assignments
|
||||
SET status = $2, contract_end = $3, monthly_hours_budget = $4, notes = $5, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, a.ID, a.Status, a.ContractEnd, a.MonthlyHoursBudget, a.Notes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update assignment: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hours
|
||||
// ============================================================================
|
||||
|
||||
// CreateHourEntry inserts a new time tracking entry.
|
||||
func (s *Store) CreateHourEntry(ctx context.Context, h *HourEntry) error {
|
||||
h.ID = uuid.New()
|
||||
h.CreatedAt = time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO dsb_hours (id, assignment_id, date, hours, category, description, billable, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, h.ID, h.AssignmentID, h.Date, h.Hours, h.Category, h.Description, h.Billable, h.CreatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert hour entry: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListHours returns time entries for an assignment, optionally filtered by month (YYYY-MM).
|
||||
func (s *Store) ListHours(ctx context.Context, assignmentID uuid.UUID, month string) ([]HourEntry, error) {
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if month != "" {
|
||||
query = `
|
||||
SELECT id, assignment_id, date, hours, category, description, billable, created_at
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1 AND to_char(date, 'YYYY-MM') = $2
|
||||
ORDER BY date DESC, created_at DESC
|
||||
`
|
||||
args = []interface{}{assignmentID, month}
|
||||
} else {
|
||||
query = `
|
||||
SELECT id, assignment_id, date, hours, category, description, billable, created_at
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1
|
||||
ORDER BY date DESC, created_at DESC
|
||||
`
|
||||
args = []interface{}{assignmentID}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query hours: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []HourEntry
|
||||
for rows.Next() {
|
||||
var h HourEntry
|
||||
if err := rows.Scan(
|
||||
&h.ID, &h.AssignmentID, &h.Date, &h.Hours, &h.Category,
|
||||
&h.Description, &h.Billable, &h.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan hour entry: %w", err)
|
||||
}
|
||||
entries = append(entries, h)
|
||||
}
|
||||
|
||||
if entries == nil {
|
||||
entries = []HourEntry{}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// GetHoursSummary returns aggregated hour statistics for an assignment, optionally filtered by month.
|
||||
func (s *Store) GetHoursSummary(ctx context.Context, assignmentID uuid.UUID, month string) (*HoursSummary, error) {
|
||||
summary := &HoursSummary{
|
||||
ByCategory: make(map[string]float64),
|
||||
Period: "all",
|
||||
}
|
||||
|
||||
if month != "" {
|
||||
summary.Period = month
|
||||
}
|
||||
|
||||
// Total and billable hours
|
||||
var totalQuery string
|
||||
var totalArgs []interface{}
|
||||
|
||||
if month != "" {
|
||||
totalQuery = `
|
||||
SELECT COALESCE(SUM(hours), 0), COALESCE(SUM(CASE WHEN billable THEN hours ELSE 0 END), 0)
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1 AND to_char(date, 'YYYY-MM') = $2
|
||||
`
|
||||
totalArgs = []interface{}{assignmentID, month}
|
||||
} else {
|
||||
totalQuery = `
|
||||
SELECT COALESCE(SUM(hours), 0), COALESCE(SUM(CASE WHEN billable THEN hours ELSE 0 END), 0)
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1
|
||||
`
|
||||
totalArgs = []interface{}{assignmentID}
|
||||
}
|
||||
|
||||
err := s.pool.QueryRow(ctx, totalQuery, totalArgs...).Scan(&summary.TotalHours, &summary.BillableHours)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query hours summary totals: %w", err)
|
||||
}
|
||||
|
||||
// Hours by category
|
||||
var catQuery string
|
||||
var catArgs []interface{}
|
||||
|
||||
if month != "" {
|
||||
catQuery = `
|
||||
SELECT category, COALESCE(SUM(hours), 0)
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1 AND to_char(date, 'YYYY-MM') = $2
|
||||
GROUP BY category
|
||||
`
|
||||
catArgs = []interface{}{assignmentID, month}
|
||||
} else {
|
||||
catQuery = `
|
||||
SELECT category, COALESCE(SUM(hours), 0)
|
||||
FROM dsb_hours
|
||||
WHERE assignment_id = $1
|
||||
GROUP BY category
|
||||
`
|
||||
catArgs = []interface{}{assignmentID}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, catQuery, catArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query hours by category: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var cat string
|
||||
var hours float64
|
||||
if err := rows.Scan(&cat, &hours); err != nil {
|
||||
return nil, fmt.Errorf("scan category hours: %w", err)
|
||||
}
|
||||
summary.ByCategory[cat] = hours
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tasks
|
||||
// ============================================================================
|
||||
|
||||
// CreateTask inserts a new DSB task.
|
||||
func (s *Store) CreateTask(ctx context.Context, t *Task) error {
|
||||
t.ID = uuid.New()
|
||||
now := time.Now().UTC()
|
||||
t.CreatedAt = now
|
||||
t.UpdatedAt = now
|
||||
|
||||
if t.Status == "" {
|
||||
t.Status = "open"
|
||||
}
|
||||
if t.Priority == "" {
|
||||
t.Priority = "medium"
|
||||
}
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO dsb_tasks (id, assignment_id, title, description, category, priority, status, due_date, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`, t.ID, t.AssignmentID, t.Title, t.Description, t.Category, t.Priority, t.Status, t.DueDate, t.CreatedAt, t.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert task: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTasks returns tasks for an assignment, optionally filtered by status.
|
||||
func (s *Store) ListTasks(ctx context.Context, assignmentID uuid.UUID, status string) ([]Task, error) {
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if status != "" {
|
||||
query = `
|
||||
SELECT id, assignment_id, title, description, category, priority, status, due_date, completed_at, created_at, updated_at
|
||||
FROM dsb_tasks
|
||||
WHERE assignment_id = $1 AND status = $2
|
||||
ORDER BY CASE priority
|
||||
WHEN 'urgent' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
ELSE 5
|
||||
END, due_date ASC NULLS LAST, created_at DESC
|
||||
`
|
||||
args = []interface{}{assignmentID, status}
|
||||
} else {
|
||||
query = `
|
||||
SELECT id, assignment_id, title, description, category, priority, status, due_date, completed_at, created_at, updated_at
|
||||
FROM dsb_tasks
|
||||
WHERE assignment_id = $1
|
||||
ORDER BY CASE priority
|
||||
WHEN 'urgent' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
ELSE 5
|
||||
END, due_date ASC NULLS LAST, created_at DESC
|
||||
`
|
||||
args = []interface{}{assignmentID}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query tasks: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tasks []Task
|
||||
for rows.Next() {
|
||||
var t Task
|
||||
if err := rows.Scan(
|
||||
&t.ID, &t.AssignmentID, &t.Title, &t.Description, &t.Category,
|
||||
&t.Priority, &t.Status, &t.DueDate, &t.CompletedAt,
|
||||
&t.CreatedAt, &t.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan task: %w", err)
|
||||
}
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
if tasks == nil {
|
||||
tasks = []Task{}
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// UpdateTask updates an existing task.
|
||||
func (s *Store) UpdateTask(ctx context.Context, t *Task) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE dsb_tasks
|
||||
SET title = $2, description = $3, category = $4, priority = $5, status = $6, due_date = $7, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, t.ID, t.Title, t.Description, t.Category, t.Priority, t.Status, t.DueDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update task: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteTask marks a task as completed with the current timestamp.
|
||||
func (s *Store) CompleteTask(ctx context.Context, taskID uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE dsb_tasks
|
||||
SET status = 'completed', completed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("complete task: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Communications
|
||||
// ============================================================================
|
||||
|
||||
// CreateCommunication inserts a new communication log entry.
|
||||
func (s *Store) CreateCommunication(ctx context.Context, c *Communication) error {
|
||||
c.ID = uuid.New()
|
||||
c.CreatedAt = time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO dsb_communications (id, assignment_id, direction, channel, subject, content, participants, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, c.ID, c.AssignmentID, c.Direction, c.Channel, c.Subject, c.Content, c.Participants, c.CreatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert communication: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCommunications returns all communication entries for an assignment.
|
||||
func (s *Store) ListCommunications(ctx context.Context, assignmentID uuid.UUID) ([]Communication, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, assignment_id, direction, channel, subject, content, participants, created_at
|
||||
FROM dsb_communications
|
||||
WHERE assignment_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`, assignmentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query communications: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var comms []Communication
|
||||
for rows.Next() {
|
||||
var c Communication
|
||||
if err := rows.Scan(
|
||||
&c.ID, &c.AssignmentID, &c.Direction, &c.Channel,
|
||||
&c.Subject, &c.Content, &c.Participants, &c.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan communication: %w", err)
|
||||
}
|
||||
comms = append(comms, c)
|
||||
}
|
||||
|
||||
if comms == nil {
|
||||
comms = []Communication{}
|
||||
}
|
||||
return comms, nil
|
||||
}
|
||||
Reference in New Issue
Block a user