package main import ( "fmt" "net/http" "strconv" "time" "zeitkort-web/repository" ) // mainView is the data backing the working area (#main). type mainView struct { Version string ResetAt time.Time ResetIntervalMinutes int Companies []repository.Company CompanyID int64 Assignments []repository.Assignment AssignmentID int64 Assignment *repository.Assignment Tasks []repository.Task Sessions []repository.WorkSession TotalHours float64 TotalAmount float64 Running *repository.WorkSession EditingTaskID int64 AddingTask bool Notice string AppError string } func qInt64(r *http.Request, key string) int64 { n, _ := strconv.ParseInt(r.URL.Query().Get(key), 10, 64) return n } func formInt64(r *http.Request, key string) int64 { n, _ := strconv.ParseInt(r.FormValue(key), 10, 64) return n } func pathInt64(r *http.Request, key string) int64 { n, _ := strconv.ParseInt(r.PathValue(key), 10, 64) return n } // buildMainView assembles everything shown for the current selection. func (s *Server) buildMainView(companyID, assignmentID int64) mainView { v := mainView{ Version: Version, CompanyID: companyID, AssignmentID: assignmentID, ResetAt: s.nextResetAt(), ResetIntervalMinutes: int(resetInterval / time.Minute), } companies, err := s.repo.AllCompanies() if err != nil { v.AppError = err.Error() return v } v.Companies = companies if companyID == 0 { return v } assignments, err := s.repo.AssignmentsByCompany(companyID) if err != nil { v.AppError = err.Error() return v } v.Assignments = assignments if assignmentID == 0 { return v } for i := range assignments { if assignments[i].ID == assignmentID { v.Assignment = &assignments[i] break } } if v.Assignment == nil { // Assignment does not belong to this company; treat as unselected. v.AssignmentID = 0 return v } if tasks, err := s.repo.TasksByAssignment(assignmentID); err == nil { v.Tasks = tasks } if sessions, err := s.repo.WorkSessionsByAssignment(assignmentID, 20); err == nil { v.Sessions = sessions for i := range sessions { if sessions[i].EndTime == nil { v.Running = &sessions[i] break } } } v.TotalHours, _ = s.repo.TotalHoursForAssignment(assignmentID) v.TotalAmount = v.TotalHours * v.Assignment.HourlyRate return v } // ── Main working area ─────────────────────────────────────────────────────── func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { v := s.buildMainView(qInt64(r, "company_id"), qInt64(r, "assignment_id")) s.render(w, "page", v) } func (s *Server) handleMain(w http.ResponseWriter, r *http.Request) { v := s.buildMainView(qInt64(r, "company_id"), qInt64(r, "assignment_id")) s.render(w, "main", v) } // renderMainWith re-renders the working area fragment for the given selection, // optionally carrying a notice or error to show the user. func (s *Server) renderMainWith(w http.ResponseWriter, companyID, assignmentID int64, notice, appErr string) { v := s.buildMainView(companyID, assignmentID) if notice != "" { v.Notice = notice } if appErr != "" { v.AppError = appErr } s.render(w, "main", v) } // ── Companies ─────────────────────────────────────────────────────────────── type companyFormView struct { Company *repository.Company CompanyID int64 } func (s *Server) handleCompanyForm(w http.ResponseWriter, r *http.Request) { view := companyFormView{} if id := pathInt64(r, "id"); id > 0 { c, err := s.repo.GetCompanyByID(id) if err != nil { s.renderMainWith(w, 0, 0, "", err.Error()) return } view.Company = c view.CompanyID = id } s.render(w, "company_form", view) } func (s *Server) handleCompanySave(w http.ResponseWriter, r *http.Request) { r.ParseForm() rate, _ := strconv.ParseFloat(r.FormValue("hourly_rate"), 64) c := repository.Company{ Name: r.FormValue("name"), Address: r.FormValue("address"), ContactEmail: r.FormValue("contact_email"), ContactPhone: r.FormValue("contact_phone"), HourlyRate: rate, } if id := pathInt64(r, "id"); id > 0 { if err := s.repo.UpdateCompany(id, c); err != nil { s.renderMainWith(w, 0, 0, "", err.Error()) return } s.renderMainWith(w, id, 0, "Company updated.", "") return } created, err := s.repo.InsertCompany(c) if err != nil { s.renderMainWith(w, 0, 0, "", err.Error()) return } s.renderMainWith(w, created.ID, 0, "Company created.", "") } func (s *Server) handleCompanyDelete(w http.ResponseWriter, r *http.Request) { id := pathInt64(r, "id") if err := s.repo.DeleteCompany(id); err != nil { s.renderMainWith(w, id, 0, "", "Cannot delete: remove its assignments first.") return } s.renderMainWith(w, 0, 0, "Company deleted.", "") } // ── Assignments ───────────────────────────────────────────────────────────── type assignmentFormView struct { Assignment *repository.Assignment CompanyID int64 AssignID int64 } func (s *Server) handleAssignmentForm(w http.ResponseWriter, r *http.Request) { view := assignmentFormView{CompanyID: qInt64(r, "company_id")} if id := pathInt64(r, "id"); id > 0 { a, err := s.repo.GetAssignmentByID(id) if err != nil { s.renderMainWith(w, view.CompanyID, 0, "", err.Error()) return } view.Assignment = a view.AssignID = id view.CompanyID = a.CompanyID } s.render(w, "assignment_form", view) } func (s *Server) handleAssignmentSave(w http.ResponseWriter, r *http.Request) { r.ParseForm() companyID := formInt64(r, "company_id") rate, _ := strconv.ParseFloat(r.FormValue("hourly_rate"), 64) // fall back to the company's default rate if none given if rate == 0 { if c, err := s.repo.GetCompanyByID(companyID); err == nil { rate = c.HourlyRate } } if id := pathInt64(r, "id"); id > 0 { status := r.FormValue("status") if status == "" { status = "active" } a := repository.Assignment{ Name: r.FormValue("name"), Description: r.FormValue("description"), HourlyRate: rate, Status: status, } if err := s.repo.UpdateAssignment(id, a); err != nil { s.renderMainWith(w, companyID, 0, "", err.Error()) return } s.renderMainWith(w, companyID, id, "Assignment updated.", "") return } a := repository.Assignment{ CompanyID: companyID, Name: r.FormValue("name"), Description: r.FormValue("description"), HourlyRate: rate, } created, err := s.repo.InsertAssignment(a) if err != nil { s.renderMainWith(w, companyID, 0, "", err.Error()) return } s.renderMainWith(w, companyID, created.ID, "Assignment created.", "") } func (s *Server) handleAssignmentDelete(w http.ResponseWriter, r *http.Request) { id := pathInt64(r, "id") companyID := qInt64(r, "company_id") if err := s.repo.DeleteAssignment(id); err != nil { s.renderMainWith(w, companyID, id, "", "Cannot delete: remove its tasks first.") return } s.renderMainWith(w, companyID, 0, "Assignment deleted.", "") } // ── Tasks ─────────────────────────────────────────────────────────────────── func (s *Server) handleTaskNewForm(w http.ResponseWriter, r *http.Request) { companyID := qInt64(r, "company_id") assignmentID := qInt64(r, "assignment_id") v := s.buildMainView(companyID, assignmentID) v.AddingTask = true s.render(w, "main", v) } func (s *Server) handleTaskEditForm(w http.ResponseWriter, r *http.Request) { companyID := qInt64(r, "company_id") assignmentID := qInt64(r, "assignment_id") v := s.buildMainView(companyID, assignmentID) v.EditingTaskID = pathInt64(r, "id") s.render(w, "main", v) } func (s *Server) handleTaskCreate(w http.ResponseWriter, r *http.Request) { r.ParseForm() companyID := formInt64(r, "company_id") assignmentID := formInt64(r, "assignment_id") name := r.FormValue("name") if name == "" { s.renderMainWith(w, companyID, assignmentID, "", "Enter a task name.") return } if _, err := s.repo.InsertTask(repository.Task{AssignmentID: assignmentID, Name: name}); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "", "") } func (s *Server) handleTaskUpdate(w http.ResponseWriter, r *http.Request) { r.ParseForm() id := pathInt64(r, "id") companyID := formInt64(r, "company_id") assignmentID := formInt64(r, "assignment_id") name := r.FormValue("name") if name == "" { s.renderMainWith(w, companyID, assignmentID, "", "Task name cannot be empty.") return } if err := s.repo.UpdateTask(id, name); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "Task renamed.", "") } func (s *Server) handleTaskDelete(w http.ResponseWriter, r *http.Request) { id := pathInt64(r, "id") companyID := qInt64(r, "company_id") assignmentID := qInt64(r, "assignment_id") if err := s.repo.DeleteTask(id); err != nil { s.renderMainWith(w, companyID, assignmentID, "", "Cannot delete: remove its work sessions first.") return } s.renderMainWith(w, companyID, assignmentID, "Task deleted.", "") } // ── Work sessions: timer ────────────────────────────────────────────────────── func (s *Server) handleSessionStart(w http.ResponseWriter, r *http.Request) { r.ParseForm() companyID := formInt64(r, "company_id") assignmentID := formInt64(r, "assignment_id") taskID := formInt64(r, "task_id") if assignmentID == 0 { s.renderMainWith(w, companyID, assignmentID, "", "Pick an assignment first.") return } if taskID == 0 { s.renderMainWith(w, companyID, assignmentID, "", "Select a task to start the timer.") return } task, err := s.repo.GetTaskByID(taskID) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } desc := fmt.Sprintf("%d - %s", task.Number, task.Name) if _, err := s.repo.InsertWorkSession(repository.WorkSession{ AssignmentID: assignmentID, TaskID: taskID, TaskDescription: desc, }); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "", "") } func (s *Server) handleSessionStop(w http.ResponseWriter, r *http.Request) { r.ParseForm() id := pathInt64(r, "id") companyID := formInt64(r, "company_id") assignmentID := formInt64(r, "assignment_id") if err := s.repo.StopWorkSession(id); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "", "") } // ── Work sessions: manual entry + edit ──────────────────────────────────────── type sessionFormView struct { Session *repository.WorkSession SessionID int64 CompanyID int64 AssignmentID int64 Tasks []repository.Task } func (s *Server) handleSessionForm(w http.ResponseWriter, r *http.Request) { view := sessionFormView{ CompanyID: qInt64(r, "company_id"), AssignmentID: qInt64(r, "assignment_id"), } if id := pathInt64(r, "id"); id > 0 { // editing: load the session via its assignment's session list view.SessionID = id // We already know the assignment from the query string. sessions, _ := s.repo.WorkSessionsByAssignment(view.AssignmentID, 1000) for i := range sessions { if sessions[i].ID == id { view.Session = &sessions[i] break } } } if view.AssignmentID > 0 { view.Tasks, _ = s.repo.TasksByAssignment(view.AssignmentID) } s.render(w, "session_form", view) } func (s *Server) handleSessionSave(w http.ResponseWriter, r *http.Request) { r.ParseForm() companyID := formInt64(r, "company_id") assignmentID := formInt64(r, "assignment_id") // Resolve the task (existing id, or a new task by name). var task *repository.Task if r.FormValue("task_id") == "new" { name := r.FormValue("new_task_name") if name == "" { s.renderMainWith(w, companyID, assignmentID, "", "Enter a name for the new task.") return } created, err := s.repo.InsertTask(repository.Task{AssignmentID: assignmentID, Name: name}) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } task = created } else { t, err := s.repo.GetTaskByID(formInt64(r, "task_id")) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", "Select a task.") return } task = t } loc := time.Now().Location() dateParsed, err := time.ParseInLocation("02-01-2006", r.FormValue("date"), loc) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", "Invalid date (use DD-MM-YYYY).") return } startParsed, err := time.Parse("15:04", r.FormValue("start")) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", "Invalid start time (use HH:MM).") return } endParsed, err := time.Parse("15:04", r.FormValue("end")) if err != nil { s.renderMainWith(w, companyID, assignmentID, "", "Invalid end time (use HH:MM).") return } startTime := time.Date(dateParsed.Year(), dateParsed.Month(), dateParsed.Day(), startParsed.Hour(), startParsed.Minute(), 0, 0, loc) endTime := time.Date(dateParsed.Year(), dateParsed.Month(), dateParsed.Day(), endParsed.Hour(), endParsed.Minute(), 0, 0, loc) if endTime.Before(startTime) { endTime = endTime.Add(24 * time.Hour) } desc := fmt.Sprintf("%d - %s", task.Number, task.Name) if id := pathInt64(r, "id"); id > 0 { if err := s.repo.UpdateWorkSession(id, repository.WorkSession{ TaskID: task.ID, TaskDescription: desc, StartTime: startTime, EndTime: &endTime, }); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "Session updated.", "") return } if _, err := s.repo.InsertManualWorkSession(repository.WorkSession{ AssignmentID: assignmentID, TaskID: task.ID, TaskDescription: desc, StartTime: startTime, EndTime: &endTime, }); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "Session added.", "") } func (s *Server) handleSessionDelete(w http.ResponseWriter, r *http.Request) { id := pathInt64(r, "id") companyID := qInt64(r, "company_id") assignmentID := qInt64(r, "assignment_id") if err := s.repo.DeleteWorkSession(id); err != nil { s.renderMainWith(w, companyID, assignmentID, "", err.Error()) return } s.renderMainWith(w, companyID, assignmentID, "Session deleted.", "") }