diff --git a/src/Project/web.go b/src/Project/web.go new file mode 100644 index 0000000000000000000000000000000000000000..d253c5b679492d3f00e46040d50deca211f81dfe --- /dev/null +++ b/src/Project/web.go @@ -0,0 +1,493 @@ +package web + +import ( + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/org-harmony/harmony/src/app/project" + "github.com/org-harmony/harmony/src/app/user" + "github.com/org-harmony/harmony/src/core/hctx" + "github.com/org-harmony/harmony/src/core/validation" + "github.com/org-harmony/harmony/src/core/web" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ProjectFormData represents the data structure needed for rendering project forms +// This struct is used for both creating new projects and editing existing ones +type ProjectFormData struct { + Project any // Can hold either *project.ProjectToCreate or *project.ProjectToUpdate + IsEditForm bool // Flag to determine if this is an edit form (true) or create form (false) +} + +// ProjectListData contains the list of projects for template rendering +// This structure is passed to the project list template +type ProjectListData struct { + Projects []*project.Project // Array of all projects belonging to a user +} + +// ProjectDetailData holds all necessary data for displaying detailed project information +// including tab navigation and requirement management +type ProjectDetailData struct { + Project *project.ProjectWithRequirements // The project with all its requirements loaded + ActiveTab string // Currently active tab: "general" or "requirements" + RequirementStatuses []string // Available status options for requirements +} + +// RegisterController sets up all HTTP routes and handlers for project management +// This function is called during application startup to configure the web routing +func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx, mongoManager *project.MongoManager) { + log.Println("INFO: Registering project controllers...") + + // Initialize the data access layer (repository) and business logic layer (service) + // Repository handles MongoDB operations, Service handles business rules + projectRepo := project.NewMongoProjectRepository(mongoManager.GetDatabase()) + projectService := project.NewProjectService(projectRepo) + log.Println("INFO: Project repository and service initialized") + + // Create a router group that requires user authentication + // All routes defined here will require a logged-in user + router := webCtx.Router.With(user.LoggedInMiddleware(appCtx)) + + // Define all HTTP routes for project management: + + // GET /project/list - Display all projects for the current user + router.Get("/project/list", projectListController(appCtx, webCtx, projectService).ServeHTTP) + + // GET /project/new - Show form for creating a new project + router.Get("/project/new", projectNewController(appCtx, webCtx).ServeHTTP) + + // POST /project/new - Process new project creation form submission + router.Post("/project/new", projectCreateController(appCtx, webCtx, projectService).ServeHTTP) + + // GET /project/{id} - Display detailed view of a specific project + router.Get("/project/{id}", projectDetailController(appCtx, webCtx, projectService).ServeHTTP) + + // GET /project/{id}/edit - Show form for editing an existing project + router.Get("/project/{id}/edit", projectEditFormController(appCtx, webCtx, projectService).ServeHTTP) + + // PUT /project/{id} - Process project update form submission + router.Put("/project/{id}", projectUpdateController(appCtx, webCtx, projectService).ServeHTTP) + + // DELETE /project/{id} - Delete a project and all its requirements + router.Delete("/project/{id}", projectDeleteController(appCtx, webCtx, projectService).ServeHTTP) + + // POST /project/search - Handle project search functionality + router.Post("/project/search", projectSearchController(appCtx, webCtx, projectService).ServeHTTP) + + log.Println("INFO: Project routes registered successfully") +} + +// projectListController handles requests to display all projects belonging to the current user +// This controller fetches projects from the database and renders them in a list view +func projectListController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + // Extract the currently logged-in user from the request context + // This user information was added by the authentication middleware + usr := user.MustCtxUser(ctx) + + log.Printf("INFO: Loading project list for user %s", usr.ID) + + // Fetch all projects created by this user from the database + // This calls through the service layer to maintain separation of concerns + projects, err := service.GetProjectsByUser(ctx, usr.ID) + if err != nil { + log.Printf("ERROR: Failed to get projects for user %s: %v", usr.ID, err) + return io.Error(web.ErrInternal, err) + } + + log.Printf("INFO: Found %d projects for user %s", len(projects), usr.ID) + + // Prepare data structure for template rendering + data := ProjectListData{Projects: projects} + log.Printf("DEBUG: Rendering project list with %d projects", len(data.Projects)) + + // Render the template with the project data + // The template system will wrap this data in BaseTemplateData automatically + err = io.Render( + data, + "project.list.page", // Template name to execute + "project/list-page.go.html", // Main page template file + "project/_list.go.html", // Partial template for the project list + ) + + if err != nil { + log.Printf("ERROR: Failed to render project list: %v", err) + return err + } + + log.Println("INFO: Project list rendered successfully") + return nil + }) +} + +// projectNewController displays an empty form for creating a new project +// This controller prepares default values and renders the project creation form +func projectNewController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + log.Println("INFO: Showing new project form") + + // Create a new project structure with sensible default values + // Start date is today, end date is 6 months in the future + toCreate := &project.ProjectToCreate{ + StartDate: time.Now(), + EndDate: time.Now().AddDate(0, 6, 0), // Add 6 months to current date + } + + // Render the project form with empty/default values + return renderProjectForm(io, &ProjectFormData{ + Project: toCreate, + IsEditForm: false, // This is a creation form, not an edit form + }, nil, nil) + }) +} + +// projectCreateController processes the form submission for creating a new project +// This controller validates the input, creates the project, and handles success/error cases +func projectCreateController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + // Get the current user to associate the new project with them + usr := user.MustCtxUser(ctx) + + log.Printf("INFO: Creating new project for user %s", usr.ID) + + // Initialize a new project creation object with the user ID + toCreate := &project.ProjectToCreate{CreatedBy: usr.ID} + + // Parse and validate the form data from the HTTP request + // This extracts form fields and validates them against defined rules + err, validationErrs := web.ReadForm(io.Request(), toCreate, appCtx.Validator) + if err != nil { + log.Printf("ERROR: Failed to read project form: %v", err) + return io.Error(web.ErrInternal, err) + } + + // If there are validation errors, redisplay the form with error messages + if validationErrs != nil { + log.Printf("WARN: Project creation validation failed: %d errors", len(validationErrs)) + return renderProjectForm(io, &ProjectFormData{ + Project: toCreate, + IsEditForm: false, + }, nil, validationErrs) + } + + log.Printf("INFO: Creating project with ID '%s' and name '%s'", toCreate.ProjectID, toCreate.Name) + + // Attempt to create the project using the business logic service + // The service will handle additional validation and database operations + newProject, err := service.CreateProject(ctx, toCreate) + if err != nil { + log.Printf("ERROR: Failed to create project '%s': %v", toCreate.ProjectID, err) + // If creation fails, redisplay the form with the error message + return renderProjectForm(io, &ProjectFormData{ + Project: toCreate, + IsEditForm: false, + }, nil, []error{validation.Error{Msg: err.Error()}}) + } + + log.Printf("INFO: Project '%s' created successfully with MongoDB ID %s", newProject.ProjectID, newProject.ID.Hex()) + + // On successful creation, redirect to the project detail page + return io.Redirect(fmt.Sprintf("/project/%s", newProject.ID.Hex()), http.StatusFound) + }) +} + +// projectDetailController displays detailed information about a specific project +// This includes general project info and requirements, organized in tabs +func projectDetailController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + + // Extract the project ID from the URL parameters + id, err := parseObjectID(io.Request(), "id") + if err != nil { + log.Printf("ERROR: Invalid project ID in URL: %v", err) + return io.Error(web.ErrInternal, err) + } + + log.Printf("INFO: Loading project details for ID %s", id.Hex()) + + // Fetch the project along with all its requirements from the database + // This is more efficient than making separate calls for project and requirements + projectWithReqs, err := service.GetProjectWithRequirements(ctx, id) + if err != nil { + log.Printf("ERROR: Failed to get project with requirements for ID %s: %v", id.Hex(), err) + return io.Error(web.ErrInternal, err) + } + + // Determine which tab should be active (from URL query parameter) + // Default to "general" tab if no tab is specified + activeTab := io.Request().URL.Query().Get("tab") + if activeTab == "" { + activeTab = "general" + } + + log.Printf("INFO: Loaded project '%s' with %d requirements, showing tab '%s'", + projectWithReqs.Project.Name, projectWithReqs.RequirementCount, activeTab) + + // Prepare all data needed for the project detail template + data := ProjectDetailData{ + Project: projectWithReqs, + ActiveTab: activeTab, + RequirementStatuses: []string{"Entwurf", "In Prüfung", "Genehmigt", "Abgelehnt"}, // Available status options + } + + // Render the project detail page with multiple template files + err = io.Render( + data, + "project.detail.page", // Main template name + "templates/project/detail-page.go.html", // Page wrapper template + "templates/project/_detail.go.html", // Project detail content + "templates/project/_requirements-list.go.html", // Requirements list partial + ) + + if err != nil { + log.Printf("ERROR: Failed to render project detail page: %v", err) + return err + } + + log.Println("INFO: Project detail page rendered successfully") + return nil + }) +} + +// projectEditFormController displays a form pre-filled with existing project data for editing +// This controller loads the current project data and renders it in an editable form +func projectEditFormController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + + // Parse the project ID from the URL + id, err := parseObjectID(io.Request(), "id") + if err != nil { + log.Printf("ERROR: Invalid project ID for edit: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Loading project edit form for ID %s", id.Hex()) + + // Get all projects for the current user to find the requested project + // This ensures the user can only edit their own projects + proj, err := service.GetProjectsByUser(ctx, user.MustCtxUser(ctx).ID) + if err != nil { + log.Printf("ERROR: Failed to get projects for edit: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + // Search through the user's projects to find the one being edited + var targetProject *project.Project + for _, p := range proj { + if p.ID == id { + targetProject = p + break + } + } + + // If the project is not found, it either doesn't exist or doesn't belong to this user + if targetProject == nil { + log.Printf("ERROR: Project with ID %s not found for edit", id.Hex()) + return io.InlineError(web.ErrInternal, errors.New("project not found")) + } + + log.Printf("INFO: Showing edit form for project '%s'", targetProject.Name) + + // Convert the project to an update structure and render the edit form + return renderProjectEditForm(io, targetProject.ToUpdate(), nil, nil) + }) +} + +// projectUpdateController processes form submissions for updating existing projects +// This controller validates changes and applies them to the database +func projectUpdateController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + + // Extract project ID from URL parameters + id, err := parseObjectID(io.Request(), "id") + if err != nil { + log.Printf("ERROR: Invalid project ID for update: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Updating project with ID %s", id.Hex()) + + // Create an update structure with the project ID + toUpdate := &project.ProjectToUpdate{ID: id} + + // Parse and validate the form data from the request + err, validationErrs := web.ReadForm(io.Request(), toUpdate, appCtx.Validator) + if err != nil { + log.Printf("ERROR: Failed to read update form: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + // If validation fails, redisplay the form with error messages + if validationErrs != nil { + log.Printf("WARN: Project update validation failed: %d errors", len(validationErrs)) + return renderProjectEditForm(io, toUpdate, nil, validationErrs) + } + + log.Printf("INFO: Updating project '%s' with new name '%s'", id.Hex(), toUpdate.Name) + + // Apply the updates through the service layer + updatedProject, err := service.UpdateProject(ctx, toUpdate) + if err != nil { + log.Printf("ERROR: Failed to update project %s: %v", id.Hex(), err) + return renderProjectEditForm(io, toUpdate, nil, []error{validation.Error{Msg: err.Error()}}) + } + + log.Printf("INFO: Project '%s' updated successfully", updatedProject.Name) + + // Redisplay the form with a success message + return renderProjectEditForm(io, updatedProject.ToUpdate(), []string{"Projekt erfolgreich aktualisiert"}, nil) + }) +} + +// projectDeleteController handles project deletion requests +// This controller removes the project and all associated requirements from the database +func projectDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + usr := user.MustCtxUser(ctx) + + // Parse the project ID from the URL + id, err := parseObjectID(io.Request(), "id") + if err != nil { + log.Printf("ERROR: Invalid project ID for deletion: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Deleting project with ID %s for user %s", id.Hex(), usr.ID) + + // Delete the project and all its requirements through the service + err = service.DeleteProject(ctx, id) + if err != nil { + log.Printf("ERROR: Failed to delete project %s: %v", id.Hex(), err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Project %s deleted successfully", id.Hex()) + + // After deletion, fetch and return the updated project list + // This provides immediate feedback to the user about the deletion + projects, err := service.GetProjectsByUser(ctx, usr.ID) + if err != nil { + log.Printf("ERROR: Failed to get updated project list after deletion: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Returning updated project list with %d projects", len(projects)) + + // Return the updated project list as HTML to replace the current list + data := ProjectListData{Projects: projects} + return io.Render( + data, + "project.list", // Template name for just the list part + "project/_list.go.html", // Partial template for project list + ) + }) +} + +// projectSearchController handles search requests for projects +// This controller filters projects based on user input and returns matching results +func projectSearchController(appCtx *hctx.AppCtx, webCtx *web.Ctx, service *project.ProjectService) http.Handler { + return web.NewController(appCtx, webCtx, func(io web.IO) error { + ctx := io.Context() + usr := user.MustCtxUser(ctx) + + request := io.Request() + + // Parse the form data to extract search parameters + err := request.ParseForm() + if err != nil { + log.Printf("ERROR: Failed to parse search form: %v", err) + return io.InlineError(web.ErrInternal, err) + } + + // Extract the search query from the form + query := request.FormValue("search") + log.Printf("INFO: Searching projects for user %s with query '%s'", usr.ID, query) + + // Perform the search using the service layer + // The service handles both empty queries (return all) and actual searches + projects, err := service.SearchProjects(ctx, query, usr.ID) + if err != nil { + log.Printf("ERROR: Project search failed for query '%s': %v", query, err) + return io.InlineError(web.ErrInternal, err) + } + + log.Printf("INFO: Search found %d projects for query '%s'", len(projects), query) + + // Return the search results as a partial HTML update + data := ProjectListData{Projects: projects} + return io.Render( + data, + "project.list", // Template name for the list + "templates/project/_list.go.html", // Partial template file + ) + }) +} + +// Helper Functions + +// parseObjectID extracts and validates a MongoDB ObjectID from HTTP request parameters +// This function ensures that URL parameters containing IDs are valid MongoDB ObjectIDs +func parseObjectID(r *http.Request, param string) (primitive.ObjectID, error) { + // Extract the parameter value from the URL + idStr := web.URLParam(r, param) + if idStr == "" { + return primitive.NilObjectID, errors.New("missing id parameter") + } + + // Convert the string to a valid MongoDB ObjectID + return primitive.ObjectIDFromHex(idStr) +} + +// renderProjectForm is a helper function to render project creation/edit forms +// This function handles both success messages and validation errors +func renderProjectForm(io web.IO, data *ProjectFormData, success []string, errs []error) error { + log.Printf("DEBUG: Rendering project form (edit=%t, errors=%d)", data.IsEditForm, len(errs)) + + // Wrap the form data with success messages and errors for template rendering + err := io.Render( + web.NewFormData(data, success, errs...), + "project.form.page", // Main template name + "project/form-page.go.html", // Page wrapper template + "project/_form.go.html", // Form partial template + ) + + if err != nil { + log.Printf("ERROR: Failed to render project form: %v", err) + } + + return err +} + +// renderProjectEditForm is a specialized helper for rendering edit forms +// This function prepares edit-specific form data and renders the appropriate templates +func renderProjectEditForm(io web.IO, toUpdate *project.ProjectToUpdate, success []string, errs []error) error { + // Create form data structure specifically for editing + formData := &ProjectFormData{ + Project: toUpdate, + IsEditForm: true, // This flag helps the template render edit-specific elements + } + + log.Printf("DEBUG: Rendering project edit form for project %s (errors=%d)", toUpdate.ID.Hex(), len(errs)) + + // Render the edit form with the current project data + err := io.Render( + web.NewFormData(formData, success, errs...), + "project.edit.form", // Template name for edit forms + "project/_form.go.html", // Shared form partial template + ) + + if err != nil { + log.Printf("ERROR: Failed to render project edit form: %v", err) + } + + return err +}