diff --git a/backend/src/models/company.go b/backend/src/models/company.go index bbda4947..f05d393c 100644 --- a/backend/src/models/company.go +++ b/backend/src/models/company.go @@ -104,6 +104,8 @@ type Company struct { Site string `json:"site" bson:"site"` + LinkedIn string `json:"linkedin,omitempty" bson:"linkedin,omitempty"` + // Company's contacts is an array of CompanyRep _id (see models.CompanyRep). Employers []primitive.ObjectID `json:"employers,omitempty" bson:"employers,omitempty"` @@ -140,8 +142,6 @@ type CompanyPublic struct { Name string `json:"name"` - Description string `json:"description"` - // Company's image (public). Image string `json:"img,omitempty"` diff --git a/backend/src/models/contact.go b/backend/src/models/contact.go index 0ab56825..3d904c91 100644 --- a/backend/src/models/contact.go +++ b/backend/src/models/contact.go @@ -8,7 +8,6 @@ import ( type ContactPhone struct { Phone string `json:"phone" bson:"phone"` - Valid bool `json:"valid" bson:"valid"` } type ContactSocials struct { @@ -22,7 +21,6 @@ type ContactSocials struct { type ContactMail struct { Mail string `json:"mail" bson:"mail"` Personal bool `json:"personal" bson:"personal"` - Valid bool `json:"valid" bson:"valid"` } type Gender string @@ -51,11 +49,11 @@ type Contact struct { Mails []ContactMail `json:"mails" bson:"mails"` } -// HasPhone (phone) returns true if contact has a valid phone +// HasPhone (phone) returns true if contact has a phone // number that is a case insensitive partial match to `phone` func (c *Contact) HasPhone(p string) bool { for _, s := range c.Phones { - if strings.Contains(strings.ToLower(s.Phone), strings.ToLower(p)) && s.Valid { + if strings.Contains(strings.ToLower(s.Phone), strings.ToLower(p)) { return true } } @@ -66,7 +64,7 @@ func (c *Contact) HasPhone(p string) bool { // that is a case insensitive partial match to `mail` func (c *Contact) HasMail(m string) bool { for _, s := range c.Mails { - if strings.Contains(strings.ToLower(s.Mail), strings.ToLower(m)) && s.Valid { + if strings.Contains(strings.ToLower(s.Mail), strings.ToLower(m)) { return true } } diff --git a/backend/src/mongodb/company.go b/backend/src/mongodb/company.go index 8a792eb0..0d936479 100644 --- a/backend/src/mongodb/company.go +++ b/backend/src/mongodb/company.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "strings" "time" "go.mongodb.org/mongo-driver/bson/primitive" @@ -31,11 +32,26 @@ func ResetCurrentPublicCompanies() { currentPublicCompanies = nil } +func formatLinkedInURL(url *string) *string { + if url == nil || *url == "" { + return url + } + + trimmed := strings.TrimSpace(*url) + if strings.HasPrefix(strings.ToLower(trimmed), "http://") || strings.HasPrefix(strings.ToLower(trimmed), "https://") { + return &trimmed + } + + formatted := "https://linkedin.com/company/" + strings.TrimPrefix(trimmed, "@") + return &formatted +} + // CreateCompanyData holds data needed to create a company type CreateCompanyData struct { Name *string `json:"name"` Description *string `json:"description"` Site *string `json:"site"` + LinkedIn *string `json:"linkedin"` } // ParseBody fills the CreateCompanyData from a body @@ -80,10 +96,17 @@ func (c *CompaniesType) CreateCompany(data CreateCompanyData) (*models.Company, return nil, err } + var linkedinValue interface{} = nil + formattedLinkedin := formatLinkedInURL(data.LinkedIn) + if formattedLinkedin != nil { + linkedinValue = *formattedLinkedin + } + insertResult, err := c.Collection.InsertOne(ctx, bson.M{ "name": data.Name, "description": data.Description, "site": data.Site, + "linkedin": linkedinValue, "employers": []primitive.ObjectID{}, "participations": []models.CompanyParticipation{}, "contact": createdContact.InsertedID.(primitive.ObjectID), @@ -325,11 +348,10 @@ func (c *CompaniesType) GetCompaniesByMembers(memberIDs []primitive.ObjectID, ev func companyToPublic(company models.Company, eventID *int) (*models.CompanyPublic, error) { public := models.CompanyPublic{ - ID: company.ID, - Name: company.Name, - Image: company.Images.Public, - Site: company.Site, - Description: company.Description, + ID: company.ID, + Name: company.Name, + Image: company.Images.Public, + Site: company.Site, } var participation *models.CompanyParticipation @@ -860,10 +882,11 @@ func (c *CompaniesType) UpdateCompanyParticipationStatus(companyID primitive.Obj // UpdateCompanyData is the data used to update a company, using the method UpdateCompany. type UpdateCompanyData struct { - Name *string - Description *string - Site *string - BillingInfo *models.CompanyBillingInfo + Name *string `json:"name"` + Description *string `json:"description"` + Site *string `json:"site"` + LinkedIn *string `json:"linkedin"` + BillingInfo *models.CompanyBillingInfo `json:"billingInfo"` } // ParseBody fills the UpdateCompanyData from a body @@ -893,6 +916,12 @@ func (c *CompaniesType) UpdateCompany(companyID primitive.ObjectID, data UpdateC if data.Site != nil { updateFields["site"] = *data.Site } + if data.LinkedIn != nil { + formattedLinkedin := formatLinkedInURL(data.LinkedIn) + if formattedLinkedin != nil { + updateFields["linkedin"] = *formattedLinkedin + } + } if data.BillingInfo != nil { billingInfo := *data.BillingInfo updateFields["billingInfo.name"] = billingInfo.Name diff --git a/backend/src/mongodb/companyReps.go b/backend/src/mongodb/companyReps.go index 2f1738b3..9efff979 100644 --- a/backend/src/mongodb/companyReps.go +++ b/backend/src/mongodb/companyReps.go @@ -105,23 +105,18 @@ func (c *CompanyRepsType) CreateCompanyRep(data CreateCompanyRepData) (*models.C dataQuery["contact"] = contact.ID } else { - contact, err := Contacts.Collection.InsertOne(ctx, bson.M{ - "phones": []models.ContactPhone{}, - "socials": bson.M{ - "facebook": "", - "skype": "", - "github": "", - "twitter": "", - "linkedin": "", - }, - "mails": []models.ContactMail{}, - }) - + // Create empty contact using CreateContact to ensure proper initialization + emptyContact := CreateContactData{ + Phones: []models.ContactPhone{}, + Socials: models.ContactSocials{}, + Mails: []models.ContactMail{}, + } + contact, err := Contacts.CreateContact(emptyContact) if err != nil { return nil, err } - dataQuery["contact"] = contact.InsertedID.(primitive.ObjectID) + dataQuery["contact"] = contact.ID } insertResult, err := c.Collection.InsertOne(ctx, dataQuery) @@ -140,7 +135,7 @@ func (c *CompanyRepsType) CreateCompanyRep(data CreateCompanyRepData) (*models.C return newRep, nil } -// UpdateCompanyRep creates a contact and adds it to a companyRep +// UpdateCompanyRep updates a company rep and its contact func (c *CompanyRepsType) UpdateCompanyRep(id primitive.ObjectID, data CreateCompanyRepData) (*models.CompanyRep, error) { ctx := context.Background() @@ -149,13 +144,17 @@ func (c *CompanyRepsType) UpdateCompanyRep(id primitive.ObjectID, data CreateCom } if data.Contact != nil { - contact, err := Contacts.CreateContact(*data.Contact) + // Get the existing rep to find its contact ID + existingRep, err := c.GetCompanyRep(id) if err != nil { return nil, err } - // add contact id to the $set document - setDoc["contact"] = contact.ID + // Update the existing contact instead of creating a new one + _, err = Contacts.UpdateContact(existingRep.Contact, *data.Contact) + if err != nil { + return nil, err + } } var updateQuery = bson.M{"$set": setDoc} diff --git a/backend/src/mongodb/contact.go b/backend/src/mongodb/contact.go index 79f579d5..3cd433a5 100644 --- a/backend/src/mongodb/contact.go +++ b/backend/src/mongodb/contact.go @@ -16,6 +16,41 @@ import ( "go.mongodb.org/mongo-driver/bson" ) +func formatSocialURL(platform, url string) string { + if url == "" { + return "" + } + + trimmed := strings.TrimSpace(url) + if strings.HasPrefix(strings.ToLower(trimmed), "http://") || strings.HasPrefix(strings.ToLower(trimmed), "https://") { + return trimmed + } + + switch strings.ToLower(platform) { + case "linkedin": + return "https://linkedin.com/in/" + strings.TrimPrefix(trimmed, "@") + case "twitter": + return "https://twitter.com/" + strings.TrimPrefix(trimmed, "@") + case "facebook": + return "https://facebook.com/" + strings.TrimPrefix(trimmed, "@") + case "github": + return "https://github.com/" + strings.TrimPrefix(trimmed, "@") + default: + return trimmed + } +} + +// formatContactSocials formats all social media links in a contact +func formatContactSocials(socials models.ContactSocials) models.ContactSocials { + return models.ContactSocials{ + LinkedIn: formatSocialURL("linkedin", socials.LinkedIn), + Twitter: formatSocialURL("twitter", socials.Twitter), + Facebook: formatSocialURL("facebook", socials.Facebook), + Github: formatSocialURL("github", socials.Github), + Skype: socials.Skype, + } +} + // ContactsType stores importat db information on contacts type ContactsType struct { Collection *mongo.Collection @@ -127,7 +162,7 @@ func (c *ContactsType) CreateContact(data CreateContactData) (*models.Contact, e ctx := context.Background() var insertData = bson.M{} insertData["phones"] = data.Phones - insertData["socials"] = data.Socials + insertData["socials"] = formatContactSocials(data.Socials) insertData["mails"] = data.Mails insertData["gender"] = data.Gender insertData["language"] = data.Language @@ -150,10 +185,10 @@ func (c *ContactsType) UpdateContact(contactID primitive.ObjectID, data CreateCo ctx := context.Background() var updateQuery = bson.M{ "$set": bson.M{ - "phones": data.Phones, - "socials": data.Socials, - "mails": data.Mails, - "gender": data.Gender, + "phones": data.Phones, + "socials": formatContactSocials(data.Socials), + "mails": data.Mails, + "gender": data.Gender, "language": data.Language, }, } diff --git a/backend/src/mongodb/speaker.go b/backend/src/mongodb/speaker.go index f7c6e77b..9465258c 100644 --- a/backend/src/mongodb/speaker.go +++ b/backend/src/mongodb/speaker.go @@ -141,7 +141,7 @@ func (s *SpeakersType) CreateSpeaker(data CreateSpeakerData) (*models.Speaker, e if data.Contact != nil { contact["phones"] = data.Contact.Phones contact["mails"] = data.Contact.Mails - contact["socials"] = data.Contact.Socials + contact["socials"] = formatContactSocials(data.Contact.Socials) } createdContact, err := Contacts.Collection.InsertOne(ctx, contact) diff --git a/backend/src/router/contacts_test.go b/backend/src/router/contacts_test.go index 4ee0df7c..5731fe03 100644 --- a/backend/src/router/contacts_test.go +++ b/backend/src/router/contacts_test.go @@ -17,7 +17,6 @@ var ( Contact1 *models.Contact Contact1Phone = models.ContactPhone{ Phone: "1", - Valid: true, } Contact1Socials = models.ContactSocials{ Facebook: "facebook", @@ -29,7 +28,6 @@ var ( Contact1Mail = models.ContactMail{ Mail: "2", Personal: true, - Valid: true, } Contact1Data = mongodb.CreateContactData{ Phones: append(make([]models.ContactPhone, 0), Contact1Phone), @@ -39,7 +37,6 @@ var ( Contact2 *models.Contact Contact2Phone = models.ContactPhone{ Phone: "3", - Valid: true, } Contact2Socials = models.ContactSocials{ Facebook: "facebook2", @@ -51,7 +48,6 @@ var ( Contact2Mail = models.ContactMail{ Mail: "4", Personal: true, - Valid: true, } Contact2Data = mongodb.CreateContactData{ Phones: append(make([]models.ContactPhone, 0), Contact2Phone), diff --git a/backend/swagger/companies-id.json b/backend/swagger/companies-id.json index 001e2a89..48b435ac 100644 --- a/backend/swagger/companies-id.json +++ b/backend/swagger/companies-id.json @@ -87,6 +87,9 @@ "site": { "type": "string" }, + "linkedin": { + "type": "string" + }, "billingInfo": { "type": "object", "properties": { diff --git a/backend/swagger/companies.json b/backend/swagger/companies.json index dd1ad1fe..54bdd8eb 100644 --- a/backend/swagger/companies.json +++ b/backend/swagger/companies.json @@ -99,6 +99,9 @@ }, "site": { "type": "string" + }, + "linkedin": { + "type": "string" } } } diff --git a/backend/swagger/models/company.json b/backend/swagger/models/company.json index eccbad08..b54db7a7 100644 --- a/backend/swagger/models/company.json +++ b/backend/swagger/models/company.json @@ -24,6 +24,9 @@ "site": { "type": "string" }, + "linkedin": { + "type": "string" + }, "employers": { "type": "array", "items": { diff --git a/frontend/src/components/ContactCard.vue b/frontend/src/components/ContactCard.vue index f4cae330..606a7084 100644 --- a/frontend/src/components/ContactCard.vue +++ b/frontend/src/components/ContactCard.vue @@ -122,10 +122,7 @@ -
+
Personal - - Invalid -
@@ -168,15 +158,6 @@ > {{ phone.phone }} -
- - Invalid - -
diff --git a/frontend/src/components/ParticipationsCard.vue b/frontend/src/components/ParticipationsCard.vue index 8d143a6a..e9fefb20 100644 --- a/frontend/src/components/ParticipationsCard.vue +++ b/frontend/src/components/ParticipationsCard.vue @@ -1,63 +1,44 @@ diff --git a/frontend/src/components/cards/CompanyCard.vue b/frontend/src/components/cards/CompanyCard.vue index 571a0715..93de3c2e 100644 --- a/frontend/src/components/cards/CompanyCard.vue +++ b/frontend/src/components/cards/CompanyCard.vue @@ -51,6 +51,7 @@ name: company.name, description: company.description, site: company.site, + linkedin: company.linkedin, }" :is-loading="isUpdating || isUploadingImage" mode="edit" @@ -112,6 +113,18 @@ {{ formatWebsite(company.site) }} + +
+ LinkedIn: + + {{ formatLinkedIn(company.linkedin) }} + +
@@ -197,7 +210,7 @@ const handleImageSelected = (file: File) => { }; const handleSubmit = async ( - data: Pick, + data: Pick, ) => { if (!props.company?.id) return; @@ -247,6 +260,16 @@ const formatWebsite = (url: string): string => { } }; +const formatLinkedIn = (url: string): string => { + try { + const urlObj = new URL(url); + const path = urlObj.pathname.replace(/^\//, ""); + return `${urlObj.hostname}/${path}`; + } catch { + return url; + } +}; + const canDelete = computed(() => { if (!authStore.decoded) return false; const role = (authStore.decoded as { role?: string }).role; diff --git a/frontend/src/components/cards/WorkflowCard.vue b/frontend/src/components/cards/WorkflowCard.vue index 0a82461f..628e1ed4 100644 --- a/frontend/src/components/cards/WorkflowCard.vue +++ b/frontend/src/components/cards/WorkflowCard.vue @@ -78,65 +78,69 @@ watch( diff --git a/frontend/src/components/companies/CompanyInfoForm.vue b/frontend/src/components/companies/CompanyInfoForm.vue index 97487d6c..e56d2a04 100644 --- a/frontend/src/components/companies/CompanyInfoForm.vue +++ b/frontend/src/components/companies/CompanyInfoForm.vue @@ -43,6 +43,20 @@ /> + +
+ + +
+
+
+ + + {{ + errors.linkedin + }} +
+
@@ -162,13 +155,6 @@ > {{ phone.phone }} - - Invalid -