diff --git a/src/app/project/repository.go b/src/app/project/repository.go
index efa2098b3e48d8c140b2a59117f546f3f8790b25..1750e39853be1f34bbeb4de93f4f4ca88f104a29 100644
--- a/src/app/project/repository.go
+++ b/src/app/project/repository.go
@@ -50,6 +50,9 @@ type ProjectRepository interface {
// Search finds projects matching a text query for a specific user
Search(ctx context.Context, query string, userID uuid.UUID) ([]*Project, error)
+
+ // CloneRequirement creates a copy of an existing requirement in a different project
+ CloneRequirement(ctx context.Context, sourceReqID primitive.ObjectID, targetProjectID string, newName string) error
}
// MongoProjectRepository implements ProjectRepository using MongoDB
@@ -434,3 +437,54 @@ func (r *MongoProjectRepository) Search(ctx context.Context, query string, userI
log.Printf("INFO: Search completed - found %d projects for query '%s'", len(projects), query)
return projects, cursor.Err()
}
+
+// CloneRequirement creates a copy of an existing requirement in a different project
+func (r *MongoProjectRepository) CloneRequirement(ctx context.Context, sourceReqID primitive.ObjectID, targetProjectID string, newName string) error {
+ ctx, cancel := context.WithTimeout(ctx, r.timeout)
+ defer cancel()
+
+ log.Printf("DEBUG: Cloning requirement %s to project %s with name '%s'", sourceReqID.Hex(), targetProjectID, newName)
+
+ // Find the source requirement
+ var sourceReq bson.M
+ err := r.requirementCollection.FindOne(ctx, bson.M{"_id": sourceReqID}).Decode(&sourceReq)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ log.Printf("WARN: Source requirement %s not found", sourceReqID.Hex())
+ return fmt.Errorf("source requirement not found")
+ }
+ log.Printf("ERROR: Failed to find source requirement %s: %v", sourceReqID.Hex(), err)
+ return fmt.Errorf("failed to find source requirement: %w", err)
+ }
+
+ // Create cloned requirement with new ID and target project
+ clonedReq := bson.M{
+ "_id": primitive.NewObjectID(),
+ "uuid": uuid.New(),
+ "project_id": targetProjectID,
+ "shortcut": newName,
+ "created_at": time.Now(),
+ "status": "Entwurf",
+ }
+
+ // Copy relevant fields from source requirement
+ fieldsToClone := []string{"condition", "system", "requirement", "parsing_result"}
+ for _, field := range fieldsToClone {
+ if value, exists := sourceReq[field]; exists {
+ clonedReq[field] = value
+ }
+ }
+
+ // Generate requirement_id based on shortcut
+ clonedReq["requirement_id"] = newName
+
+ // Insert the cloned requirement
+ _, err = r.requirementCollection.InsertOne(ctx, clonedReq)
+ if err != nil {
+ log.Printf("ERROR: Failed to insert cloned requirement: %v", err)
+ return fmt.Errorf("failed to clone requirement: %w", err)
+ }
+
+ log.Printf("INFO: Requirement cloned successfully to project %s", targetProjectID)
+ return nil
+}
diff --git a/src/app/project/service.go b/src/app/project/service.go
index dfbdbd7c5562d769175f432a0570d3c6f595db68..c414b925166c2c22b89e9de64d7505a03f511885 100644
--- a/src/app/project/service.go
+++ b/src/app/project/service.go
@@ -194,3 +194,27 @@ func (s *ProjectService) SearchProjects(ctx context.Context, query string, userI
log.Printf("INFO: Search completed - found %d projects for query '%s'", len(projects), query)
return projects, nil
}
+
+// CloneRequirement handles the cloning of a requirement to a different project
+func (s *ProjectService) CloneRequirement(ctx context.Context, sourceReqID primitive.ObjectID, targetProjectID string, newName string) error {
+ log.Printf("INFO: Cloning requirement %s to project %s with name '%s'", sourceReqID.Hex(), targetProjectID, newName)
+
+ // Validate that the target project exists
+ targetProject, err := s.repo.FindByProjectID(ctx, targetProjectID)
+ if err != nil {
+ log.Printf("ERROR: Target project '%s' not found: %v", targetProjectID, err)
+ return fmt.Errorf("target project not found")
+ }
+
+ log.Printf("DEBUG: Target project '%s' found, proceeding with clone", targetProject.Name)
+
+ // Delegate to repository for the actual cloning operation
+ err = s.repo.CloneRequirement(ctx, sourceReqID, targetProjectID, newName)
+ if err != nil {
+ log.Printf("ERROR: Failed to clone requirement %s: %v", sourceReqID.Hex(), err)
+ return err
+ }
+
+ log.Printf("INFO: Requirement cloned successfully to project '%s'", targetProject.Name)
+ return nil
+}
diff --git a/src/app/project/web/web.go b/src/app/project/web/web.go
index 5ee83471b654611cc87f9b9a2a1216bd3d7d7444..c3b46f930fe5d24cdeb51c4ffceaeb36239eaf7d 100644
--- a/src/app/project/web/web.go
+++ b/src/app/project/web/web.go
@@ -36,6 +36,15 @@ type ProjectDetailData struct {
RequirementStatuses []string // Available status options for requirements
}
+// RequirementCloneFormData is passed to the requirement clone modal
+type RequirementCloneFormData struct {
+ Name string `hvalidate:"required"`
+ ProjectID string
+ Requirement *project.RequirementSummary
+ Projects []*project.Project
+ Cloned bool
+}
+
// 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) {
@@ -87,6 +96,12 @@ func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx, mongoManager *proj
// POST /project/search - Handle project search functionality
router.Post("/project/search", projectSearchController(appCtx, webCtx, projectService).ServeHTTP)
+ // GET /requirement/{id}/clone/modal - Show requirement clone modal
+ router.Get("/requirement/{id}/clone/modal", requirementCloneModalController(appCtx, webCtx, projectService).ServeHTTP)
+
+ // POST /requirement/{id}/clone - Process requirement cloning
+ router.Post("/requirement/{id}/clone", requirementCloneController(appCtx, webCtx, projectService).ServeHTTP)
+
log.Println("INFO: Project routes registered successfully")
}
@@ -531,3 +546,129 @@ func renderProjectEditForm(io web.IO, toUpdate *project.ProjectToUpdate, success
return err
}
+
+// requirementCloneModalController displays the modal for cloning a requirement
+func requirementCloneModalController(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 requirement ID from URL
+ reqID, err := parseObjectID(io.Request(), "id")
+ if err != nil {
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ // Find the requirement by searching through all projects
+ var targetReq *project.RequirementSummary
+ projects, err := service.GetProjectsByUser(ctx, usr.ID)
+ if err != nil {
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ for _, proj := range projects {
+ projWithReqs, err := service.GetProjectWithRequirements(ctx, proj.ID)
+ if err != nil {
+ continue
+ }
+ for _, req := range projWithReqs.Requirements {
+ if req.ID == reqID {
+ targetReq = req
+ break
+ }
+ }
+ if targetReq != nil {
+ break
+ }
+ }
+
+ if targetReq == nil {
+ return io.InlineError(web.ErrInternal, errors.New("requirement not found"))
+ }
+
+ return io.Render(web.NewFormData(RequirementCloneFormData{
+ Requirement: targetReq,
+ Projects: projects,
+ }, nil), "requirement.clone.modal", "project/_modal-clone-requirement.go.html")
+ })
+}
+
+// requirementCloneController processes the requirement cloning form submission
+func requirementCloneController(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)
+
+ log.Printf("INFO: Starting requirement clone process for user %s", usr.ID)
+
+ // Parse requirement ID from URL
+ reqID, err := parseObjectID(io.Request(), "id")
+ if err != nil {
+ log.Printf("ERROR: Failed to parse requirement ID: %v", err)
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ log.Printf("INFO: Cloning requirement with ID %s", reqID.Hex())
+
+ // Get projects for form data
+ projects, err := service.GetProjectsByUser(ctx, usr.ID)
+ if err != nil {
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ // Find the source requirement
+ var sourceReq *project.RequirementSummary
+ for _, proj := range projects {
+ projWithReqs, err := service.GetProjectWithRequirements(ctx, proj.ID)
+ if err != nil {
+ continue
+ }
+ for _, req := range projWithReqs.Requirements {
+ if req.ID == reqID {
+ sourceReq = req
+ break
+ }
+ }
+ if sourceReq != nil {
+ break
+ }
+ }
+
+ if sourceReq == nil {
+ log.Printf("ERROR: Source requirement %s not found", reqID.Hex())
+ return io.InlineError(web.ErrInternal, errors.New("source requirement not found"))
+ }
+
+ log.Printf("INFO: Found source requirement %s", sourceReq.RequirementID)
+
+ formData := &RequirementCloneFormData{Requirement: sourceReq, Projects: projects}
+ err, validationErrs := web.ReadForm(io.Request(), formData, appCtx.Validator)
+ if err != nil {
+ log.Printf("ERROR: Failed to read form: %v", err)
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ log.Printf("INFO: Form data - ProjectID: %s, Name: %s", formData.ProjectID, formData.Name)
+
+ if validationErrs != nil {
+ log.Printf("WARN: Validation errors: %v", validationErrs)
+ return io.Render(web.NewFormData(formData, nil, validationErrs...), "requirement.clone.modal", "project/_modal-clone-requirement.go.html")
+ }
+
+ // Clone the requirement
+ log.Printf("INFO: Calling service.CloneRequirement with ProjectID: %s, Name: %s", formData.ProjectID, formData.Name)
+ err = service.CloneRequirement(ctx, reqID, formData.ProjectID, formData.Name)
+ if err != nil {
+ log.Printf("ERROR: Clone failed: %v", err)
+ return io.InlineError(web.ErrInternal, err)
+ }
+
+ log.Printf("INFO: Requirement cloned successfully")
+
+ // Return a simple success response for API calls
+ io.Response().Header().Set("Content-Type", "text/plain")
+ io.Response().WriteHeader(http.StatusOK)
+ _, err = io.Response().Write([]byte("Anforderung erfolgreich geklont"))
+ return err
+ })
+}
diff --git a/templates/project/_requirements-list.go.html b/templates/project/_requirements-list.go.html
index c4c6b1ea6483326877acb5ee85c18f656bffd0e7..d485ca1c73f82ff51a115d03f3b9990414dbeb20 100644
--- a/templates/project/_requirements-list.go.html
+++ b/templates/project/_requirements-list.go.html
@@ -78,7 +78,9 @@
<td>
<div class="btn-group btn-group-sm">
<!-- Clone requirement button -->
- <button class="btn btn-outline-secondary" title="Klonen">
+ <button onclick="cloneRequirement('{{ .ID.Hex }}', '{{ .RequirementID }}')"
+ class="btn btn-outline-secondary"
+ title="Klonen">
📋
</button>
<!-- Delete requirement button -->
@@ -94,4 +96,55 @@
</div>
{{ end }}
</div>
+
+<script>
+function cloneRequirement(reqId, reqName) {
+ // Get list of available projects from the current page context
+ const projects = [
+ {{ range $.Data.Project.Project.CreatedBy }}
+ // This won't work, we need to get projects differently
+ {{ end }}
+ ];
+
+ // Simple prompt-based solution for now
+ const targetProjectId = prompt(`Anforderung "${reqName}" klonen.\n\nGeben Sie die Projekt-ID des Zielprojekts ein:`);
+ if (!targetProjectId) return;
+
+ const newName = prompt(`Neuer Name für die geklonte Anforderung:`, `${reqName}-kopie`);
+ if (!newName) return;
+
+ // Debug: Log the request details
+ console.log('Making clone request:', {
+ url: `/requirement/${reqId}/clone`,
+ targetProjectId: targetProjectId,
+ newName: newName
+ });
+
+ // Make the clone request
+ fetch(`/requirement/${reqId}/clone`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: `ProjectID=${encodeURIComponent(targetProjectId)}&Name=${encodeURIComponent(newName)}`
+ })
+ .then(response => {
+ console.log('Response status:', response.status);
+ console.log('Response headers:', response.headers);
+ return response.text().then(text => {
+ console.log('Response body:', text);
+ if (response.ok) {
+ alert(`Anforderung erfolgreich nach Projekt "${targetProjectId}" geklont!`);
+ location.reload(); // Refresh to show changes
+ } else {
+ alert(`Fehler beim Klonen der Anforderung: ${response.status} - ${text}`);
+ }
+ });
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert('Fehler beim Klonen der Anforderung: ' + error.message);
+ });
+}
+</script>
{{ end }}
\ No newline at end of file