diff --git a/backend/src/models/company.go b/backend/src/models/company.go index cb3930db..96f5ab76 100644 --- a/backend/src/models/company.go +++ b/backend/src/models/company.go @@ -43,31 +43,33 @@ type CompanyParticipation struct { // These are used to sync communications with Gmail. GmailThreadIds []string `json:"gmailThreadIds,omitempty" bson:"gmailThreadIds,omitempty"` - // Stand details - StandDetails StandDetails `json:"standDetails,omitempty" bson:"standDetails,omitempty"` + // Stand details + StandDetails StandDetails `json:"standDetails,omitempty" bson:"standDetails,omitempty"` - // Stand and days at the venue - Stands []Stand `json:"stands,omitempty" bson:"stands,omitempty"` + // Stand and days at the venue + Stands []Stand `json:"stands,omitempty" bson:"stands,omitempty"` + // Tasks tracks the company's task pipeline progress. + Tasks CompanyTasks `json:"tasks" bson:"tasks"` } type Stand struct { - // Stand identifier - StandID string `json:"standId" bson:"standId"` + // Stand identifier + StandID string `json:"standId" bson:"standId"` - // Day at the venue - Date *time.Time `json:"date,omitempty" bson:"date,omitempty"` + // Day at the venue + Date *time.Time `json:"date,omitempty" bson:"date,omitempty"` } type StandDetails struct { - // Number of chairs required by the company - Chairs int `json:"chairs" bson:"chairs"` + // Number of chairs required by the company + Chairs int `json:"chairs" bson:"chairs"` - // Require front table - Table bool `json:"table" bson:"table"` + // Require front table + Table bool `json:"table" bson:"table"` - // Require lettering - Lettering bool `json:"lettering" bson:"lettering"` + // Require lettering + Lettering bool `json:"lettering" bson:"lettering"` } // CompanyBillingInfo of company @@ -132,11 +134,11 @@ type CompanyParticipationPublic struct { // Participation's package is a Package _id (see models.Package). Package PackagePublic `json:"package,omitempty"` - // Stand details - StandDetails StandDetails `json:"standDetails,omitempty"` + // Stand details + StandDetails StandDetails `json:"standDetails,omitempty"` - // Days at the venue - Stands []Stand `json:"stands,omitempty"` + // Days at the venue + Stands []Stand `json:"stands,omitempty"` } // CompanyPublic represents a company to be contacted by the team, that will hopefully participate diff --git a/backend/src/models/speaker.go b/backend/src/models/speaker.go index a7295e2b..501fe7ea 100644 --- a/backend/src/models/speaker.go +++ b/backend/src/models/speaker.go @@ -44,6 +44,9 @@ type SpeakerParticipation struct { // GmailThreadIds is an array of Gmail thread IDs linked to this participation. // These are used to sync communications with Gmail. GmailThreadIds []string `json:"gmailThreadIds,omitempty" bson:"gmailThreadIds,omitempty"` + + // Tasks tracks the speaker's task pipeline progress. + Tasks SpeakerTasks `json:"tasks" bson:"tasks"` } type SpeakerImages struct { @@ -68,8 +71,8 @@ type Speaker struct { Name string `json:"name" bson:"name"` // Contact is an _id of Contact (see models.Contact). - Contact *primitive.ObjectID `json:"contact,omitempty" bson:"contact"` - ContactObject *Contact `json:"contactObject,omitempty" bson:"contactObject"` + Contact *primitive.ObjectID `json:"contact,omitempty" bson:"contact"` + ContactObject *Contact `json:"contactObject,omitempty" bson:"contactObject"` // Title of the speaker (CEO @ HugeCorportation, for example). Title string `json:"title" bson:"title"` @@ -77,8 +80,8 @@ type Speaker struct { // Bio of the speaker. Careful, this will be visible on our website! Bio string `json:"bio" bson:"bio"` - // Company name - CompanyName string `json:"companyName" bson:"companyName"` + // Company name + CompanyName string `json:"companyName" bson:"companyName"` // This is only visible by the team. Praise and trash talk at will. Notes string `json:"notes" bson:"notes"` @@ -120,8 +123,8 @@ type SpeakerPublic struct { // Bio of the speaker. Careful, this will be visible on our website! Bio string `json:"bio" bson:"bio"` - // Company name - CompanyName string `json:"companyName,omitempty" bson:"companyName"` + // Company name + CompanyName string `json:"companyName,omitempty" bson:"companyName"` Images SpeakerImagesPublic `json:"imgs" bson:"imgs"` Participations []SpeakerParticipationPublic `json:"participation" bson:"participations"` diff --git a/backend/src/models/task.go b/backend/src/models/task.go new file mode 100644 index 00000000..32db689b --- /dev/null +++ b/backend/src/models/task.go @@ -0,0 +1,265 @@ +package models + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +// ============================================================ +// Shared task fields (used by both companies and speakers) +// ============================================================ + +// TaskLogos tracks logo delivery status. +type TaskLogos struct { + Received bool `json:"received" bson:"received"` + NeedsReviewing bool `json:"needsReviewing" bson:"needsReviewing"` +} + +// ============================================================ +// Company-specific task types +// ============================================================ + +// CompanyTaskConfirmation tracks confirmation-related progress for a company. +type CompanyTaskConfirmation struct { + AskedForInfo bool `json:"askedForInfo" bson:"askedForInfo"` +} + +// CompanyTaskContract tracks contract lifecycle. +type CompanyTaskContract struct { + Sent bool `json:"sent" bson:"sent"` + Created bool `json:"created" bson:"created"` + Signed bool `json:"signed" bson:"signed"` + ReceiptSent bool `json:"receiptSent" bson:"receiptSent"` + Paid bool `json:"paid" bson:"paid"` +} + +// CompanyTaskSessionTitles tracks session / workshop titles. +type CompanyTaskSessionTitles struct { + PresentationTitle string `json:"presentationTitle" bson:"presentationTitle"` + WorkshopTitle string `json:"workshopTitle" bson:"workshopTitle"` +} + +// CompanyTaskCorlief tracks Corlief-related steps. +type CompanyTaskCorlief struct { + PreNotice bool `json:"preNotice" bson:"preNotice"` + Scheduled bool `json:"scheduled" bson:"scheduled"` + Reserved bool `json:"reserved" bson:"reserved"` +} + +// CompanyTaskLogistics tracks logistics info. +type CompanyTaskLogistics struct { + RequestedInfo bool `json:"requestedInfo" bson:"requestedInfo"` + CarStatus string `json:"carStatus" bson:"carStatus"` // "not_responded", "wants", "not_wants" + LicensePlate string `json:"licensePlate" bson:"licensePlate"` +} + +// CompanyTasks is the top-level task object embedded in CompanyParticipation. +type CompanyTasks struct { + Confirmation CompanyTaskConfirmation `json:"confirmation" bson:"confirmation"` + Logos TaskLogos `json:"logos" bson:"logos"` + Contract CompanyTaskContract `json:"contract" bson:"contract"` + SessionTitles CompanyTaskSessionTitles `json:"sessionTitles" bson:"sessionTitles"` + Corlief CompanyTaskCorlief `json:"corlief" bson:"corlief"` + Logistics CompanyTaskLogistics `json:"logistics" bson:"logistics"` + PO string `json:"po" bson:"po"` +} + +// ============================================================ +// Speaker-specific task types +// ============================================================ + +// SpeakerTaskConfirmation tracks confirmation-related progress for a speaker. +type SpeakerTaskConfirmation struct { + Phone string `json:"phone" bson:"phone"` + LinkedIn string `json:"linkedin" bson:"linkedin"` + WantsLinkedIn string `json:"wantsLinkedinTag" bson:"wantsLinkedinTag"` // "not_responded", "yes", "no" + Observations string `json:"observations" bson:"observations"` +} + +// UnmarshalBSON handles legacy bool values stored for WantsLinkedIn. +func (s *SpeakerTaskConfirmation) UnmarshalBSON(data []byte) error { + var raw bson.Raw + if err := bson.Unmarshal(data, &raw); err != nil { + return err + } + + decodeString := func(key string) string { + val, err := raw.LookupErr(key) + if err != nil { + return "" + } + if val.Type == bsontype.String { + v, _ := val.StringValueOK() + return v + } + return "" + } + + decodeLinkedIn := func(key string) string { + val, err := raw.LookupErr(key) + if err != nil { + return "not_responded" + } + switch val.Type { + case bsontype.Boolean: + b, _ := val.BooleanOK() + return boolToString(b) + case bsontype.String: + v, _ := val.StringValueOK() + return v + default: + return "not_responded" + } + } + + s.Phone = decodeString("phone") + s.LinkedIn = decodeString("linkedin") + s.WantsLinkedIn = decodeLinkedIn("wantsLinkedinTag") + s.Observations = decodeString("observations") + return nil +} + +// MarshalBSON always writes string values. +func (s SpeakerTaskConfirmation) MarshalBSON() ([]byte, error) { + return bson.Marshal(bson.D{ + {Key: "phone", Value: s.Phone}, + {Key: "linkedin", Value: s.LinkedIn}, + {Key: "wantsLinkedinTag", Value: s.WantsLinkedIn}, + {Key: "observations", Value: s.Observations}, + }) +} + +// SpeakerTaskFlightLeg stores one leg (arrival or departure). +type SpeakerTaskFlightLeg struct { + Airport string `json:"airport" bson:"airport"` + FlightNumber string `json:"flightNumber" bson:"flightNumber"` + Date *time.Time `json:"date,omitempty" bson:"date,omitempty"` + Time string `json:"time" bson:"time"` +} + +// SpeakerTaskFlightDetails stores pricing / status / booking info. +type SpeakerTaskFlightDetails struct { + Price string `json:"price" bson:"price"` + Status string `json:"status" bson:"status"` // "pending", "received", "approved", "bought" + Link string `json:"link" bson:"link"` + BookingRef string `json:"bookingRef" bson:"bookingRef"` +} + +// SpeakerTaskFlightRefund stores refund info. +type SpeakerTaskFlightRefund struct { + Amount string `json:"amount" bson:"amount"` + Method string `json:"method" bson:"method"` + InfoNeeded string `json:"infoNeeded" bson:"infoNeeded"` + Status string `json:"status" bson:"status"` // "not_started", "receipt_requested", "info_requested", "done" +} + +// SpeakerTaskFlights stores everything flight-related. +type SpeakerTaskFlights struct { + NeedsFlights string `json:"needsFlights" bson:"needsFlights"` // "not_responded", "yes", "no" + Requested bool `json:"requested" bson:"requested"` + Arrival SpeakerTaskFlightLeg `json:"arrival" bson:"arrival"` + Departure SpeakerTaskFlightLeg `json:"departure" bson:"departure"` + Details SpeakerTaskFlightDetails `json:"details" bson:"details"` + Refund SpeakerTaskFlightRefund `json:"refund" bson:"refund"` +} + +// SpeakerTaskCoverage tracks video/photo coverage confirmation. +type SpeakerTaskCoverage struct { + Video string `json:"video" bson:"video"` // "not_responded", "yes", "no" + Streaming string `json:"streaming" bson:"streaming"` // "not_responded", "yes", "no" + Photo string `json:"photo" bson:"photo"` // "not_responded", "yes", "no" +} + +// boolToString converts legacy boolean values to the new string format. +func boolToString(v interface{}) string { + switch b := v.(type) { + case bool: + if b { + return "yes" + } + return "no" + case string: + return b + default: + return "not_responded" + } +} + +// UnmarshalBSON handles legacy boolean fields being decoded into string fields. +func (c *SpeakerTaskCoverage) UnmarshalBSON(data []byte) error { + var raw bson.Raw + if err := bson.Unmarshal(data, &raw); err != nil { + return err + } + + decodeField := func(key string) string { + val, err := raw.LookupErr(key) + if err != nil { + return "not_responded" + } + switch val.Type { + case bsontype.Boolean: + b, _ := val.BooleanOK() + return boolToString(b) + case bsontype.String: + s, _ := val.StringValueOK() + return s + default: + return "not_responded" + } + } + + c.Video = decodeField("video") + c.Streaming = decodeField("streaming") + c.Photo = decodeField("photo") + return nil +} + +// MarshalBSON always writes string values. +func (c SpeakerTaskCoverage) MarshalBSON() ([]byte, error) { + return bson.Marshal(bson.D{ + {Key: "video", Value: c.Video}, + {Key: "streaming", Value: c.Streaming}, + {Key: "photo", Value: c.Photo}, + }) +} + +// SpeakerTaskMaterials tracks talk info and materials delivery. +type SpeakerTaskMaterials struct { + Requested bool `json:"requested" bson:"requested"` + TalkTitle string `json:"talkTitle" bson:"talkTitle"` + TalkDescription string `json:"talkDescription" bson:"talkDescription"` + Received bool `json:"received" bson:"received"` + TestSchedule string `json:"testSchedule" bson:"testSchedule"` + TestDone bool `json:"testDone" bson:"testDone"` +} + +// SpeakerTaskHotel stores hotel / booking / payment info. +type SpeakerTaskHotel struct { + NeedsHotel string `json:"needsHotel" bson:"needsHotel"` // "not_responded", "yes", "no" + Requested bool `json:"requested" bson:"requested"` + HotelName string `json:"hotelName" bson:"hotelName"` + RoomType string `json:"roomType" bson:"roomType"` + Price string `json:"price" bson:"price"` + CheckIn *time.Time `json:"checkIn,omitempty" bson:"checkIn,omitempty"` + CheckOut *time.Time `json:"checkOut,omitempty" bson:"checkOut,omitempty"` + NumNights string `json:"numNights" bson:"numNights"` + NumGuests string `json:"numGuests" bson:"numGuests"` + GuestNames string `json:"guestNames" bson:"guestNames"` + Invoice bool `json:"invoice" bson:"invoice"` + Paid bool `json:"paid" bson:"paid"` + Notes string `json:"notes" bson:"notes"` +} + +// SpeakerTasks is the top-level task object embedded in SpeakerParticipation. +type SpeakerTasks struct { + Confirmation SpeakerTaskConfirmation `json:"confirmation" bson:"confirmation"` + Logos TaskLogos `json:"logos" bson:"logos"` + AskedForInfo bool `json:"askedForInfo" bson:"askedForInfo"` + Flights SpeakerTaskFlights `json:"flights" bson:"flights"` + Coverage SpeakerTaskCoverage `json:"coverage" bson:"coverage"` + Materials SpeakerTaskMaterials `json:"materials" bson:"materials"` + Hotel SpeakerTaskHotel `json:"hotel" bson:"hotel"` +} diff --git a/backend/src/mongodb/task.go b/backend/src/mongodb/task.go new file mode 100644 index 00000000..0ca31076 --- /dev/null +++ b/backend/src/mongodb/task.go @@ -0,0 +1,107 @@ +package mongodb + +import ( + "context" + "encoding/json" + "io" + "log" + + "github.com/sinfo/deck2/src/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// ============================================================ +// Company Tasks +// ============================================================ + +// UpdateCompanyTasksData holds the payload sent by the frontend. +type UpdateCompanyTasksData struct { + Tasks *models.CompanyTasks `json:"tasks"` +} + +// ParseBody fills the UpdateCompanyTasksData from a request body. +func (d *UpdateCompanyTasksData) ParseBody(body io.Reader) error { + if err := json.NewDecoder(body).Decode(d); err != nil { + return err + } + return nil +} + +// UpdateCompanyTasks persists the task data on the company's current-event participation. +func (c *CompaniesType) UpdateCompanyTasks(companyID primitive.ObjectID, data UpdateCompanyTasksData) (*models.Company, error) { + ctx := context.Background() + var updatedCompany models.Company + + currentEvent, err := Events.GetCurrentEvent() + if err != nil { + return nil, err + } + + setFields := bson.M{ + "participations.$.tasks": data.Tasks, + } + + updateQuery := bson.M{"$set": setFields} + filterQuery := bson.M{"_id": companyID, "participations.event": currentEvent.ID} + + optionsQuery := options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + if err := c.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedCompany); err != nil { + log.Println("Error updating company tasks:", err) + return nil, err + } + + ResetCurrentPublicCompanies() + + return &updatedCompany, nil +} + +// ============================================================ +// Speaker Tasks +// ============================================================ + +// UpdateSpeakerTasksData holds the payload sent by the frontend. +type UpdateSpeakerTasksData struct { + Tasks *models.SpeakerTasks `json:"tasks"` +} + +// ParseBody fills the UpdateSpeakerTasksData from a request body. +func (d *UpdateSpeakerTasksData) ParseBody(body io.Reader) error { + if err := json.NewDecoder(body).Decode(d); err != nil { + return err + } + return nil +} + +// UpdateSpeakerTasks persists the task data on the speaker's current-event participation. +func (s *SpeakersType) UpdateSpeakerTasks(speakerID primitive.ObjectID, data UpdateSpeakerTasksData) (*models.Speaker, error) { + ctx := context.Background() + var updatedSpeaker models.Speaker + + currentEvent, err := Events.GetCurrentEvent() + if err != nil { + return nil, err + } + + setFields := bson.M{ + "participations.$.tasks": data.Tasks, + } + + updateQuery := bson.M{"$set": setFields} + filterQuery := bson.M{"_id": speakerID, "participations.event": currentEvent.ID} + + optionsQuery := options.FindOneAndUpdate() + optionsQuery.SetReturnDocument(options.After) + + if err := s.Collection.FindOneAndUpdate(ctx, filterQuery, updateQuery, optionsQuery).Decode(&updatedSpeaker); err != nil { + log.Println("Error updating speaker tasks:", err) + return nil, err + } + + ResetCurrentPublicSpeakers() + + return &updatedSpeaker, nil +} diff --git a/backend/src/router/init.go b/backend/src/router/init.go index c2db8fbb..2402b4e6 100644 --- a/backend/src/router/init.go +++ b/backend/src/router/init.go @@ -183,6 +183,7 @@ func InitializeRouter() { companyRouter.HandleFunc("/{id}/employer", authMember(addEmployer)).Methods("POST") companyRouter.HandleFunc("/{id}/employer/{rep}", authMember(removeEmployer)).Methods("DELETE") companyRouter.HandleFunc("/{id}/contract/docx", authCoordinator(generateCompanyContractDocx)).Methods("POST") + companyRouter.HandleFunc("/{id}/participation/tasks", authMember(updateCompanyTasks)).Methods("PUT") // speaker handlers speakerRouter := r.PathPrefix("/speakers").Subrouter() @@ -211,6 +212,7 @@ func InitializeRouter() { speakerRouter.HandleFunc("/{id}/participation/gmail-threads", authMember(updateSpeakerGmailThreadIds)).Methods("PUT") speakerRouter.HandleFunc("/{id}/participation/gmail-sync", authMember(syncSpeakerGmailMessages)).Methods("POST") speakerRouter.HandleFunc("/{id}/participation", authCoordinator(removeSpeakerParticipation)).Methods("DELETE") + speakerRouter.HandleFunc("/{id}/participation/tasks", authMember(updateSpeakerTasks)).Methods("PUT") // flightInfo handlers flightInfoRouter := r.PathPrefix("/flightInfo").Subrouter() diff --git a/backend/src/router/task.go b/backend/src/router/task.go new file mode 100644 index 00000000..5f5f79da --- /dev/null +++ b/backend/src/router/task.go @@ -0,0 +1,91 @@ +package router + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/sinfo/deck2/src/models" + "github.com/sinfo/deck2/src/mongodb" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ============================================================ +// Company Tasks +// ============================================================ + +func updateCompanyTasks(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + params := mux.Vars(r) + companyID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Companies.GetCompany(companyID); err != nil { + http.Error(w, "Invalid company ID: "+err.Error(), http.StatusNotFound) + return + } + + var data = &mongodb.UpdateCompanyTasksData{} + + if err := data.ParseBody(r.Body); err != nil { + http.Error(w, "Could not parse body: "+err.Error(), http.StatusBadRequest) + return + } + + updatedCompany, err := mongodb.Companies.UpdateCompanyTasks(companyID, *data) + + if err != nil { + http.Error(w, "Could not update company tasks: "+err.Error(), http.StatusExpectationFailed) + return + } + + json.NewEncoder(w).Encode(updatedCompany) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindUpdated, + Company: &updatedCompany.ID, + }) + } +} + +// ============================================================ +// Speaker Tasks +// ============================================================ + +func updateSpeakerTasks(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + params := mux.Vars(r) + speakerID, _ := primitive.ObjectIDFromHex(params["id"]) + + if _, err := mongodb.Speakers.GetSpeaker(speakerID); err != nil { + http.Error(w, "Invalid speaker ID: "+err.Error(), http.StatusNotFound) + return + } + + var data = &mongodb.UpdateSpeakerTasksData{} + + if err := data.ParseBody(r.Body); err != nil { + http.Error(w, "Could not parse body: "+err.Error(), http.StatusBadRequest) + return + } + + updatedSpeaker, err := mongodb.Speakers.UpdateSpeakerTasks(speakerID, *data) + + if err != nil { + http.Error(w, "Could not update speaker tasks: "+err.Error(), http.StatusExpectationFailed) + return + } + + json.NewEncoder(w).Encode(updatedSpeaker) + + // notify + if credentials, ok := r.Context().Value(credentialsKey).(models.AuthorizationCredentials); ok { + mongodb.Notifications.Notify(credentials.ID, mongodb.CreateNotificationData{ + Kind: models.NotificationKindUpdated, + Speaker: &updatedSpeaker.ID, + }) + } +} diff --git a/frontend/src/api/companies.ts b/frontend/src/api/companies.ts index cb569925..b6a0442c 100644 --- a/frontend/src/api/companies.ts +++ b/frontend/src/api/companies.ts @@ -10,6 +10,7 @@ import type { UpdateCompanyData, UpdateCompanyParticipationData, } from "@/dto/companies"; +import type { CompanyTasks } from "@/dto/tasks"; import type { CreateThread, ParticipationCommunications, @@ -91,6 +92,9 @@ export const updateRepresentativeOrder = ( export const uploadCompanyInternalImage = (id: string, data: FormData) => instance.post(`/companies/${id}/image/internal`, data); +export const uploadCompanyPublicImage = (id: string, data: FormData) => + instance.post(`/companies/${id}/image/public`, data); + export const deleteCompany = (id: string) => instance.delete(`/companies/${id}`); @@ -145,5 +149,8 @@ export const generateCompanyContract = ( }); }; +export const updateCompanyTasks = (id: string, tasks: CompanyTasks) => + instance.put(`/companies/${id}/participation/tasks`, { tasks }); + export const announceAcceptedCompanies = () => instance.post<{ announced: number }>("/companies/announce"); diff --git a/frontend/src/api/speakers.ts b/frontend/src/api/speakers.ts index 130c50c3..8586a286 100644 --- a/frontend/src/api/speakers.ts +++ b/frontend/src/api/speakers.ts @@ -6,6 +6,7 @@ import type { UpdateSpeakerData, UpdateSpeakerParticipationData, } from "@/dto/speakers"; +import type { SpeakerTasks } from "@/dto/tasks"; import { instance } from "."; import { type ParticipationCommunications, @@ -51,6 +52,12 @@ export const createSpeaker = (data: CreateSpeakerData) => export const uploadSpeakerInternalImage = (id: string, data: FormData) => instance.post(`/speakers/${id}/image/internal`, data); +export const uploadSpeakerPublicImage = (id: string, data: FormData) => + instance.post(`/speakers/${id}/image/public/speaker`, data); + +export const uploadSpeakerCompanyImage = (id: string, data: FormData) => + instance.post(`/speakers/${id}/image/public/company`, data); + export const deleteSpeaker = (id: string) => instance.delete(`/speakers/${id}`); @@ -85,3 +92,6 @@ export const syncSpeakerGmailMessages = ( instance.post(`/speakers/${id}/participation/gmail-sync`, { messages, }); + +export const updateSpeakerTasks = (id: string, tasks: SpeakerTasks) => + instance.put(`/speakers/${id}/participation/tasks`, { tasks }); diff --git a/frontend/src/components/ImageUpload.vue b/frontend/src/components/ImageUpload.vue index a5619ede..fdccf3d3 100644 --- a/frontend/src/components/ImageUpload.vue +++ b/frontend/src/components/ImageUpload.vue @@ -76,6 +76,7 @@ interface Props { previewAlt?: string; previewSize?: "sm" | "md"; disabled?: boolean; + initialUrl?: string; } const props = withDefaults(defineProps(), { @@ -85,6 +86,7 @@ const props = withDefaults(defineProps(), { previewAlt: "Image preview", previewSize: "md", disabled: false, + initialUrl: undefined, }); const emit = defineEmits<{ @@ -102,6 +104,11 @@ const { handleUrlChange, } = useImageUpload(); +// Seed the preview from the prop if no new file has been chosen yet +if (props.initialUrl) { + imagePreview.value = props.initialUrl; +} + const previewSizeClass = props.previewSize === "sm" ? "w-20 h-20" : "w-24 h-24"; // Emit the selected file when it changes diff --git a/frontend/src/components/companies/EditableCompanyParticipation.vue b/frontend/src/components/companies/EditableCompanyParticipation.vue index 34a432de..2102776b 100644 --- a/frontend/src/components/companies/EditableCompanyParticipation.vue +++ b/frontend/src/components/companies/EditableCompanyParticipation.vue @@ -120,7 +120,7 @@