package handler import ( "encoding/json" "fmt" "net/http" "strconv" "bugulma/backend/internal/domain" "bugulma/backend/internal/matching" "bugulma/backend/internal/service" "github.com/gin-gonic/gin" "gorm.io/datatypes" ) type OrganizationHandler struct { orgService *service.OrganizationService imageService *service.ImageService resourceFlowService *service.ResourceFlowService matchingService *matching.Service } func NewOrganizationHandler(orgService *service.OrganizationService, imageService *service.ImageService, resourceFlowService *service.ResourceFlowService, matchingService *matching.Service) *OrganizationHandler { return &OrganizationHandler{ orgService: orgService, imageService: imageService, resourceFlowService: resourceFlowService, matchingService: matchingService, } } type CreateOrganizationRequest struct { // Required fields Name string `json:"name" binding:"required"` Sector string `json:"sector" binding:"required"` Subtype string `json:"subtype" binding:"required"` // Basic information Description string `json:"description"` LogoURL string `json:"logoUrl"` GalleryImages []string `json:"galleryImages"` Website string `json:"website"` Address string `json:"address"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` // Business-specific fields LegalForm string `json:"legalForm"` PrimaryContactEmail string `json:"primaryContactEmail"` PrimaryContactPhone string `json:"primaryContactPhone"` IndustrialSector string `json:"industrialSector"` CompanySize int `json:"companySize"` YearsOperation int `json:"yearsOperation"` SupplyChainRole string `json:"supplyChainRole"` Certifications []string `json:"certifications"` BusinessFocus []string `json:"businessFocus"` StrategicVision string `json:"strategicVision"` DriversBarriers string `json:"driversBarriers"` ReadinessMaturity *int `json:"readinessMaturity"` // 1-5 TrustScore *float64 `json:"trustScore"` // 0.0-1.0 // Technical capabilities TechnicalExpertise []string `json:"technicalExpertise"` AvailableTechnology []string `json:"availableTechnology"` ManagementSystems []string `json:"managementSystems"` // Products and Services SellsProducts []domain.ProductJSON `json:"sellsProducts"` OffersServices []domain.ServiceJSON `json:"offersServices"` NeedsServices []domain.ServiceNeedJSON `json:"needsServices"` // Cultural/Historical building fields YearBuilt string `json:"yearBuilt"` BuilderOwner string `json:"builderOwner"` Architect string `json:"architect"` OriginalPurpose string `json:"originalPurpose"` CurrentUse string `json:"currentUse"` Style string `json:"style"` Materials string `json:"materials"` Storeys *int `json:"storeys"` HeritageStatus string `json:"heritageStatus"` // Metadata Verified bool `json:"verified"` Notes string `json:"notes"` Sources []string `json:"sources"` // Relationships TrustNetwork []string `json:"trustNetwork"` ExistingSymbioticRelationships []string `json:"existingSymbioticRelationships"` } func (h *OrganizationHandler) Create(c *gin.Context) { var req CreateOrganizationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } orgReq := service.CreateOrganizationRequest{ Name: req.Name, Subtype: domain.OrganizationSubtype(req.Subtype), Sector: req.Sector, Description: req.Description, LogoURL: req.LogoURL, GalleryImages: req.GalleryImages, Website: req.Website, Address: req.Address, Latitude: req.Latitude, Longitude: req.Longitude, LegalForm: req.LegalForm, PrimaryContactEmail: req.PrimaryContactEmail, PrimaryContactPhone: req.PrimaryContactPhone, IndustrialSector: req.IndustrialSector, CompanySize: req.CompanySize, YearsOperation: req.YearsOperation, SupplyChainRole: domain.SupplyChainRole(req.SupplyChainRole), Certifications: req.Certifications, BusinessFocus: req.BusinessFocus, StrategicVision: req.StrategicVision, DriversBarriers: req.DriversBarriers, ReadinessMaturity: req.ReadinessMaturity, TrustScore: req.TrustScore, TechnicalExpertise: req.TechnicalExpertise, AvailableTechnology: req.AvailableTechnology, ManagementSystems: req.ManagementSystems, SellsProducts: req.SellsProducts, OffersServices: req.OffersServices, NeedsServices: req.NeedsServices, YearBuilt: req.YearBuilt, BuilderOwner: req.BuilderOwner, Architect: req.Architect, OriginalPurpose: req.OriginalPurpose, CurrentUse: req.CurrentUse, Style: req.Style, Materials: req.Materials, Storeys: req.Storeys, HeritageStatus: req.HeritageStatus, Verified: req.Verified, Notes: req.Notes, Sources: req.Sources, TrustNetwork: req.TrustNetwork, ExistingSymbioticRelationships: req.ExistingSymbioticRelationships, } org, err := h.orgService.Create(c.Request.Context(), orgReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, org) } func (h *OrganizationHandler) GetByID(c *gin.Context) { id := c.Param("id") org, err := h.orgService.GetByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } c.JSON(http.StatusOK, org) } func (h *OrganizationHandler) GetAll(c *gin.Context) { // Check for sector filter sector := c.Query("sector") if sector != "" { orgs, err := h.orgService.GetBySector(c.Request.Context(), sector) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) return } // No filter - return all organizations orgs, err := h.orgService.GetAll(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) } func (h *OrganizationHandler) Update(c *gin.Context) { id := c.Param("id") var req CreateOrganizationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } org, err := h.orgService.GetByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } // Update fields org.Name = req.Name org.Sector = req.Sector org.Subtype = domain.OrganizationSubtype(req.Subtype) org.Description = req.Description org.LogoURL = req.LogoURL org.Website = req.Website if err := h.orgService.Update(c.Request.Context(), org); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, org) } func (h *OrganizationHandler) Delete(c *gin.Context) { id := c.Param("id") if err := h.orgService.Delete(c.Request.Context(), id); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } c.JSON(http.StatusNoContent, nil) } func (h *OrganizationHandler) GetBySubtype(c *gin.Context) { subtype := c.Param("subtype") orgs, err := h.orgService.GetBySubtype(c.Request.Context(), domain.OrganizationSubtype(subtype)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) } func (h *OrganizationHandler) GetBySector(c *gin.Context) { sector := c.Param("sector") orgs, err := h.orgService.GetBySector(c.Request.Context(), sector) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) } // GetSectorStats returns sector statistics (top sectors by organization count) func (h *OrganizationHandler) GetSectorStats(c *gin.Context) { // force reload // Get limit from query param, default to 10 limitStr := c.DefaultQuery("limit", "10") limit, err := strconv.Atoi(limitStr) if err != nil || limit < 1 || limit > 50 { limit = 10 } stats, err := h.orgService.GetSectorStats(c.Request.Context(), limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"sectors": stats}) } func (h *OrganizationHandler) GetByCertification(c *gin.Context) { cert := c.Param("cert") orgs, err := h.orgService.GetByCertification(c.Request.Context(), cert) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) } // Search handles search requests for organizations // Query parameters: // - q: search query string (required) // - limit: maximum number of results (optional, default: 50, max: 200) func (h *OrganizationHandler) Search(c *gin.Context) { query := c.Query("q") if query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"}) return } limit := 50 // Default limit if limitStr := c.Query("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit if limit > 200 { limit = 200 // Maximum limit } } } orgs, err := h.orgService.Search(c.Request.Context(), query, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Return structured response matching frontend schema response := gin.H{ "organizations": orgs, "count": len(orgs), "total": len(orgs), // For now, we don't have total count from search } c.JSON(http.StatusOK, response) } // SearchSuggestions handles autocomplete/suggestion requests // Query parameters: // - q: search query string (required) // - limit: maximum number of suggestions (optional, default: 10, max: 50) func (h *OrganizationHandler) SearchSuggestions(c *gin.Context) { query := c.Query("q") if query == "" { c.JSON(http.StatusOK, []string{}) // Return empty array for empty query return } limit := 10 // Default limit if limitStr := c.Query("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit if limit > 50 { limit = 50 // Maximum limit } } } suggestions, err := h.orgService.SearchSuggestions(c.Request.Context(), query, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, suggestions) } func (h *OrganizationHandler) GetNearby(c *gin.Context) { var query struct { Latitude float64 `form:"lat" binding:"required"` Longitude float64 `form:"lng" binding:"required"` RadiusKm float64 `form:"radius" binding:"required"` } if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } orgs, err := h.orgService.GetWithinRadius(c.Request.Context(), query.Latitude, query.Longitude, query.RadiusKm) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, orgs) } // UploadLogo handles logo image uploads for organizations func (h *OrganizationHandler) UploadLogo(c *gin.Context) { orgID := c.Param("id") // Get the uploaded file file, header, err := c.Request.FormFile("logo") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No logo file provided"}) return } defer file.Close() // Save the image uploadedImage, err := h.imageService.SaveImage(file, header, "logos") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save logo: %v", err)}) return } // Update the organization with the new logo URL org, err := h.orgService.GetByID(c.Request.Context(), orgID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } // Delete old logo if it exists if org.LogoURL != "" { h.imageService.DeleteImage(org.LogoURL) } org.LogoURL = uploadedImage.URL if err := h.orgService.Update(c.Request.Context(), org); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"}) return } c.JSON(http.StatusOK, gin.H{ "url": uploadedImage.URL, "message": "Logo uploaded successfully", }) } // UploadGalleryImage handles gallery image uploads for organizations func (h *OrganizationHandler) UploadGalleryImage(c *gin.Context) { orgID := c.Param("id") // Get the uploaded file file, header, err := c.Request.FormFile("image") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No image file provided"}) return } defer file.Close() // Save the image uploadedImage, err := h.imageService.SaveImage(file, header, "gallery") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save image: %v", err)}) return } // Update the organization by adding the image to gallery org, err := h.orgService.GetByID(c.Request.Context(), orgID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } // Get current gallery images galleryImages := []string{} if len(org.GalleryImages) > 0 { // Parse the JSON field var currentImages []string if err := json.Unmarshal(org.GalleryImages, ¤tImages); err == nil { galleryImages = currentImages } } // Add new image galleryImages = append(galleryImages, uploadedImage.URL) // Convert to JSON for storage galleryJSON, _ := json.Marshal(galleryImages) org.GalleryImages = datatypes.JSON(galleryJSON) if err := h.orgService.Update(c.Request.Context(), org); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"}) return } c.JSON(http.StatusOK, gin.H{ "url": uploadedImage.URL, "galleryImages": galleryImages, "message": "Gallery image uploaded successfully", }) } // DeleteGalleryImage removes a gallery image from an organization func (h *OrganizationHandler) DeleteGalleryImage(c *gin.Context) { orgID := c.Param("id") imageURL := c.Query("url") if imageURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Image URL required"}) return } // Get the organization org, err := h.orgService.GetByID(c.Request.Context(), orgID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } // Remove the image from gallery galleryImages := []string{} if len(org.GalleryImages) > 0 { // Parse the JSON field var currentImages []string if err := json.Unmarshal(org.GalleryImages, ¤tImages); err == nil { for _, img := range currentImages { if img != imageURL { galleryImages = append(galleryImages, img) } } } } // Delete the file from disk if err := h.imageService.DeleteImage(imageURL); err != nil { // Log error but continue with database update fmt.Printf("Warning: failed to delete image file: %v\n", err) } // Convert to JSON for storage galleryJSON, _ := json.Marshal(galleryImages) org.GalleryImages = datatypes.JSON(galleryJSON) if err := h.orgService.Update(c.Request.Context(), org); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"}) return } c.JSON(http.StatusOK, gin.H{ "galleryImages": galleryImages, "message": "Gallery image deleted successfully", }) } // GetSimilarOrganizations returns organizations similar to the given organization func (h *OrganizationHandler) GetSimilarOrganizations(c *gin.Context) { orgID := c.Param("id") limitStr := c.DefaultQuery("limit", "5") limit := 5 if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit } // For now, return organizations of the same sector/type // TODO: Implement more sophisticated similarity algorithm org, err := h.orgService.GetByID(c.Request.Context(), orgID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"}) return } // Get organizations by sector similarOrgs, err := h.orgService.GetBySector(c.Request.Context(), org.Sector) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get similar organizations"}) return } // Filter out the original organization and limit results var result []*domain.Organization for _, similarOrg := range similarOrgs { if similarOrg.ID != orgID && len(result) < limit { result = append(result, similarOrg) } } c.JSON(http.StatusOK, result) } // GetOrganizationProposals returns proposals related to the organization func (h *OrganizationHandler) GetOrganizationProposals(c *gin.Context) { // orgID := c.Param("id") // TODO: Use when implementing proposals functionality // TODO: Implement proposals functionality // For now, return empty array c.JSON(http.StatusOK, []interface{}{}) } // GetOrganizationResources returns resource flows for the organization func (h *OrganizationHandler) GetOrganizationResources(c *gin.Context) { orgID := c.Param("id") // Get resource flows by organization ID resourceFlows, err := h.resourceFlowService.GetByOrganizationID(c.Request.Context(), orgID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get organization resources"}) return } c.JSON(http.StatusOK, resourceFlows) } // DirectSymbiosisMatch represents a direct symbiosis match type DirectSymbiosisMatch struct { PartnerID string `json:"partner_id"` PartnerName string `json:"partner_name"` Resource string `json:"resource"` ResourceFlowID string `json:"resource_flow_id"` } // DirectSymbiosisResponse represents the response for direct symbiosis matches type DirectSymbiosisResponse struct { Providers []DirectSymbiosisMatch `json:"providers"` Consumers []DirectSymbiosisMatch `json:"consumers"` } // GetDirectMatches returns direct matches for the organization func (h *OrganizationHandler) GetDirectMatches(c *gin.Context) { orgID := c.Param("id") if orgID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Organization ID is required"}) return } ctx := c.Request.Context() // Get organization's resource flows resourceFlows, err := h.resourceFlowService.GetByOrganizationID(ctx, orgID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get resource flows: %v", err)}) return } var providers []DirectSymbiosisMatch var consumers []DirectSymbiosisMatch // Separate flows by direction var inputFlows []*domain.ResourceFlow // What this org needs (consumes) var outputFlows []*domain.ResourceFlow // What this org provides (produces) for _, flow := range resourceFlows { if flow.Direction == domain.DirectionInput { inputFlows = append(inputFlows, flow) } else if flow.Direction == domain.DirectionOutput { outputFlows = append(outputFlows, flow) } } // Find providers: organizations that can provide what this org needs for _, inputFlow := range inputFlows { // Find organizations that have output flows of the same type matchingOutputs, err := h.resourceFlowService.GetByTypeAndDirection(ctx, inputFlow.Type, domain.DirectionOutput) if err != nil { continue } for _, outputFlow := range matchingOutputs { // Skip if it's the same organization if outputFlow.OrganizationID == orgID { continue } // Get organization info org, err := h.orgService.GetByID(ctx, outputFlow.OrganizationID) if err != nil { continue } providers = append(providers, DirectSymbiosisMatch{ PartnerID: outputFlow.OrganizationID, PartnerName: org.Name, Resource: string(outputFlow.Type), ResourceFlowID: outputFlow.ID, }) } } // Find consumers: organizations that need what this org provides for _, outputFlow := range outputFlows { // Find organizations that have input flows of the same type matchingInputs, err := h.resourceFlowService.GetByTypeAndDirection(ctx, outputFlow.Type, domain.DirectionInput) if err != nil { continue } for _, inputFlow := range matchingInputs { // Skip if it's the same organization if inputFlow.OrganizationID == orgID { continue } // Get organization info org, err := h.orgService.GetByID(ctx, inputFlow.OrganizationID) if err != nil { continue } consumers = append(consumers, DirectSymbiosisMatch{ PartnerID: inputFlow.OrganizationID, PartnerName: org.Name, Resource: string(inputFlow.Type), ResourceFlowID: inputFlow.ID, }) } } // Remove duplicates and limit results providers = h.deduplicateMatches(providers, 10) consumers = h.deduplicateMatches(consumers, 10) response := DirectSymbiosisResponse{ Providers: providers, Consumers: consumers, } c.JSON(http.StatusOK, response) } // deduplicateMatches removes duplicate matches and limits the number of results func (h *OrganizationHandler) deduplicateMatches(matches []DirectSymbiosisMatch, limit int) []DirectSymbiosisMatch { seen := make(map[string]bool) var result []DirectSymbiosisMatch for _, match := range matches { key := match.PartnerID + ":" + match.Resource if !seen[key] && len(result) < limit { seen[key] = true result = append(result, match) } } return result } // GetUserOrganizations returns organizations associated with the current user // For now, returns all organizations (can be enhanced with user-organization relationships later) func (h *OrganizationHandler) GetUserOrganizations(c *gin.Context) { ctx := c.Request.Context() // Get user ID from context (set by middleware) _, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // TODO: In future, implement user-organization relationship table // For now, return all organizations as a temporary solution // This allows the frontend to work while we develop proper user-org relationships organizations, err := h.orgService.GetAll(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get organizations: %v", err)}) return } c.JSON(http.StatusOK, organizations) }