Files
2026-06-18 21:41:47 +01:00

494 lines
15 KiB
Go

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.", "")
}