Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/src/mongodb/speaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ func (s *SpeakersType) CreateSpeaker(data CreateSpeakerData) (*models.Speaker, e
ctx := context.Background()

contact := bson.M{
"phones": []models.ContactPhone{},
"gender": data.Contact.Gender,
"language": data.Contact.Language,
"phones": []models.ContactPhone{},
"socials": bson.M{
"facebook": "",
"skype": "",
Expand Down
25 changes: 10 additions & 15 deletions frontend/public/templates/speakers/33-pt.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,20 +236,16 @@
"
>
<p>
<strong
>Car<b style="color: red">o/a/e</b> {{.Speaker}},</strong
>
<strong>Car{{.SpeakerArticle}} {{.Speaker}},</strong>
</p>
<p>
Sou <b style="color: red">o/a/e</b> {{.Member}}, um
orgulhoso membro da SINFO. Em nome da equipa organizadora da
SINFO {{.Edition}}, convidamo-l<b style="color: red"
>o/a/e</b
>
a participar como orador<b style="color: red">(a)</b> de uma
palestra no nosso evento. Ficaríamos muito gratos pela
oportunidade de recebê-l<b style="color: red">o/a/e</b> se
aceitar este convite.
Sou {{.MemberArticle}} {{.Member}}, um{{.MemberSuffix}}
orgulhos{{.MemberArticle}} membro da SINFO. Em nome da
equipa organizadora da SINFO {{.Edition}},
convidamo-l{{.SpeakerArticle}} a participar como
orador{{.SpeakerSuffix}} de uma palestra no nosso evento.
Ficaríamos muito gratos pela oportunidade de
recebê-l{{.SpeakerArticle}} se aceitar este convite.
</p>

<p>
Expand Down Expand Up @@ -409,9 +405,8 @@
{{if .Paragraph}} {{.Paragraph}} {{else}} A maioria dos
nossos visitantes são estudantes de Informática ansiosos por
começar as suas carreiras e seria com muito prazer que iam
ouvi-l<b style="color: red">o/a</b> falar sobre o seu
percurso profissional e aprender com a sua experiência!
{{end}}
ouvi-l{{.SpeakerArticle}} falar sobre o seu percurso
profissional e aprender com a sua experiência! {{end}}
</p>

<br />
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/BulkEmailDialogTrigger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ import {
import { humanReadableParticipationStatus } from "@/dto/index";
import type { ParticipationStatus } from "@/dto/index";
import type { CompanyWithParticipation } from "@/dto/companies";
import type { SpeakerWithParticipation } from "@/dto/speakers";
import type { SpeakerWithContactAndParticipation } from "@/dto/speakers";
import {
useBulkCompanyEmails,
useBulkSpeakerEmails,
Expand All @@ -462,7 +462,7 @@ interface Props {
size?: "sm" | "default" | "lg" | "icon";
buttonClass?: string;
companies?: CompanyWithParticipation[];
speakers?: SpeakerWithParticipation[];
speakers?: SpeakerWithContactAndParticipation[];
entityType: "companies" | "speakers";
}

Expand Down Expand Up @@ -499,7 +499,7 @@ const speakerBulk = useBulkSpeakerEmails();
const processBulkEmails = async (
templateCategory: EmailTemplateCategory,
statuses: ParticipationStatus[],
entities: CompanyWithParticipation[] | SpeakerWithParticipation[],
entities: CompanyWithParticipation[] | SpeakerWithContactAndParticipation[],
) => {
if (props.entityType === "companies") {
return companyBulk.processBulkEmails(
Expand All @@ -511,7 +511,7 @@ const processBulkEmails = async (
return speakerBulk.processBulkEmails(
templateCategory,
statuses,
entities as SpeakerWithParticipation[],
entities as SpeakerWithContactAndParticipation[],
);
};

Expand Down
34 changes: 0 additions & 34 deletions frontend/src/components/Communications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,6 @@
<span>{{ getKindLabel(thread.kind) }}</span>
</div>

<div class="flex items-center gap-1">
<div
:class="[
'w-2 h-2 rounded-full',
getStatusColor(thread.status),
]"
></div>
</div>

<span>{{ formatDate(thread.posted) }}</span>
</div>

<!-- Edit mode -->
<div
v-if="editingThreadId === thread.id"
Expand Down Expand Up @@ -413,16 +401,7 @@
import { useQuery, useQueryCache } from "@pinia/colada";
import { getAllEvents } from "@/api/events";
import { getAllMembers } from "@/api/members";
import {
updateCompanyGmailThreadIds,
syncCompanyGmailMessages,
type GmailMessageData,
} from "@/api/companies";
import {
updateSpeakerGmailThreadIds,
syncSpeakerGmailMessages,
} from "@/api/speakers";
import { ThreadKind, ThreadStatus } from "@/dto/threads";

Check failure on line 404 in frontend/src/components/Communications.vue

View workflow job for this annotation

GitHub Actions / Frontend ESLint & Prettier

'ThreadStatus' is defined but never used
import type {
ParticipationCommunications,
ThreadWithEntry,
Expand Down Expand Up @@ -1046,19 +1025,6 @@
}
};

const getStatusColor = (status: ThreadStatus): string => {
switch (status) {
case ThreadStatus.ThreadStatusApproved:
return "bg-green-500";
case ThreadStatus.ThreadStatusReviewed:
return "bg-yellow-500";
case ThreadStatus.ThreadStatusPending:
return "bg-red-500";
default:
return "bg-gray-400";
}
};

const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
Expand Down
42 changes: 30 additions & 12 deletions frontend/src/components/companies/ContactForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<!-- Gender Field -->
<div class="space-y-2">
<Label class="text-sm font-medium">Gender</Label>
<Label class="text-sm font-medium">Gender *</Label>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asterisk in the label 'Gender *' indicates this field should be required, but there is no validation or enforcement in the code to support that.

<ToggleGroup
v-model="formData.contact.gender"
type="single"
Expand All @@ -38,7 +38,7 @@

<!-- Language Field -->
<div class="space-y-2">
<Label class="text-sm font-medium">Language</Label>
<Label class="text-sm font-medium">Language *</Label>
<ToggleGroup
v-model="formData.contact.language"
type="single"
Expand All @@ -60,7 +60,7 @@
<!-- Email Section -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">Email Addresses *</Label>
<Label class="text-sm font-medium">Email Addresses</Label>
<Button
variant="ghost"
size="sm"
Expand Down Expand Up @@ -237,6 +237,18 @@
{{ showMoreSocials ? "- Less" : "+ More" }} social platforms
</Button>
</div>

<!-- Validation Message (Visible in embedded mode) -->
<div
v-if="withoutAction && !isValid && validationMessage"
class="rounded-md bg-destructive/15 p-3 mt-4"
>
<p
class="text-sm font-medium text-destructive flex items-center gap-2"
>
{{ validationMessage }}
</p>
</div>
</div>
</div>

Expand Down Expand Up @@ -350,22 +362,28 @@ watch(
{ immediate: true },
);

const isValid = computed(() => {
const hasName = props.withoutName || formData.name?.trim();
const hasGender = !!formData.contact.gender;
const hasLanguage = !!formData.contact.language;

return hasName && hasGender && hasLanguage;
});

watch(
() => formData,
(newData) => emit("updated", newData),
(newData) => {
if (isValid.value) {
emit("updated", newData);
}
},
{ deep: true, immediate: true },
);

const isValid = computed(() => {
const hasName = props.withoutName || formData.name?.trim();
const hasEmail = formData.contact.mails.some((mail) => mail.mail.trim());
return hasName && hasEmail;
});

const validationMessage = computed(() => {
if (!props.withoutName && !formData.name?.trim()) return "Name is required";
if (!formData.contact.mails.some((mail) => mail.mail.trim()))
return "At least one email is required";
if (!formData.contact.gender) return "Gender is required";
if (!formData.contact.language) return "Language is required";
return "";
});

Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/speakers/CreateSpeakerForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@
Back
</Button>
<div class="flex gap-2">
<Button :disabled="isLoading" @click="createSpeakerAndFinish">
<Button

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 3 relies on ContactForm, adding the required validation there makes the step 3 validation redundant. These changes in this file can be removed.

:disabled="isLoading || !isStep3Valid"
@click="createSpeakerAndFinish"
>
<span>Create Speaker</span>
</Button>
</div>
Expand Down Expand Up @@ -257,6 +260,13 @@ const isStep1Valid = computed(() => {
);
});

const isStep3Valid = computed(() => {
return (
contactData.value.gender != undefined &&
contactData.value.language != undefined
);
});

// Step navigation
const nextStep = () => {
if (currentStep.value === 1 && validateStep1()) {
Expand Down Expand Up @@ -319,6 +329,8 @@ const createSpeakerAndFinish = async () => {
(phone) => phone.phone && phone.phone.trim().length > 0,
),
socials: contactData.value.socials || {},
gender: contactData.value.gender,
language: contactData.value.language,
};

const createData: CreateSpeakerData = {
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/speakers/SpeakerCommunications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<script setup lang="ts">
import { getSpeakerCommunications } from "@/api/speakers";
import type { SpeakerWithParticipation } from "@/dto/speakers";
import type { SpeakerWithContactObject } from "@/dto/speakers";
import { useEventStore } from "@/stores/event";
import { usePostSpeakerThreadMutation } from "@/mutations/speakers";
import Communications from "../Communications.vue";
Expand All @@ -22,7 +22,7 @@ import { computed } from "vue";
import { useAuthStore } from "@/stores/auth";

const props = defineProps<{
speaker: SpeakerWithParticipation;
speaker: SpeakerWithContactObject;
}>();

const eventStore = useEventStore();
Expand All @@ -41,9 +41,15 @@ const templates = computed(() =>

const createSpeakerTemplateVariables = () => {
const endDate = new Date(eventStore.selectedEvent?.end || 0);
const memberGender = authStore.member!.contactObject.gender;
const speakerGender = props.speaker.contactObject.gender;

return [
createEmailVariable.memberArticle(memberGender),
createEmailVariable.memberSuffix(memberGender),
createEmailVariable.member(authStore.member!),
createEmailVariable.speakerArticle(speakerGender),
createEmailVariable.speakerSuffix(speakerGender),
createEmailVariable.speaker(props.speaker),
createEmailVariable.edition(eventStore.selectedEvent?.id || 0),
createEmailVariable.editionOrdinal(eventStore.selectedEvent?.id || 0),
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/composables/useBulkEmails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { useAuthStore } from "@/stores/auth";
import { useEventStore } from "@/stores/event";
import { getCompanyRepresentatives } from "@/api/companies";
import type { CompanyWithParticipation } from "@/dto/companies";
import type { Speaker, SpeakerWithParticipation } from "@/dto/speakers";
import type {
Speaker,
SpeakerWithContactAndParticipation,
} from "@/dto/speakers";
import type { ParticipationStatus } from "@/dto/index";
import {
EmailTemplateCategory,
Expand All @@ -24,10 +27,10 @@ import { Gender, Language } from "@/dto/contacts";
// Generic types for bulk emails
export type BulkEmailEntity =
| CompanyWithParticipation
| SpeakerWithParticipation;
| SpeakerWithContactAndParticipation;
function isSpeaker(
entity: BulkEmailEntity,
): entity is SpeakerWithParticipation {
): entity is SpeakerWithContactAndParticipation {
return (entity as Speaker).companyName !== undefined;
}

Expand Down Expand Up @@ -176,9 +179,9 @@ const companyEmailFetcher: EmailFetcher<CompanyWithParticipation> = {
};

// Speaker email fetcher
const speakerEmailFetcher: EmailFetcher<SpeakerWithParticipation> = {
const speakerEmailFetcher: EmailFetcher<SpeakerWithContactAndParticipation> = {
getEmail: async (
speaker: SpeakerWithParticipation,
speaker: SpeakerWithContactAndParticipation,
): Promise<EmailWithDetails | null> => {
try {
const response = await getSpeakerById(speaker.id);
Expand All @@ -201,7 +204,8 @@ const speakerEmailFetcher: EmailFetcher<SpeakerWithParticipation> = {
);
}
},
getEntityName: (speaker: SpeakerWithParticipation): string => speaker.name,
getEntityName: (speaker: SpeakerWithContactAndParticipation): string =>
speaker.name,
};

export const useBulkEmails = <T extends BulkEmailEntity>(
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/composables/useDirectEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { useAuthStore } from "@/stores/auth";
import { useEventStore } from "@/stores/event";
import { getCompanyRepresentatives } from "@/api/companies";
import type { CompanyWithParticipation } from "@/dto/companies";
import type { Speaker, SpeakerWithParticipation } from "@/dto/speakers";
import type {
Speaker,
SpeakerWithContactAndParticipation,
} from "@/dto/speakers";
import {
EmailTemplateCategory,
getVariablesFromType,
Expand All @@ -22,11 +25,11 @@ import type { Contact } from "@/dto/contacts";

export type DirectEmailEntity =
| CompanyWithParticipation
| SpeakerWithParticipation;
| SpeakerWithContactAndParticipation;

function isSpeaker(
entity: DirectEmailEntity,
): entity is SpeakerWithParticipation {
): entity is SpeakerWithContactAndParticipation {
return (entity as Speaker).companyName !== undefined;
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/dto/speakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ export interface UpdateSpeakerParticipationData {
feedback?: string;
room?: SpeakerParticipationRoom;
}

export type SpeakerWithContactAndParticipation = SpeakerWithContactObject &
SpeakerWithParticipation;
Loading
Loading