From 1f040c45618474befae05f84cddde67d4566a203 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 10 Apr 2026 15:26:50 +0530 Subject: [PATCH 1/7] refactor: jobs list and form ui --- frontend/src/pages/JobForm.vue | 145 ++++++++++++++++++++++++++------- frontend/src/pages/Jobs.vue | 8 +- 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/frontend/src/pages/JobForm.vue b/frontend/src/pages/JobForm.vue index 5524223b..424df035 100644 --- a/frontend/src/pages/JobForm.vue +++ b/frontend/src/pages/JobForm.vue @@ -13,13 +13,13 @@ -
-
-
- {{ __('Job Details') }} -
-
-
+
+
+
+
+ {{ __('Job Details') }} +
+
-
+
+ + +
+
+
+
+ +
+
+
+ {{ __('Location') }} +
-
-
-
-
-
- {{ __('Company Details') }} -
-
-
+
+
+ {{ __('Company Details') }} +
-
-
-
+
diff --git a/frontend/src/pages/Jobs.vue b/frontend/src/pages/Jobs.vue index d068d257..44688380 100644 --- a/frontend/src/pages/Jobs.vue +++ b/frontend/src/pages/Jobs.vue @@ -22,13 +22,13 @@ - {{ __('New Job') }} + {{ __('Create') }}
@@ -96,8 +96,8 @@
-
-
+
+
-
-
-

- {{ applicationCount }} - {{ applicationCount === 1 ? __('Application') : __('Applications') }} -

