initial commit
This commit is contained in:
+493
@@ -0,0 +1,493 @@
|
||||
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.", "")
|
||||
}
|
||||
Reference in New Issue
Block a user