494 lines
15 KiB
Go
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.", "")
|
|
}
|