feat: Phase A backend — notes CRUD, detail endpoints, dashboard fix
- Create kanzlai.notes table (polymorphic FK with CHECK constraint,
partial indexes, RLS)
- Add Note model, NoteService (ListByParent, Create, Update, Delete),
and NoteHandler with endpoints: GET/POST /api/notes, PUT/DELETE /api/notes/{id}
- Add GET /api/deadlines/{deadlineID} detail endpoint
- Add GET /api/appointments/{id} detail endpoint
- Add GET /api/case-events/{id} detail endpoint (new CaseEventHandler)
- Fix dashboard query: add case_id to upcoming_deadlines SELECT,
add id and case_id to recent_activity SELECT
- Register all new routes in router.go
This commit is contained in:
@@ -42,6 +42,7 @@ type UpcomingDeadline struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
DueDate string `json:"due_date" db:"due_date"`
|
||||
CaseID uuid.UUID `json:"case_id" db:"case_id"`
|
||||
CaseNumber string `json:"case_number" db:"case_number"`
|
||||
CaseTitle string `json:"case_title" db:"case_title"`
|
||||
Status string `json:"status" db:"status"`
|
||||
@@ -56,8 +57,10 @@ type UpcomingAppointment struct {
|
||||
}
|
||||
|
||||
type RecentActivity struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
EventType *string `json:"event_type" db:"event_type"`
|
||||
Title string `json:"title" db:"title"`
|
||||
CaseID uuid.UUID `json:"case_id" db:"case_id"`
|
||||
CaseNumber string `json:"case_number" db:"case_number"`
|
||||
EventDate *time.Time `json:"event_date" db:"event_date"`
|
||||
}
|
||||
@@ -109,7 +112,7 @@ func (s *DashboardService) Get(ctx context.Context, tenantID uuid.UUID) (*Dashbo
|
||||
|
||||
// Upcoming deadlines (next 7 days)
|
||||
deadlineQuery := `
|
||||
SELECT d.id, d.title, d.due_date, c.case_number, c.title AS case_title, d.status
|
||||
SELECT d.id, d.title, d.due_date, d.case_id, c.case_number, c.title AS case_title, d.status
|
||||
FROM deadlines d
|
||||
JOIN cases c ON c.id = d.case_id AND c.tenant_id = d.tenant_id
|
||||
WHERE d.tenant_id = $1 AND d.status = 'pending' AND d.due_date >= $2 AND d.due_date <= $3
|
||||
@@ -135,7 +138,7 @@ func (s *DashboardService) Get(ctx context.Context, tenantID uuid.UUID) (*Dashbo
|
||||
|
||||
// Recent activity (last 10 case events)
|
||||
activityQuery := `
|
||||
SELECT ce.event_type, ce.title, c.case_number, ce.event_date
|
||||
SELECT ce.id, ce.event_type, ce.title, ce.case_id, c.case_number, ce.event_date
|
||||
FROM case_events ce
|
||||
JOIN cases c ON c.id = ce.case_id AND c.tenant_id = ce.tenant_id
|
||||
WHERE ce.tenant_id = $1
|
||||
|
||||
120
backend/internal/services/note_service.go
Normal file
120
backend/internal/services/note_service.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
|
||||
)
|
||||
|
||||
type NoteService struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewNoteService(db *sqlx.DB) *NoteService {
|
||||
return &NoteService{db: db}
|
||||
}
|
||||
|
||||
// ListByParent returns all notes for a given parent entity, scoped to tenant.
|
||||
func (s *NoteService) ListByParent(ctx context.Context, tenantID uuid.UUID, parentType string, parentID uuid.UUID) ([]models.Note, error) {
|
||||
col, err := parentColumn(parentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
`SELECT id, tenant_id, case_id, deadline_id, appointment_id, case_event_id,
|
||||
content, created_by, created_at, updated_at
|
||||
FROM notes
|
||||
WHERE tenant_id = $1 AND %s = $2
|
||||
ORDER BY created_at DESC`, col)
|
||||
|
||||
var notes []models.Note
|
||||
if err := s.db.SelectContext(ctx, ¬es, query, tenantID, parentID); err != nil {
|
||||
return nil, fmt.Errorf("listing notes by %s: %w", parentType, err)
|
||||
}
|
||||
if notes == nil {
|
||||
notes = []models.Note{}
|
||||
}
|
||||
return notes, nil
|
||||
}
|
||||
|
||||
type CreateNoteInput struct {
|
||||
CaseID *uuid.UUID `json:"case_id,omitempty"`
|
||||
DeadlineID *uuid.UUID `json:"deadline_id,omitempty"`
|
||||
AppointmentID *uuid.UUID `json:"appointment_id,omitempty"`
|
||||
CaseEventID *uuid.UUID `json:"case_event_id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// Create inserts a new note.
|
||||
func (s *NoteService) Create(ctx context.Context, tenantID uuid.UUID, createdBy *uuid.UUID, input CreateNoteInput) (*models.Note, error) {
|
||||
id := uuid.New()
|
||||
now := time.Now().UTC()
|
||||
|
||||
query := `INSERT INTO notes (id, tenant_id, case_id, deadline_id, appointment_id, case_event_id, content, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9)
|
||||
RETURNING id, tenant_id, case_id, deadline_id, appointment_id, case_event_id, content, created_by, created_at, updated_at`
|
||||
|
||||
var n models.Note
|
||||
err := s.db.GetContext(ctx, &n, query,
|
||||
id, tenantID, input.CaseID, input.DeadlineID, input.AppointmentID, input.CaseEventID,
|
||||
input.Content, createdBy, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating note: %w", err)
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// Update modifies a note's content.
|
||||
func (s *NoteService) Update(ctx context.Context, tenantID, noteID uuid.UUID, content string) (*models.Note, error) {
|
||||
query := `UPDATE notes SET content = $1, updated_at = $2
|
||||
WHERE id = $3 AND tenant_id = $4
|
||||
RETURNING id, tenant_id, case_id, deadline_id, appointment_id, case_event_id, content, created_by, created_at, updated_at`
|
||||
|
||||
var n models.Note
|
||||
err := s.db.GetContext(ctx, &n, query, content, time.Now().UTC(), noteID, tenantID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("updating note: %w", err)
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// Delete removes a note.
|
||||
func (s *NoteService) Delete(ctx context.Context, tenantID, noteID uuid.UUID) error {
|
||||
result, err := s.db.ExecContext(ctx, "DELETE FROM notes WHERE id = $1 AND tenant_id = $2", noteID, tenantID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting note: %w", err)
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking delete result: %w", err)
|
||||
}
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("note not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parentColumn(parentType string) (string, error) {
|
||||
switch parentType {
|
||||
case "case":
|
||||
return "case_id", nil
|
||||
case "deadline":
|
||||
return "deadline_id", nil
|
||||
case "appointment":
|
||||
return "appointment_id", nil
|
||||
case "case_event":
|
||||
return "case_event_id", nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid parent type: %s", parentType)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user