+
+
+
+ {{ totalApplications.data }} + {{ + totalApplications.data === 1 + ? __('Application') + : __('Applications') + }} +
+ + +
@@ -32,9 +41,10 @@ showTooltip: false, selectable: false, }" + class="h-[79vh] border-b" > {{ item }}
-
+
+
+
+ {{ applications.data?.length }} {{ __('of') }} + {{ totalApplications.data }} +
@@ -172,8 +181,7 @@ import { usePageMeta, toast, } from 'frappe-ui' -import { RefreshCw } from 'lucide-vue-next' -import { computed, inject, onMounted, ref, reactive } from 'vue' +import { computed, inject, ref, reactive, watch } from 'vue' import { sessionStore } from '../stores/session' import EmptyState from '@/components/EmptyState.vue' @@ -181,7 +189,7 @@ const dayjs = inject('$dayjs') const { brand } = sessionStore() const showEmailModal = ref(false) const selectedApplicant = ref(null) -const applicationCount = ref(0) +const search = ref('') const emailForm = reactive({ subject: '', message: '', @@ -195,19 +203,6 @@ const props = defineProps({ }, }) -onMounted(() => { - getApplicationCount() -}) - -const getApplicationCount = () => { - call('frappe.client.get_count', { - doctype: 'LMS Job Application', - filters: { job: props.job }, - }).then((count) => { - applicationCount.value = count - }) -} - const applications = createListResource({ doctype: 'LMS Job Application', fields: [ @@ -225,6 +220,37 @@ const applications = createListResource({ auto: true, }) +const totalApplications = createResource({ + url: 'frappe.client.get_count', + params: { + doctype: 'LMS Job Application', + filters: { + job: props.job, + }, + }, + auto: true, + cache: ['totalApplications', props.job], + onError(err) { + toast.error(err.messages?.[0] || err) + console.error('Error fetching total applications:', err) + }, +}) + +watch(search, () => { + let filters = { + job: props.job, + user: ['like', `%${search.value}%`], + } + applications.update({ + filters: filters, + }) + applications.reload() + totalApplications.update({ + filters: filters, + }) + totalApplications.reload() +}) + const emailResource = createResource({ url: 'frappe.core.doctype.communication.email.make', makeParams(values) { @@ -298,25 +324,26 @@ const applicationColumns = computed(() => { { label: __('Full Name'), key: 'full_name', - width: 2, + width: 3, icon: 'user', }, { label: __('Email'), key: 'email', - width: 2, + width: 3, icon: 'at-sign', }, { label: __('Applied On'), key: 'applied_on', - width: 1, + width: 2, icon: 'calendar', }, { label: '', key: 'actions', width: 1, + align: 'right', }, ] }) @@ -326,7 +353,7 @@ const applicantRows = computed(() => { return applications.data.map((application) => ({ ...application, full_name: application.full_name, - applied_on: dayjs(application.creation).fromNow(), + applied_on: dayjs(application.creation).format('DD MMM YYYY'), })) }) diff --git a/frontend/src/pages/Quizzes.vue b/frontend/src/pages/Quizzes.vue index 47cc4231..2935120b 100644 --- a/frontend/src/pages/Quizzes.vue +++ b/frontend/src/pages/Quizzes.vue @@ -184,6 +184,7 @@ watch(search, () => { totalQuizzes.update({ filters: quizFilters.value, }) + totalQuizzes.reload() }) const quizzes = createListResource({ From c7ccb2d1c5234263a62a071be589c962fba50218 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 10 Apr 2026 18:06:30 +0530 Subject: [PATCH 3/7] refactor: certified participants list --- frontend/src/pages/Assignments.vue | 1 + frontend/src/pages/CertifiedParticipants.vue | 115 +++++++++--------- frontend/src/pages/JobForm.vue | 87 ------------- .../ProgrammingExercises.vue | 1 + lms/lms/api.py | 10 +- 5 files changed, 64 insertions(+), 150 deletions(-) diff --git a/frontend/src/pages/Assignments.vue b/frontend/src/pages/Assignments.vue index 17a1e75a..f2e6f6be 100644 --- a/frontend/src/pages/Assignments.vue +++ b/frontend/src/pages/Assignments.vue @@ -182,6 +182,7 @@ watch([titleFilter, typeFilter], () => { totalAssignments.update({ filters: assignmentFilter.value, }) + totalAssignments.reload() }) const reloadAssignments = () => { diff --git a/frontend/src/pages/CertifiedParticipants.vue b/frontend/src/pages/CertifiedParticipants.vue index 410a337a..f82f94d8 100644 --- a/frontend/src/pages/CertifiedParticipants.vue +++ b/frontend/src/pages/CertifiedParticipants.vue @@ -12,9 +12,9 @@ -
-
-
+
+
+
{{ memberCount }} {{ __('Certified Members') }}
-
- +
+
+ + + {{ participant.certificate_count }} + {{ + participant.certificate_count > 1 + ? __('certificates') + : __('certificate') + }} + +
+
+ + {{ + dayjs(participant.issue_date).format('DD MMM YYYY') + }} +
+
+
+
-
- +
+
+ {{ participants.data?.length }} {{ __('of') }} + {{ memberCount }} +
@@ -132,7 +132,7 @@ import { usePageMeta, } from 'frappe-ui' import { computed, inject, onMounted, ref } from 'vue' -import { GraduationCap } from 'lucide-vue-next' +import { GraduationCap, Calendar } from 'lucide-vue-next' import { sessionStore } from '../stores/session' import { useRouter } from 'vue-router' import EmptyState from '@/components/EmptyState.vue' @@ -163,7 +163,6 @@ const participants = createListResource({ url: 'lms.lms.api.get_certified_participants', start: 0, cache: ['certified_participants'], - pageLength: 100, }) const getMemberCount = () => { diff --git a/frontend/src/pages/JobForm.vue b/frontend/src/pages/JobForm.vue index 424df035..58b712af 100644 --- a/frontend/src/pages/JobForm.vue +++ b/frontend/src/pages/JobForm.vue @@ -109,93 +109,6 @@
-
diff --git a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue index 9a1ae149..f9c43bf6 100644 --- a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue +++ b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue @@ -203,6 +203,7 @@ const updateList = () => { totalExercises.update({ filters: filters, }) + totalExercises.reload() } const getFilters = () => { diff --git a/lms/lms/api.py b/lms/lms/api.py index cd5f5ac8..bfbde0cf 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -344,11 +344,11 @@ def get_evaluator_details(evaluator: str): @frappe.whitelist() -def get_certified_participants(filters: dict = None, start: int = 0, page_length: int = 100): +def get_certified_participants(filters: dict = None, start: int = 0, page_length: int = 40): query = get_certification_query(filters) query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length) participants = query.run(as_dict=True) - + print(participants) for participant in participants: details = get_certified_participant_details(participant.member) participant.update(details) @@ -361,7 +361,7 @@ def get_certified_participant_details(member: str): details = frappe.db.get_value( "User", member, - ["full_name", "user_image", "username", "country", "headline", "open_to"], + ["full_name", "user_image", "username", "creation", "headline", "open_to"], as_dict=1, ) details["certificate_count"] = count @@ -374,12 +374,12 @@ def get_certification_query(filters: dict = None): query = ( frappe.qb.from_(Certificate) - .select(Certificate.member, Certificate.issue_date) - .distinct() + .select(Certificate.member, fn.Max(Certificate.issue_date).as_("issue_date")) .join(User) .on(Certificate.member == User.name) .where(Certificate.published == 1) .where(User.enabled == 1) + .groupby(Certificate.member) ) if filters: From af08e6842ad5ee1a568b3747d68346692ede09b0 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 10 Apr 2026 19:54:59 +0530 Subject: [PATCH 4/7] fix: list pagination --- frontend/src/pages/Assignments.vue | 12 +- frontend/src/pages/CertifiedParticipants.vue | 4 +- frontend/src/pages/Jobs.vue | 105 ++++++++++++------ .../ProgrammingExercises.vue | 12 +- frontend/src/pages/Quizzes.vue | 14 ++- lms/lms/api.py | 9 +- 6 files changed, 102 insertions(+), 54 deletions(-) diff --git a/frontend/src/pages/Assignments.vue b/frontend/src/pages/Assignments.vue index f2e6f6be..31f21c67 100644 --- a/frontend/src/pages/Assignments.vue +++ b/frontend/src/pages/Assignments.vue @@ -20,8 +20,8 @@ -
-
+
+
{{ __('{0} Assignments').format(assignments.data?.length) }}
@@ -52,7 +52,7 @@ showAssignmentForm = true }, }" - class="h-[79vh] border-b" + class="h-[79vh] px-5" > - -
+
+ +
+
diff --git a/frontend/src/pages/CertifiedParticipants.vue b/frontend/src/pages/CertifiedParticipants.vue index f82f94d8..56b899cb 100644 --- a/frontend/src/pages/CertifiedParticipants.vue +++ b/frontend/src/pages/CertifiedParticipants.vue @@ -107,7 +107,9 @@
- +
+ +
+
+
+ {{ jobs.data?.length }} {{ __('of') }} + {{ jobCount.data }} +
+
@@ -119,6 +134,7 @@ import { Button, Breadcrumbs, call, + createListResource, createResource, FormControl, TabButtons, @@ -141,7 +157,6 @@ const searchQuery = ref('') const country = ref(null) const filters = ref({}) const orFilters = ref({}) -const jobCount = ref(0) const closedJobs = ref(0) const activeTab = ref('Open') const readOnlyMode = window.read_only_mode @@ -157,9 +172,7 @@ const isModerator = computed(() => { }) const getClosedJobCount = () => { - if (!user.data?.name) { - return - } + if (!user.data?.name) return const filters = { status: 'Closed', @@ -177,6 +190,14 @@ const getClosedJobCount = () => { }) } +const jobCount = createResource({ + url: 'frappe.client.get_count', + params: { + doctype: 'Job Opportunity', + filters: filters.value, + }, +}) + const setFiltersFromURL = () => { let queries = new URLSearchParams(location.search) if (queries.has('type')) { @@ -187,53 +208,53 @@ const setFiltersFromURL = () => { } } -const tabs = computed(() => { - const tabsArray = [ - { - label: __('Open'), - }, - ] - - if (closedJobs.value) { - tabsArray.push({ - label: __('Closed'), - }) - } - - return tabsArray -}) - -const jobs = createResource({ +const jobs = createListResource({ url: 'lms.lms.api.get_job_opportunities', + doctype: 'Job Opportunity', + start: 0, cache: ['jobs'], }) const updateJobs = () => { updateFilters() jobs.update({ - params: { - filters: filters.value, - orFilters: orFilters.value, - }, + filters: filters.value, + orFilters: orFilters.value, }) jobs.reload() + jobCount.update({ + filters: filters.value, + orFilters: orFilters.value, + }) + jobCount.reload() } const updateFilters = () => { filters.value.status = 'Open' + updateJobTypeFilter() + updateWorkModeFilter() + updateSearchQueryFilter() + updateCountryFilter() + updateTabFilter() +} +const updateJobTypeFilter = () => { if (jobType.value && jobType.value !== ' ') { filters.value.type = jobType.value } else { delete filters.value.type } +} +const updateWorkModeFilter = () => { if (workMode.value && workMode.value !== ' ') { filters.value.work_mode = workMode.value } else { delete filters.value.work_mode } +} +const updateSearchQueryFilter = () => { if (searchQuery.value) { orFilters.value = { job_title: ['like', `%${searchQuery.value}%`], @@ -243,13 +264,17 @@ const updateFilters = () => { } else { orFilters.value = {} } +} +const updateCountryFilter = () => { if (country.value) { filters.value.country = country.value } else { delete filters.value.country } +} +const updateTabFilter = () => { if (activeTab.value === 'Closed') { filters.value.status = 'Closed' if (!isModerator.value) { @@ -269,8 +294,20 @@ watch(country, (val) => { updateJobs() }) -watch(jobs, () => { - jobCount.value = jobs.data?.length || 0 +const tabs = computed(() => { + const tabsArray = [ + { + label: __('Open'), + }, + ] + + if (closedJobs.value) { + tabsArray.push({ + label: __('Closed'), + }) + } + + return tabsArray }) const jobTypes = computed(() => { @@ -286,9 +323,9 @@ const jobTypes = computed(() => { const workModes = computed(() => { return [ { label: ' ', value: ' ' }, - { label: 'On site', value: 'On-site' }, - { label: 'Hybrid', value: 'Hybrid' }, - { label: 'Remote', value: 'Remote' }, + { label: __('On-site'), value: 'On-site' }, + { label: __('Hybrid'), value: 'Hybrid' }, + { label: __('Remote'), value: 'Remote' }, ] }) diff --git a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue index f9c43bf6..ecde7da9 100644 --- a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue +++ b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue @@ -34,8 +34,8 @@
-
-
+
+
{{ __('{0} Exercises').format(exercises.data?.length) }}
@@ -69,7 +69,7 @@ showForm = true }, }" - class="h-[79vh] border-b" + class="h-[79vh] px-5" >
- -
+
+ +
+
diff --git a/frontend/src/pages/Quizzes.vue b/frontend/src/pages/Quizzes.vue index 2935120b..8df31dd7 100644 --- a/frontend/src/pages/Quizzes.vue +++ b/frontend/src/pages/Quizzes.vue @@ -10,8 +10,8 @@ {{ __('Create') }} -
-
+
+
{{ __('{0} Quizzes').format(quizzes.data?.length) }}
@@ -27,7 +27,7 @@ :rows="quizzes.data" row-key="name" :options="{ showTooltip: false, selectable: true }" - class="h-[79vh] border-b" + class="h-[79vh] px-5" > - -
+
+ +
+
@@ -296,7 +298,7 @@ const quizColumns = computed(() => { { label: __('Show Answers'), key: 'show_answers', - width: 1, + width: 0.5, align: 'center', icon: 'eye', }, diff --git a/lms/lms/api.py b/lms/lms/api.py index bfbde0cf..c91ff9ad 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -232,14 +232,16 @@ def get_job_details(job: str): @frappe.whitelist(allow_guest=True) -def get_job_opportunities(filters: dict = None, orFilters: dict = None): +def get_job_opportunities( + filters: dict = None, or_filters: dict = None, start: int = 0, page_length: int = 40 +): if not filters: filters = {} jobs = frappe.get_all( "Job Opportunity", filters=filters, - or_filters=orFilters, + or_filters=or_filters, fields=[ "job_title", "location", @@ -252,6 +254,8 @@ def get_job_opportunities(filters: dict = None, orFilters: dict = None): "creation", "description", ], + start=start, + page_length=page_length, order_by="creation desc", ) @@ -348,7 +352,6 @@ def get_certified_participants(filters: dict = None, start: int = 0, page_length query = get_certification_query(filters) query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length) participants = query.run(as_dict=True) - print(participants) for participant in participants: details = get_certified_participant_details(participant.member) participant.update(details) From ec5e45e6c6cfa71c19c3b6144b383f11c125fa42 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Sat, 11 Apr 2026 19:50:59 +0530 Subject: [PATCH 5/7] fix: mobile view of lists --- frontend/src/pages/Assignments.vue | 8 +++++--- frontend/src/pages/CertifiedParticipants.vue | 6 +++--- frontend/src/pages/Jobs.vue | 4 ++-- frontend/src/pages/Notifications.vue | 4 ++-- .../pages/ProgrammingExercises/ProgrammingExercises.vue | 9 ++++++--- frontend/src/pages/Programs/Programs.vue | 4 ++-- frontend/src/pages/Quizzes.vue | 4 ++-- 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/Assignments.vue b/frontend/src/pages/Assignments.vue index 31f21c67..0ef6f4fa 100644 --- a/frontend/src/pages/Assignments.vue +++ b/frontend/src/pages/Assignments.vue @@ -21,7 +21,9 @@
-
+
{{ __('{0} Assignments').format(assignments.data?.length) }}
@@ -52,7 +54,7 @@ showAssignmentForm = true }, }" - class="h-[79vh] px-5" + class="h-[71vh] lg:h-[79vh] px-5" > { icon: 'tag', }, { - label: __('Modified'), + label: __('Updated On'), key: 'modified', width: 1, align: 'right', diff --git a/frontend/src/pages/CertifiedParticipants.vue b/frontend/src/pages/CertifiedParticipants.vue index 56b899cb..7a819734 100644 --- a/frontend/src/pages/CertifiedParticipants.vue +++ b/frontend/src/pages/CertifiedParticipants.vue @@ -58,9 +58,9 @@
-
+
-
+
diff --git a/frontend/src/pages/Jobs.vue b/frontend/src/pages/Jobs.vue index f373bcf5..7707fa5d 100644 --- a/frontend/src/pages/Jobs.vue +++ b/frontend/src/pages/Jobs.vue @@ -98,7 +98,7 @@
-
+
diff --git a/frontend/src/pages/Notifications.vue b/frontend/src/pages/Notifications.vue index 04ea50ba..874ee1dd 100644 --- a/frontend/src/pages/Notifications.vue +++ b/frontend/src/pages/Notifications.vue @@ -39,9 +39,9 @@ />
-
+
diff --git a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue index ecde7da9..0c92970a 100644 --- a/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue +++ b/frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue @@ -6,6 +6,7 @@