initial commit
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemoryRepository implements Repository with process-local memory only.
|
||||
// It is intended for web-only proof-of-concept deployments where data should
|
||||
// disappear periodically and never be written to disk.
|
||||
type MemoryRepository struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
nextCompanyID int64
|
||||
nextAssignmentID int64
|
||||
nextTaskID int64
|
||||
nextSessionID int64
|
||||
nextInvoiceID int64
|
||||
|
||||
companies map[int64]Company
|
||||
assignments map[int64]Assignment
|
||||
tasks map[int64]Task
|
||||
sessions map[int64]WorkSession
|
||||
invoices map[int64]Invoice
|
||||
}
|
||||
|
||||
func NewMemoryRepository() *MemoryRepository {
|
||||
repo := &MemoryRepository{}
|
||||
repo.Reset()
|
||||
return repo
|
||||
}
|
||||
|
||||
// Reset wipes every in-memory record and restarts all ids at 1.
|
||||
func (repo *MemoryRepository) Reset() {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
repo.nextCompanyID = 1
|
||||
repo.nextAssignmentID = 1
|
||||
repo.nextTaskID = 1
|
||||
repo.nextSessionID = 1
|
||||
repo.nextInvoiceID = 1
|
||||
|
||||
repo.companies = make(map[int64]Company)
|
||||
repo.assignments = make(map[int64]Assignment)
|
||||
repo.tasks = make(map[int64]Task)
|
||||
repo.sessions = make(map[int64]WorkSession)
|
||||
repo.invoices = make(map[int64]Invoice)
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) Migrate() error { return nil }
|
||||
|
||||
// ── Companies ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) InsertCompany(c Company) (*Company, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
c.ID = repo.nextCompanyID
|
||||
repo.nextCompanyID++
|
||||
c.CreatedAt = time.Now()
|
||||
repo.companies[c.ID] = c
|
||||
return copyCompany(c), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) AllCompanies() ([]Company, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
all := make([]Company, 0, len(repo.companies))
|
||||
for _, c := range repo.companies {
|
||||
all = append(all, c)
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return strings.ToLower(all[i].Name) < strings.ToLower(all[j].Name)
|
||||
})
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) GetCompanyByID(id int64) (*Company, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
c, ok := repo.companies[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return copyCompany(c), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) UpdateCompany(id int64, updated Company) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
c, ok := repo.companies[id]
|
||||
if !ok || id == 0 {
|
||||
return ErrUpdateFailed
|
||||
}
|
||||
c.Name = updated.Name
|
||||
c.Address = updated.Address
|
||||
c.ContactEmail = updated.ContactEmail
|
||||
c.ContactPhone = updated.ContactPhone
|
||||
c.HourlyRate = updated.HourlyRate
|
||||
repo.companies[id] = c
|
||||
|
||||
for assignmentID, a := range repo.assignments {
|
||||
if a.CompanyID == id {
|
||||
a.CompanyName = c.Name
|
||||
repo.assignments[assignmentID] = a
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) DeleteCompany(id int64) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
for _, a := range repo.assignments {
|
||||
if a.CompanyID == id {
|
||||
return ErrHasChildren
|
||||
}
|
||||
}
|
||||
if _, ok := repo.companies[id]; !ok {
|
||||
return ErrDeleteFailed
|
||||
}
|
||||
delete(repo.companies, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Assignments ────────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) InsertAssignment(a Assignment) (*Assignment, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
company, ok := repo.companies[a.CompanyID]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
a.ID = repo.nextAssignmentID
|
||||
repo.nextAssignmentID++
|
||||
a.CompanyName = company.Name
|
||||
if a.Status == "" {
|
||||
a.Status = "active"
|
||||
}
|
||||
a.CreatedAt = time.Now()
|
||||
repo.assignments[a.ID] = a
|
||||
return copyAssignment(a), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) AssignmentsByCompany(companyID int64) ([]Assignment, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
all := make([]Assignment, 0)
|
||||
for _, a := range repo.assignments {
|
||||
if a.CompanyID == companyID {
|
||||
all = append(all, a)
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].CreatedAt.After(all[j].CreatedAt)
|
||||
})
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) AllActiveAssignments() ([]Assignment, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
all := make([]Assignment, 0)
|
||||
for _, a := range repo.assignments {
|
||||
if a.Status == "active" {
|
||||
all = append(all, a)
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
if strings.ToLower(all[i].CompanyName) == strings.ToLower(all[j].CompanyName) {
|
||||
return strings.ToLower(all[i].Name) < strings.ToLower(all[j].Name)
|
||||
}
|
||||
return strings.ToLower(all[i].CompanyName) < strings.ToLower(all[j].CompanyName)
|
||||
})
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) GetAssignmentByID(id int64) (*Assignment, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
a, ok := repo.assignments[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return copyAssignment(a), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) UpdateAssignment(id int64, updated Assignment) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
a, ok := repo.assignments[id]
|
||||
if !ok || id == 0 {
|
||||
return ErrUpdateFailed
|
||||
}
|
||||
a.Name = updated.Name
|
||||
a.Description = updated.Description
|
||||
a.HourlyRate = updated.HourlyRate
|
||||
if updated.Status != "" {
|
||||
a.Status = updated.Status
|
||||
}
|
||||
repo.assignments[id] = a
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) DeleteAssignment(id int64) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
for _, t := range repo.tasks {
|
||||
if t.AssignmentID == id {
|
||||
return ErrHasChildren
|
||||
}
|
||||
}
|
||||
if _, ok := repo.assignments[id]; !ok {
|
||||
return ErrDeleteFailed
|
||||
}
|
||||
for sessionID, s := range repo.sessions {
|
||||
if s.AssignmentID == id {
|
||||
delete(repo.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
delete(repo.assignments, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Tasks ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) InsertTask(t Task) (*Task, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if _, ok := repo.assignments[t.AssignmentID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
maxNum := 0
|
||||
for _, existing := range repo.tasks {
|
||||
if existing.AssignmentID == t.AssignmentID && existing.Number > maxNum {
|
||||
maxNum = existing.Number
|
||||
}
|
||||
}
|
||||
t.ID = repo.nextTaskID
|
||||
repo.nextTaskID++
|
||||
t.Number = maxNum + 1
|
||||
repo.tasks[t.ID] = t
|
||||
withTotals := repo.taskWithTotalLocked(t)
|
||||
return copyTask(withTotals), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) TasksByAssignment(assignmentID int64) ([]Task, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
all := make([]Task, 0)
|
||||
for _, t := range repo.tasks {
|
||||
if t.AssignmentID == assignmentID {
|
||||
all = append(all, repo.taskWithTotalLocked(t))
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].Number < all[j].Number })
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) GetTaskByID(id int64) (*Task, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
t, ok := repo.tasks[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
withTotals := repo.taskWithTotalLocked(t)
|
||||
return copyTask(withTotals), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) UpdateTask(id int64, name string) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
t, ok := repo.tasks[id]
|
||||
if !ok || id == 0 || name == "" {
|
||||
return ErrUpdateFailed
|
||||
}
|
||||
t.Name = name
|
||||
repo.tasks[id] = t
|
||||
|
||||
newDesc := fmt.Sprintf("%d - %s", t.Number, t.Name)
|
||||
for sessionID, s := range repo.sessions {
|
||||
if s.TaskID == id {
|
||||
s.TaskDescription = newDesc
|
||||
repo.sessions[sessionID] = s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) DeleteTask(id int64) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
for _, s := range repo.sessions {
|
||||
if s.TaskID == id {
|
||||
return ErrHasChildren
|
||||
}
|
||||
}
|
||||
if _, ok := repo.tasks[id]; !ok {
|
||||
return ErrDeleteFailed
|
||||
}
|
||||
delete(repo.tasks, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) TotalMinutesForTask(taskID int64) (float64, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
if _, ok := repo.tasks[taskID]; !ok {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
return repo.totalMinutesForTaskLocked(taskID), nil
|
||||
}
|
||||
|
||||
// ── Work Sessions ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) InsertWorkSession(s WorkSession) (*WorkSession, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if _, ok := repo.assignments[s.AssignmentID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if _, ok := repo.tasks[s.TaskID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.ID = repo.nextSessionID
|
||||
repo.nextSessionID++
|
||||
s.StartTime = time.Now()
|
||||
s.EndTime = nil
|
||||
s.DurationMinutes = 0
|
||||
repo.sessions[s.ID] = s
|
||||
return copyWorkSession(s), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) StopWorkSession(id int64) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
s, ok := repo.sessions[id]
|
||||
if !ok {
|
||||
return ErrUpdateFailed
|
||||
}
|
||||
now := time.Now()
|
||||
s.EndTime = &now
|
||||
s.DurationMinutes = now.Sub(s.StartTime).Minutes()
|
||||
repo.sessions[id] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) InsertManualWorkSession(s WorkSession) (*WorkSession, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if s.EndTime == nil {
|
||||
return nil, ErrUpdateFailed
|
||||
}
|
||||
if _, ok := repo.assignments[s.AssignmentID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if _, ok := repo.tasks[s.TaskID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
s.ID = repo.nextSessionID
|
||||
repo.nextSessionID++
|
||||
end := *s.EndTime
|
||||
s.EndTime = &end
|
||||
s.DurationMinutes = end.Sub(s.StartTime).Minutes()
|
||||
repo.sessions[s.ID] = s
|
||||
return copyWorkSession(s), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) UpdateWorkSession(id int64, updated WorkSession) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
s, ok := repo.sessions[id]
|
||||
if !ok || id == 0 {
|
||||
return ErrUpdateFailed
|
||||
}
|
||||
if _, ok := repo.tasks[updated.TaskID]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
s.TaskID = updated.TaskID
|
||||
s.TaskDescription = updated.TaskDescription
|
||||
s.StartTime = updated.StartTime
|
||||
if updated.EndTime != nil {
|
||||
end := *updated.EndTime
|
||||
s.EndTime = &end
|
||||
s.DurationMinutes = end.Sub(updated.StartTime).Minutes()
|
||||
} else {
|
||||
s.EndTime = nil
|
||||
s.DurationMinutes = 0
|
||||
}
|
||||
repo.sessions[id] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) DeleteWorkSession(id int64) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if _, ok := repo.sessions[id]; !ok {
|
||||
return ErrDeleteFailed
|
||||
}
|
||||
delete(repo.sessions, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) WorkSessionsByAssignment(assignmentID int64, limit int) ([]WorkSession, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
all := make([]WorkSession, 0)
|
||||
for _, s := range repo.sessions {
|
||||
if s.AssignmentID == assignmentID {
|
||||
all = append(all, *copyWorkSession(s))
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].StartTime.After(all[j].StartTime) })
|
||||
if limit > 0 && len(all) > limit {
|
||||
all = all[:limit]
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) TotalHoursForAssignment(assignmentID int64) (float64, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
if _, ok := repo.assignments[assignmentID]; !ok {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
var minutes float64
|
||||
for _, s := range repo.sessions {
|
||||
if s.AssignmentID == assignmentID && s.EndTime != nil {
|
||||
minutes += s.DurationMinutes
|
||||
}
|
||||
}
|
||||
return minutes / 60.0, nil
|
||||
}
|
||||
|
||||
// ── Invoices ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) NextInvoiceNumber(date time.Time) (string, error) {
|
||||
repo.mu.RLock()
|
||||
defer repo.mu.RUnlock()
|
||||
|
||||
prefix := date.Format("2006-01-02")
|
||||
count := 0
|
||||
for _, inv := range repo.invoices {
|
||||
if strings.HasPrefix(inv.InvoiceNumber, prefix+"_") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s_%d", prefix, count+1), nil
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) InsertInvoice(inv Invoice) (*Invoice, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if _, ok := repo.assignments[inv.AssignmentID]; !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
for _, existing := range repo.invoices {
|
||||
if existing.InvoiceNumber == inv.InvoiceNumber {
|
||||
return nil, fmt.Errorf("invoice number already exists")
|
||||
}
|
||||
}
|
||||
inv.ID = repo.nextInvoiceID
|
||||
repo.nextInvoiceID++
|
||||
inv.GeneratedAt = time.Now()
|
||||
repo.invoices[inv.ID] = inv
|
||||
return copyInvoice(inv), nil
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (repo *MemoryRepository) taskWithTotalLocked(t Task) Task {
|
||||
t.TotalMinutes = repo.totalMinutesForTaskLocked(t.ID)
|
||||
return t
|
||||
}
|
||||
|
||||
func (repo *MemoryRepository) totalMinutesForTaskLocked(taskID int64) float64 {
|
||||
var total float64
|
||||
for _, s := range repo.sessions {
|
||||
if s.TaskID == taskID && s.EndTime != nil {
|
||||
total += s.DurationMinutes
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func copyCompany(c Company) *Company {
|
||||
v := c
|
||||
return &v
|
||||
}
|
||||
|
||||
func copyAssignment(a Assignment) *Assignment {
|
||||
v := a
|
||||
return &v
|
||||
}
|
||||
|
||||
func copyTask(t Task) *Task {
|
||||
v := t
|
||||
return &v
|
||||
}
|
||||
|
||||
func copyWorkSession(s WorkSession) *WorkSession {
|
||||
v := s
|
||||
if s.EndTime != nil {
|
||||
end := *s.EndTime
|
||||
v.EndTime = &end
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func copyInvoice(inv Invoice) *Invoice {
|
||||
v := inv
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUpdateFailed = errors.New("update failed")
|
||||
ErrDeleteFailed = errors.New("delete failed")
|
||||
ErrNotFound = errors.New("record not found")
|
||||
ErrHasChildren = errors.New("cannot delete: has dependent records that must be removed first")
|
||||
)
|
||||
|
||||
// Repository is the interface which must be satisfied in order to
|
||||
// connect to a database.
|
||||
type Repository interface {
|
||||
Migrate() error
|
||||
|
||||
InsertCompany(c Company) (*Company, error)
|
||||
AllCompanies() ([]Company, error)
|
||||
GetCompanyByID(id int64) (*Company, error)
|
||||
UpdateCompany(id int64, updated Company) error
|
||||
DeleteCompany(id int64) error
|
||||
|
||||
InsertAssignment(a Assignment) (*Assignment, error)
|
||||
AssignmentsByCompany(companyID int64) ([]Assignment, error)
|
||||
AllActiveAssignments() ([]Assignment, error)
|
||||
GetAssignmentByID(id int64) (*Assignment, error)
|
||||
UpdateAssignment(id int64, updated Assignment) error
|
||||
DeleteAssignment(id int64) error
|
||||
|
||||
InsertTask(t Task) (*Task, error)
|
||||
TasksByAssignment(assignmentID int64) ([]Task, error)
|
||||
GetTaskByID(id int64) (*Task, error)
|
||||
UpdateTask(id int64, name string) error
|
||||
DeleteTask(id int64) error
|
||||
TotalMinutesForTask(taskID int64) (float64, error)
|
||||
|
||||
InsertWorkSession(s WorkSession) (*WorkSession, error)
|
||||
InsertManualWorkSession(s WorkSession) (*WorkSession, error)
|
||||
StopWorkSession(id int64) error
|
||||
UpdateWorkSession(id int64, updated WorkSession) error
|
||||
DeleteWorkSession(id int64) error
|
||||
WorkSessionsByAssignment(assignmentID int64, limit int) ([]WorkSession, error)
|
||||
TotalHoursForAssignment(assignmentID int64) (float64, error)
|
||||
|
||||
NextInvoiceNumber(date time.Time) (string, error)
|
||||
InsertInvoice(inv Invoice) (*Invoice, error)
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
HourlyRate float64 `json:"hourly_rate"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
ID int64 `json:"id"`
|
||||
CompanyID int64 `json:"company_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
HourlyRate float64 `json:"hourly_rate"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Task is a reusable task within an assignment. Each task gets an
|
||||
// incremental number scoped to its assignment (1, 2, 3...).
|
||||
// Time spent on sessions with this task stacks up.
|
||||
type Task struct {
|
||||
ID int64 `json:"id"`
|
||||
AssignmentID int64 `json:"assignment_id"`
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name"`
|
||||
TotalMinutes float64 `json:"total_minutes"`
|
||||
}
|
||||
|
||||
// WorkSession is a single timed block of work on a task.
|
||||
type WorkSession struct {
|
||||
ID int64 `json:"id"`
|
||||
AssignmentID int64 `json:"assignment_id"`
|
||||
TaskID int64 `json:"task_id"`
|
||||
TaskDescription string `json:"task_description"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
DurationMinutes float64 `json:"duration_minutes"`
|
||||
}
|
||||
|
||||
// Invoice is a generated invoice record, used for tracking invoice numbers.
|
||||
type Invoice struct {
|
||||
ID int64 `json:"id"`
|
||||
AssignmentID int64 `json:"assignment_id"`
|
||||
InvoiceNumber string `json:"invoice_number"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user