diff --git a/addons/dance_attendance/__manifest__.py b/addons/dance_attendance/__manifest__.py
index ee30ad2..343653a 100644
--- a/addons/dance_attendance/__manifest__.py
+++ b/addons/dance_attendance/__manifest__.py
@@ -14,4 +14,12 @@
],
"application": False,
"installable": True,
+ "assets": {
+ "web.assets_backend": [
+ "dance_attendance/static/src/sessions_page/sessions_page.js",
+ "dance_attendance/static/src/sessions_page/sessions_page.xml",
+ "dance_attendance/static/src/attendance_page/attendance_page.js",
+ "dance_attendance/static/src/attendance_page/attendance_page.xml",
+ ],
+ },
}
diff --git a/addons/dance_attendance/static/src/attendance_page/attendance_page.js b/addons/dance_attendance/static/src/attendance_page/attendance_page.js
new file mode 100644
index 0000000..e263c14
--- /dev/null
+++ b/addons/dance_attendance/static/src/attendance_page/attendance_page.js
@@ -0,0 +1,91 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceAttendancePage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ attendanceRecords: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadAttendance();
+ });
+ }
+
+ async loadAttendance() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|", "|",
+ ["student_id.name", "ilike", this.state.searchQuery],
+ ["batch_id.name", "ilike", this.state.searchQuery],
+ ["session_id.name", "ilike", this.state.searchQuery],
+ ["remarks", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.attendanceRecords = await this.orm.searchRead(
+ "dance.attendance",
+ domain,
+ ["date", "batch_id", "session_id", "student_id", "status", "remarks"],
+ { limit: 50, order: "date DESC" }
+ );
+ } catch (e) {
+ console.error("Error loading attendance:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadAttendance();
+ }
+
+ openAttendance(recordId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.attendance",
+ res_id: recordId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.client", name: "Leave Requests", tag: "dance_leave.action_leave_requests_dashboard" },
+ attendance: { type: "ir.actions.client", name: "Attendance", tag: "dance_attendance.action_attendance_dashboard" },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceAttendancePage.template = "dance_attendance.DanceAttendancePage";
+
+registry.category("actions").add("dance_attendance.action_attendance_dashboard", DanceAttendancePage);
diff --git a/addons/dance_attendance/static/src/attendance_page/attendance_page.xml b/addons/dance_attendance/static/src/attendance_page/attendance_page.xml
new file mode 100644
index 0000000..6491806
--- /dev/null
+++ b/addons/dance_attendance/static/src/attendance_page/attendance_page.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Attendance...
+
+
+
+
+
No Attendance Records Found
+
Try adjusting your search.
+
+
+
+
+
+
+ | Date |
+ Batch |
+ Session |
+ Student |
+ Status |
+ Remarks |
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_attendance/static/src/sessions_page/sessions_page.js b/addons/dance_attendance/static/src/sessions_page/sessions_page.js
new file mode 100644
index 0000000..e243398
--- /dev/null
+++ b/addons/dance_attendance/static/src/sessions_page/sessions_page.js
@@ -0,0 +1,108 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceSessionsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ sessions: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadSessions();
+ });
+ }
+
+ async loadSessions() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["batch_id.name", "ilike", this.state.searchQuery],
+ ["instructor_id.name", "ilike", this.state.searchQuery],
+ ["room", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.sessions = await this.orm.searchRead(
+ "dance.session",
+ domain,
+ ["date", "batch_id", "start_time", "end_time", "room", "instructor_id", "attendance_count", "present_count", "absent_count", "late_count", "status"],
+ { limit: 50, order: "date DESC, start_time DESC" }
+ );
+ } catch (e) {
+ console.error("Error loading sessions:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadSessions();
+ }
+
+ openSession(sessionId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.session",
+ res_id: sessionId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createSession() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.session",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ formatTime(floatTime) {
+ if (typeof floatTime !== "number") return "-";
+ const hours = Math.floor(floatTime);
+ const minutes = Math.round((floatTime - hours) * 60);
+ const paddedHours = String(hours).padStart(2, "0");
+ const paddedMinutes = String(minutes).padStart(2, "0");
+ return `${paddedHours}:${paddedMinutes}`;
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceSessionsPage.template = "dance_attendance.DanceSessionsPage";
+
+registry.category("actions").add("dance_attendance.action_sessions_dashboard", DanceSessionsPage);
diff --git a/addons/dance_attendance/static/src/sessions_page/sessions_page.xml b/addons/dance_attendance/static/src/sessions_page/sessions_page.xml
new file mode 100644
index 0000000..e89906b
--- /dev/null
+++ b/addons/dance_attendance/static/src/sessions_page/sessions_page.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Sessions...
+
+
+
+
+
No Sessions Found
+
Try adjusting your search or create a new session.
+
+
+
+
+
+
+
+ | Date |
+ Batch |
+ Start Time |
+ End Time |
+ Room |
+ Instructor |
+ Attendance Count |
+ Present Count |
+ Absent Count |
+ Late Count |
+ Status |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_attendance/views/dance_attendance_views.xml b/addons/dance_attendance/views/dance_attendance_views.xml
index e85fcce..aba93fc 100644
--- a/addons/dance_attendance/views/dance_attendance_views.xml
+++ b/addons/dance_attendance/views/dance_attendance_views.xml
@@ -61,6 +61,11 @@
+
+ Attendance Dashboard
+ dance_attendance.action_attendance_dashboard
+
+
Attendance
dance.attendance
@@ -68,5 +73,5 @@
-
+
diff --git a/addons/dance_attendance/views/dance_session_views.xml b/addons/dance_attendance/views/dance_session_views.xml
index 7ef3fad..d14dccb 100644
--- a/addons/dance_attendance/views/dance_session_views.xml
+++ b/addons/dance_attendance/views/dance_session_views.xml
@@ -115,6 +115,11 @@
+
+ Sessions Dashboard
+ dance_attendance.action_sessions_dashboard
+
+
Sessions
dance.session
@@ -122,5 +127,5 @@
-
+
diff --git a/addons/dance_batch/__manifest__.py b/addons/dance_batch/__manifest__.py
index bcd66ec..0b95cee 100644
--- a/addons/dance_batch/__manifest__.py
+++ b/addons/dance_batch/__manifest__.py
@@ -12,6 +12,14 @@
"views/dance_batch_views.xml",
"views/dance_enrollment_views.xml",
],
+ "assets": {
+ "web.assets_backend": [
+ "dance_batch/static/src/batches_page/batches_page.js",
+ "dance_batch/static/src/batches_page/batches_page.xml",
+ "dance_batch/static/src/enrollments_page/enrollments_page.js",
+ "dance_batch/static/src/enrollments_page/enrollments_page.xml"
+ ]
+ },
"application": False,
"installable": True,
}
diff --git a/addons/dance_batch/static/src/batches_page/batches_page.js b/addons/dance_batch/static/src/batches_page/batches_page.js
new file mode 100644
index 0000000..dca949e
--- /dev/null
+++ b/addons/dance_batch/static/src/batches_page/batches_page.js
@@ -0,0 +1,98 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceBatchesPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ batches: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadBatches();
+ });
+ }
+
+ async loadBatches() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["name", "ilike", this.state.searchQuery],
+ ["dance_style", "ilike", this.state.searchQuery],
+ ["instructor_id.name", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.batches = await this.orm.searchRead(
+ "dance.batch",
+ domain,
+ ["name", "dance_style", "level", "instructor_id", "schedule_days", "room", "active_enrollment_count", "capacity", "status"],
+ { limit: 50 }
+ );
+ } catch (e) {
+ console.error("Error loading batches:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadBatches();
+ }
+
+ openBatch(batchId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.batch",
+ res_id: batchId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createBatch() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.batch",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.act_window", name: "Events & Recitals", res_model: "dance.event", views: [[false, "list"], [false, "form"]] },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.act_window", name: "Performance", res_model: "dance.performance.record", views: [[false, "list"], [false, "form"]] }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceBatchesPage.template = "dance_batch.DanceBatchesPage";
+
+registry.category("actions").add("dance_batch.action_batches_dashboard", DanceBatchesPage);
diff --git a/addons/dance_batch/static/src/batches_page/batches_page.xml b/addons/dance_batch/static/src/batches_page/batches_page.xml
new file mode 100644
index 0000000..b16b779
--- /dev/null
+++ b/addons/dance_batch/static/src/batches_page/batches_page.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No Batches Found
+
Try adjusting your search or create a new batch.
+
+
+
+
+
+
+
+ | Name |
+ Dance Style |
+ Level |
+ Instructor |
+ Schedule Days |
+ Room |
+ Current Strength |
+ Capacity |
+ Status |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_batch/static/src/enrollments_page/enrollments_page.js b/addons/dance_batch/static/src/enrollments_page/enrollments_page.js
new file mode 100644
index 0000000..5a4cbb9
--- /dev/null
+++ b/addons/dance_batch/static/src/enrollments_page/enrollments_page.js
@@ -0,0 +1,108 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceEnrollmentsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ enrollments: [],
+ loading: false,
+ searchQuery: "",
+ hasInvoiceCount: false
+ });
+
+ onWillStart(async () => {
+ await this.loadEnrollments();
+ });
+ }
+
+ async loadEnrollments() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["student_id.name", "ilike", this.state.searchQuery],
+ ["batch_id.name", "ilike", this.state.searchQuery],
+ ["name", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ // Check if invoice_count field exists on the model
+ const fieldsInfo = await this.orm.call("dance.enrollment", "fields_get", [], { attributes: ["string"] });
+ this.state.hasInvoiceCount = !!fieldsInfo.invoice_count;
+
+ const fieldsToFetch = ["student_id", "batch_id", "enrollment_date", "status"];
+ if (this.state.hasInvoiceCount) {
+ fieldsToFetch.push("invoice_count");
+ }
+
+ this.state.enrollments = await this.orm.searchRead(
+ "dance.enrollment",
+ domain,
+ fieldsToFetch,
+ { limit: 50 }
+ );
+ } catch (e) {
+ console.error("Error loading enrollments:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadEnrollments();
+ }
+
+ openEnrollment(enrollmentId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.enrollment",
+ res_id: enrollmentId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createEnrollment() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.enrollment",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.act_window", name: "Events & Recitals", res_model: "dance.event", views: [[false, "list"], [false, "form"]] },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.act_window", name: "Performance", res_model: "dance.performance.record", views: [[false, "list"], [false, "form"]] }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceEnrollmentsPage.template = "dance_batch.DanceEnrollmentsPage";
+
+registry.category("actions").add("dance_batch.action_enrollments_dashboard", DanceEnrollmentsPage);
diff --git a/addons/dance_batch/static/src/enrollments_page/enrollments_page.xml b/addons/dance_batch/static/src/enrollments_page/enrollments_page.xml
new file mode 100644
index 0000000..be3924d
--- /dev/null
+++ b/addons/dance_batch/static/src/enrollments_page/enrollments_page.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Enrollments...
+
+
+
+
+
No Enrollments Found
+
Try adjusting your search or create a new enrollment.
+
+
+
+
+
+
+
+ | Student |
+ Batch |
+ Enrollment Date |
+ Status |
+ Invoice Count |
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_batch/views/dance_batch_views.xml b/addons/dance_batch/views/dance_batch_views.xml
index da98281..b52df56 100644
--- a/addons/dance_batch/views/dance_batch_views.xml
+++ b/addons/dance_batch/views/dance_batch_views.xml
@@ -95,6 +95,11 @@
+
+ Batches Dashboard
+ dance_batch.action_batches_dashboard
+
+
Batches
dance.batch
@@ -102,5 +107,5 @@
-
+
diff --git a/addons/dance_batch/views/dance_enrollment_views.xml b/addons/dance_batch/views/dance_enrollment_views.xml
index 03e5e7e..e4d7a13 100644
--- a/addons/dance_batch/views/dance_enrollment_views.xml
+++ b/addons/dance_batch/views/dance_enrollment_views.xml
@@ -56,6 +56,11 @@
+
+ Enrollments Dashboard
+ dance_batch.action_enrollments_dashboard
+
+
Enrollments
dance.enrollment
@@ -63,5 +68,5 @@
-
+
diff --git a/addons/dance_event/__manifest__.py b/addons/dance_event/__manifest__.py
index 76f57e3..1393052 100644
--- a/addons/dance_event/__manifest__.py
+++ b/addons/dance_event/__manifest__.py
@@ -15,4 +15,14 @@
],
"application": False,
"installable": True,
+ "assets": {
+ "web.assets_backend": [
+ "dance_event/static/src/events_page/events_page.js",
+ "dance_event/static/src/events_page/events_page.xml",
+ "dance_event/static/src/performances_page/performances_page.js",
+ "dance_event/static/src/performances_page/performances_page.xml",
+ "dance_event/static/src/rehearsals_page/rehearsals_page.js",
+ "dance_event/static/src/rehearsals_page/rehearsals_page.xml",
+ ],
+ },
}
diff --git a/addons/dance_event/static/src/events_page/events_page.js b/addons/dance_event/static/src/events_page/events_page.js
new file mode 100644
index 0000000..e746810
--- /dev/null
+++ b/addons/dance_event/static/src/events_page/events_page.js
@@ -0,0 +1,98 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceEventsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ events: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadEvents();
+ });
+ }
+
+ async loadEvents() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["name", "ilike", this.state.searchQuery],
+ ["venue", "ilike", this.state.searchQuery],
+ ["event_type", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.events = await this.orm.searchRead(
+ "dance.event",
+ domain,
+ ["name", "event_type", "date", "venue", "participant_count", "performance_count", "rehearsal_count", "state"],
+ { limit: 50 }
+ );
+ } catch (e) {
+ console.error("Error loading events:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadEvents();
+ }
+
+ openEvent(eventId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.event",
+ res_id: eventId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createEvent() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.event",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceEventsPage.template = "dance_event.DanceEventsPage";
+
+registry.category("actions").add("dance_event.action_events_dashboard", DanceEventsPage);
diff --git a/addons/dance_event/static/src/events_page/events_page.xml b/addons/dance_event/static/src/events_page/events_page.xml
new file mode 100644
index 0000000..4229b6b
--- /dev/null
+++ b/addons/dance_event/static/src/events_page/events_page.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No Events Found
+
Try adjusting your search or create a new event.
+
+
+
+
+
+
+
+ | Event Name |
+ Type |
+ Date |
+ Venue |
+ Participant Count |
+ Performance Count |
+ Rehearsal Count |
+ State |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_event/static/src/performances_page/performances_page.js b/addons/dance_event/static/src/performances_page/performances_page.js
new file mode 100644
index 0000000..032128c
--- /dev/null
+++ b/addons/dance_event/static/src/performances_page/performances_page.js
@@ -0,0 +1,99 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DancePerformancesPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ performances: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadPerformances();
+ });
+ }
+
+ async loadPerformances() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["event_id.name", "ilike", this.state.searchQuery],
+ ["student_id.name", "ilike", this.state.searchQuery],
+ ["costume_needed", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.performances = await this.orm.searchRead(
+ "dance.event.performance",
+ domain,
+ ["event_id", "student_id", "role", "costume_needed", "costume_status"],
+ { limit: 50 }
+ );
+ } catch (e) {
+ console.error("Error loading performances:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadPerformances();
+ }
+
+ openPerformance(perfId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.event.performance",
+ res_id: perfId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createPerformance() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.event.performance",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DancePerformancesPage.template = "dance_event.DancePerformancesPage";
+
+registry.category("actions").add("dance_event.action_performances_dashboard", DancePerformancesPage);
diff --git a/addons/dance_event/static/src/performances_page/performances_page.xml b/addons/dance_event/static/src/performances_page/performances_page.xml
new file mode 100644
index 0000000..fd5086e
--- /dev/null
+++ b/addons/dance_event/static/src/performances_page/performances_page.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
Performances & Costumes
+
+
+
+
+
+
+
Loading Performances...
+
+
+
+
+
No Performances Found
+
Try adjusting your search or create a new performance.
+
+
+
+
+
+
+
+ | Event |
+ Student |
+ Role |
+ Costume Needed |
+ Costume Status |
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_event/static/src/rehearsals_page/rehearsals_page.js b/addons/dance_event/static/src/rehearsals_page/rehearsals_page.js
new file mode 100644
index 0000000..287740b
--- /dev/null
+++ b/addons/dance_event/static/src/rehearsals_page/rehearsals_page.js
@@ -0,0 +1,99 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceRehearsalsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ rehearsals: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadRehearsals();
+ });
+ }
+
+ async loadRehearsals() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["name", "ilike", this.state.searchQuery],
+ ["event_id.name", "ilike", this.state.searchQuery],
+ ["room", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.rehearsals = await this.orm.searchRead(
+ "dance.rehearsal",
+ domain,
+ ["event_id", "date", "display_time", "room", "attendance_count", "present_count", "absent_count", "status"],
+ { limit: 50 }
+ );
+ } catch (e) {
+ console.error("Error loading rehearsals:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadRehearsals();
+ }
+
+ openRehearsal(rehearsalId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.rehearsal",
+ res_id: rehearsalId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createRehearsal() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.rehearsal",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceRehearsalsPage.template = "dance_event.DanceRehearsalsPage";
+
+registry.category("actions").add("dance_event.action_rehearsals_dashboard", DanceRehearsalsPage);
diff --git a/addons/dance_event/static/src/rehearsals_page/rehearsals_page.xml b/addons/dance_event/static/src/rehearsals_page/rehearsals_page.xml
new file mode 100644
index 0000000..b28db1d
--- /dev/null
+++ b/addons/dance_event/static/src/rehearsals_page/rehearsals_page.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Rehearsals...
+
+
+
+
+
No Rehearsals Found
+
Try adjusting your search or schedule a new rehearsal.
+
+
+
+
+
+
+
+ | Event |
+ Date |
+ Display Time |
+ Room |
+ Attendance Count |
+ Present Count |
+ Absent Count |
+ Status |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_event/views/dance_event_performance_views.xml b/addons/dance_event/views/dance_event_performance_views.xml
index d8c2041..a1fabb1 100644
--- a/addons/dance_event/views/dance_event_performance_views.xml
+++ b/addons/dance_event/views/dance_event_performance_views.xml
@@ -60,6 +60,11 @@
+
+ Performances Dashboard
+ dance_event.action_performances_dashboard
+
+
Performances & Costumes
dance.event.performance
@@ -67,5 +72,5 @@
-
+
diff --git a/addons/dance_event/views/dance_event_views.xml b/addons/dance_event/views/dance_event_views.xml
index 3a5a6c1..7869f67 100644
--- a/addons/dance_event/views/dance_event_views.xml
+++ b/addons/dance_event/views/dance_event_views.xml
@@ -108,6 +108,11 @@
+
+ Events Dashboard
+ dance_event.action_events_dashboard
+
+
Events & Recitals
dance.event
@@ -115,6 +120,5 @@
-
-
+
diff --git a/addons/dance_event/views/dance_rehearsal_views.xml b/addons/dance_event/views/dance_rehearsal_views.xml
index 3c0d96a..93d3296 100644
--- a/addons/dance_event/views/dance_rehearsal_views.xml
+++ b/addons/dance_event/views/dance_rehearsal_views.xml
@@ -89,6 +89,11 @@
+
+ Rehearsals Dashboard
+ dance_event.action_rehearsals_dashboard
+
+
Rehearsals
dance.rehearsal
@@ -96,5 +101,5 @@
-
+
diff --git a/addons/dance_guardian/__manifest__.py b/addons/dance_guardian/__manifest__.py
index 275fdaf..2cdda24 100644
--- a/addons/dance_guardian/__manifest__.py
+++ b/addons/dance_guardian/__manifest__.py
@@ -1,7 +1,7 @@
{
"name": "Dance Guardian Management",
"summary": "Guardian contacts and student linking for dance school portal",
- "version": "17.0.1.0.0",
+ "version": "17.0.1.0.3",
"category": "Education",
"author": "Metetroncube",
"license": "LGPL-3",
@@ -9,6 +9,12 @@
"data": [
"views/res_partner_guardian_views.xml",
],
+ "assets": {
+ "web.assets_backend": [
+ "dance_guardian/static/src/guardians_page/guardians_page.js",
+ "dance_guardian/static/src/guardians_page/guardians_page.xml"
+ ]
+ },
"application": False,
"installable": True,
}
diff --git a/addons/dance_guardian/static/src/guardians_page/guardians_page.js b/addons/dance_guardian/static/src/guardians_page/guardians_page.js
new file mode 100644
index 0000000..86da138
--- /dev/null
+++ b/addons/dance_guardian/static/src/guardians_page/guardians_page.js
@@ -0,0 +1,102 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Component, useState, onWillStart } from "@odoo/owl";
+
+export class DanceGuardiansPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ guardians: [],
+ loading: true,
+ searchQuery: "",
+ });
+
+ onWillStart(async () => {
+ await this.loadGuardians();
+ });
+ }
+
+ async loadGuardians() {
+ this.state.loading = true;
+ let domain = [['is_guardian', '=', true], ['is_student', '=', false]];
+ if (this.state.searchQuery) {
+ domain.push(['name', 'ilike', this.state.searchQuery]);
+ }
+
+ this.state.guardians = await this.orm.searchRead(
+ "res.partner",
+ domain,
+ ["id", "name", "phone", "mobile", "email", "guardian_student_ids"]
+ );
+ this.state.loading = false;
+ }
+
+ onSearch(ev) {
+ if (ev.key === "Enter" || ev.type === "click") {
+ this.loadGuardians();
+ }
+ }
+
+ async openGuardian(id) {
+ let viewId = false;
+ try {
+ viewId = await this.orm.call("ir.model.data", "_xmlid_to_res_id", ["dance_guardian.view_dance_guardian_form"]);
+ } catch (e) {
+ console.warn("Could not load custom guardian form view ID", e);
+ }
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'res.partner',
+ res_id: id,
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ });
+ }
+
+ async createGuardian() {
+ let viewId = false;
+ try {
+ viewId = await this.orm.call("ir.model.data", "_xmlid_to_res_id", ["dance_guardian.view_dance_guardian_form"]);
+ } catch (e) {
+ console.warn("Could not load custom guardian form view ID", e);
+ }
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'res.partner',
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ context: {
+ default_is_guardian: true,
+ default_is_student: false
+ }
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.act_window", name: "Enrollment Requests", res_model: "dance.enrollment.request", views: [[false, "list"], [false, "form"]] },
+ sessions: { type: "ir.actions.act_window", name: "Sessions", res_model: "dance.session", views: [[false, "list"], [false, "form"]] },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceGuardiansPage.template = "dance_guardian.DanceGuardiansPage";
+registry.category("actions").add("dance_guardian.action_guardians_dashboard", DanceGuardiansPage);
diff --git a/addons/dance_guardian/static/src/guardians_page/guardians_page.xml b/addons/dance_guardian/static/src/guardians_page/guardians_page.xml
new file mode 100644
index 0000000..3f8174f
--- /dev/null
+++ b/addons/dance_guardian/static/src/guardians_page/guardians_page.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Guardians Management
+
+
+
+
+
+
+
Loading Guardians...
+
+
+
+
+
No Guardians Found
+
Try adjusting your search or create a new guardian.
+
+
+
+
+
+
+
+
+ | Guardian Name |
+ Phone |
+ Mobile |
+ Email |
+ Linked Students |
+
+
+
+
+ |
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+ Student(s)
+
+
+ None
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_guardian/views/res_partner_guardian_views.xml b/addons/dance_guardian/views/res_partner_guardian_views.xml
index 2b41cf3..b462f43 100644
--- a/addons/dance_guardian/views/res_partner_guardian_views.xml
+++ b/addons/dance_guardian/views/res_partner_guardian_views.xml
@@ -51,8 +51,88 @@
-
+
Guardians
+ dance_guardian.action_guardians_dashboard
+
+
+
+
+ dance.guardian.form
+ res.partner
+
+
+
+
+
+
+
+ Guardians List
res.partner
tree,form
[('is_guardian', '=', True), ('is_student', '=', False)]
@@ -60,7 +140,7 @@
+ (0, 0, {'view_mode': 'form', 'view_id': ref('view_dance_guardian_form')})]"/>
@@ -79,5 +159,6 @@
-
+
+
diff --git a/addons/dance_leave/__manifest__.py b/addons/dance_leave/__manifest__.py
index f5ee9d1..ae89a0b 100644
--- a/addons/dance_leave/__manifest__.py
+++ b/addons/dance_leave/__manifest__.py
@@ -13,4 +13,10 @@
],
"application": False,
"installable": True,
+ "assets": {
+ "web.assets_backend": [
+ "dance_leave/static/src/leave_requests_page/leave_requests_page.js",
+ "dance_leave/static/src/leave_requests_page/leave_requests_page.xml",
+ ],
+ },
}
diff --git a/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.js b/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.js
new file mode 100644
index 0000000..0af666a
--- /dev/null
+++ b/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.js
@@ -0,0 +1,99 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceLeaveRequestsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ leaveRequests: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadLeaveRequests();
+ });
+ }
+
+ async loadLeaveRequests() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|",
+ ["student_id.name", "ilike", this.state.searchQuery],
+ ["guardian_id.name", "ilike", this.state.searchQuery],
+ ["status", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.leaveRequests = await this.orm.searchRead(
+ "dance.leave.request",
+ domain,
+ ["student_id", "guardian_id", "from_date", "to_date", "status", "reviewed_by_id"],
+ { limit: 50, order: "from_date DESC" }
+ );
+ } catch (e) {
+ console.error("Error loading leave requests:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadLeaveRequests();
+ }
+
+ openLeaveRequest(requestId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.leave.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createLeaveRequest() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.leave.request",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.client", name: "Leave Requests", tag: "dance_leave.action_leave_requests_dashboard" },
+ attendance: { type: "ir.actions.client", name: "Attendance", tag: "dance_attendance.action_attendance_dashboard" },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceLeaveRequestsPage.template = "dance_leave.DanceLeaveRequestsPage";
+
+registry.category("actions").add("dance_leave.action_leave_requests_dashboard", DanceLeaveRequestsPage);
diff --git a/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.xml b/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.xml
new file mode 100644
index 0000000..99127fa
--- /dev/null
+++ b/addons/dance_leave/static/src/leave_requests_page/leave_requests_page.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Leave Requests...
+
+
+
+
+
No Leave Requests Found
+
Try adjusting your search or create a new request.
+
+
+
+
+
+
+
+ | Student |
+ Guardian |
+ From Date |
+ To Date |
+ Status |
+ Reviewed By |
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_leave/views/dance_leave_request_views.xml b/addons/dance_leave/views/dance_leave_request_views.xml
index 979c431..5db3cde 100644
--- a/addons/dance_leave/views/dance_leave_request_views.xml
+++ b/addons/dance_leave/views/dance_leave_request_views.xml
@@ -75,6 +75,11 @@
+
+ Leave Requests Dashboard
+ dance_leave.action_leave_requests_dashboard
+
+
Leave Requests
dance.leave.request
@@ -82,5 +87,5 @@
-
+
diff --git a/addons/dance_leave/views/dance_portal_leave_templates.xml b/addons/dance_leave/views/dance_portal_leave_templates.xml
index 0583692..f1eae4a 100644
--- a/addons/dance_leave/views/dance_portal_leave_templates.xml
+++ b/addons/dance_leave/views/dance_portal_leave_templates.xml
@@ -5,33 +5,43 @@
Leave request submitted for review.
-
-
Request Leave
+
-
- Leave Requests
-
-
-
- | From |
- To |
- Reason |
- Status |
-
-
-
-
- | No leave requests found. |
-
-
- |
- |
- |
- |
-
-
-
+
+ Leave Requests
+
+
+
+
+
+ | From |
+ To |
+ Reason |
+ Status |
+
+
+
+
+ | No leave requests found. |
+
+
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
@@ -42,36 +52,44 @@
Request Leave
-
-
-
+
+
-
-
-
-
-
-
- SEP
- 12
-
-
-
-
-
-
Inter Academy Dance Competition
-
-
-
Madurai Convention Hall
+
+
+
+
+
+
+
-
Registration Open
-
-
-
-
-
-
-
-
-
-
-
-
- OCT
- 05
-
-
-
-
-
-
Bharatanatyam Arangetram
-
-
-
Nritya Main Stage
+
+
+
+
+
+
+
+
+
+
+
+
+
-
Limited Seats
-
-
-
-
-
-
-
-
-
-
-
-
- DEC
- 18
-
-
-
-
-
-
Winter Cultural Festival
-
-
-
Open Air Theatre
+
+
+
+
+
+
+
Planning
+
Rehearsals
+
Completed
-
Family Event
+
+
+
-
-
-
-
-
-
-
-
-
-
- JAN
- 10
-
-
-
-
-
-
New Year Dance Gala
-
-
- Chennai Auditorium
-
-
All Dance Styles
-
-
-
-
-
-
+
diff --git a/addons/dance_performance/__manifest__.py b/addons/dance_performance/__manifest__.py
index 2171fcb..0829401 100644
--- a/addons/dance_performance/__manifest__.py
+++ b/addons/dance_performance/__manifest__.py
@@ -14,4 +14,12 @@
],
"application": False,
"installable": True,
+ "assets": {
+ "web.assets_backend": [
+ "dance_performance/static/src/grade_cards_page/grade_cards_page.js",
+ "dance_performance/static/src/grade_cards_page/grade_cards_page.xml",
+ "dance_performance/static/src/performance_criteria_page/performance_criteria_page.js",
+ "dance_performance/static/src/performance_criteria_page/performance_criteria_page.xml",
+ ],
+ },
}
diff --git a/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.js b/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.js
new file mode 100644
index 0000000..14b3517
--- /dev/null
+++ b/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.js
@@ -0,0 +1,102 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceGradeCardsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ gradeCards: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadGradeCards();
+ });
+ }
+
+ async loadGradeCards() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [
+ "|", "|", "|",
+ ["student_id.name", "ilike", this.state.searchQuery],
+ ["batch_id.name", "ilike", this.state.searchQuery],
+ ["instructor_id.name", "ilike", this.state.searchQuery],
+ ["grade", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.gradeCards = await this.orm.searchRead(
+ "dance.performance.record",
+ domain,
+ ["assessment_date", "student_id", "batch_id", "instructor_id", "total_score", "total_max_score", "percentage", "grade", "state"],
+ { limit: 50, order: "assessment_date DESC" }
+ );
+ } catch (e) {
+ console.error("Error loading grade cards:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadGradeCards();
+ }
+
+ openGradeCard(recordId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.performance.record",
+ res_id: recordId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createGradeCard() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.performance.record",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.client", name: "Leave Requests", tag: "dance_leave.action_leave_requests_dashboard" },
+ attendance: { type: "ir.actions.client", name: "Attendance", tag: "dance_attendance.action_attendance_dashboard" },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" },
+ grade_cards: { type: "ir.actions.client", name: "Grade Cards", tag: "dance_performance.action_grade_cards_dashboard" },
+ performance_criteria: { type: "ir.actions.client", name: "Performance Criteria", tag: "dance_performance.action_performance_criteria_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceGradeCardsPage.template = "dance_performance.DanceGradeCardsPage";
+
+registry.category("actions").add("dance_performance.action_grade_cards_dashboard", DanceGradeCardsPage);
diff --git a/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.xml b/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.xml
new file mode 100644
index 0000000..062ee0d
--- /dev/null
+++ b/addons/dance_performance/static/src/grade_cards_page/grade_cards_page.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Grade Cards...
+
+
+
+
+
No Grade Cards Found
+
Try adjusting your search or create a new grade card.
+
+
+
+
+
+
+
+ | Assessment Date |
+ Student |
+ Batch |
+ Instructor |
+ Total Score |
+ Total Max Score |
+ Percentage |
+ Grade |
+ State |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.js b/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.js
new file mode 100644
index 0000000..e523234
--- /dev/null
+++ b/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.js
@@ -0,0 +1,96 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DancePerformanceCriteriaPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ criteria: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadCriteria();
+ });
+ }
+
+ async loadCriteria() {
+ this.state.loading = true;
+ try {
+ let domain = [];
+ if (this.state.searchQuery) {
+ domain = [["name", "ilike", this.state.searchQuery]];
+ }
+
+ this.state.criteria = await this.orm.searchRead(
+ "dance.performance.criterion",
+ domain,
+ ["sequence", "name", "max_score", "active"],
+ { limit: 50, order: "sequence ASC" }
+ );
+ } catch (e) {
+ console.error("Error loading criteria:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadCriteria();
+ }
+
+ openCriterion(recordId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.performance.criterion",
+ res_id: recordId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ createCriterion() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.performance.criterion",
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.client", name: "Leave Requests", tag: "dance_leave.action_leave_requests_dashboard" },
+ attendance: { type: "ir.actions.client", name: "Attendance", tag: "dance_attendance.action_attendance_dashboard" },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" },
+ grade_cards: { type: "ir.actions.client", name: "Grade Cards", tag: "dance_performance.action_grade_cards_dashboard" },
+ performance_criteria: { type: "ir.actions.client", name: "Performance Criteria", tag: "dance_performance.action_performance_criteria_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DancePerformanceCriteriaPage.template = "dance_performance.DancePerformanceCriteriaPage";
+
+registry.category("actions").add("dance_performance.action_performance_criteria_dashboard", DancePerformanceCriteriaPage);
diff --git a/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.xml b/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.xml
new file mode 100644
index 0000000..802b28a
--- /dev/null
+++ b/addons/dance_performance/static/src/performance_criteria_page/performance_criteria_page.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
Performance Criteria
+
+
+
+
+
+
+
Loading Criteria...
+
+
+
+
+
No Performance Criteria Found
+
Try adjusting your search or create a new criterion.
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_performance/views/dance_performance_views.xml b/addons/dance_performance/views/dance_performance_views.xml
index 6fff2e4..3cdbcc7 100644
--- a/addons/dance_performance/views/dance_performance_views.xml
+++ b/addons/dance_performance/views/dance_performance_views.xml
@@ -134,6 +134,16 @@
+
+ Grade Cards Dashboard
+ dance_performance.action_grade_cards_dashboard
+
+
+
+ Performance Criteria Dashboard
+ dance_performance.action_performance_criteria_dashboard
+
+
Grade Cards
dance.performance.record
@@ -149,6 +159,6 @@
-
-
+
+
diff --git a/addons/dance_performance/views/dance_portal_templates.xml b/addons/dance_performance/views/dance_portal_templates.xml
index 8bb3105..27c8b5c 100644
--- a/addons/dance_performance/views/dance_portal_templates.xml
+++ b/addons/dance_performance/views/dance_portal_templates.xml
@@ -1,35 +1,43 @@
-
- Performance
-
-
-
- | Date |
- Batch |
- Score |
- Percentage |
- Grade |
-
-
-
-
- | No shared grade cards found. |
-
-
- |
- |
-
- /
- |
-
- %
- |
- |
-
-
-
+
+ Performance
+
+
+
+
+
+ | Date |
+ Batch |
+ Score |
+ Percentage |
+ Grade |
+
+
+
+
+ | No shared grade cards found. |
+
+
+ |
+ |
+
+ /
+ |
+
+ %
+ |
+
+
+
+
+ |
+
+
+
+
+
diff --git a/addons/dance_portal/controllers/__pycache__/portal.cpython-310.pyc b/addons/dance_portal/controllers/__pycache__/portal.cpython-310.pyc
index d441eb8..35737e9 100644
Binary files a/addons/dance_portal/controllers/__pycache__/portal.cpython-310.pyc and b/addons/dance_portal/controllers/__pycache__/portal.cpython-310.pyc differ
diff --git a/addons/dance_portal/controllers/portal.py b/addons/dance_portal/controllers/portal.py
index 2b90fe9..2b2d4bd 100644
--- a/addons/dance_portal/controllers/portal.py
+++ b/addons/dance_portal/controllers/portal.py
@@ -19,7 +19,7 @@ class DanceCustomerPortal(CustomerPortal):
("is_student", "=", True),
"|",
("guardian_id", "=", partner.id),
- ("guardian_ids", "in", partner.id),
+ ("guardian_ids", "in", [partner.id]),
]
def _get_portal_student(self, student_id):
diff --git a/addons/dance_portal/views/dance_portal_templates.xml b/addons/dance_portal/views/dance_portal_templates.xml
index 6b939c5..bae801b 100644
--- a/addons/dance_portal/views/dance_portal_templates.xml
+++ b/addons/dance_portal/views/dance_portal_templates.xml
@@ -1,5 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -34,41 +279,59 @@
My Students
-
- No students are linked to your portal account yet.
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
My Students
+
+
+ No students are linked to your portal account yet.
+
+
+
@@ -81,204 +344,284 @@
-
-
-
-
-
Student
-
- - Code
-
- - Status
-
- - Style
-
- - Level
-
-
-
-
-
-
-
-
-
Attendance
-
-
%
+
+
+
+
Student Dashboard
+
+
+
+
-
-
-
-
Fees
-
-
+
+
+
+
+
-
student invoices
+
%
+
+
+
+
+ Session(s)
+ Absence(s)
+
+
+
+
+
+
+
+
+
+
+ Student invoices pending or generated.
+
+
+
Enrolled Batches
+
+
+
+
+
+ | Batch |
+ Status |
+ Enrollment Date |
+
+
+
+
+ | No enrollments found. |
+
+
+ |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
Attendance Details
+
+
+
+
+
+ | Date |
+ Batch |
+ Session |
+ Status |
+
+
+
+
+ | No attendance records found. |
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
Upcoming Sessions
+
+
+
+
+
+ | Date |
+ Time |
+ Batch |
+ Room |
+ Status |
+
+
+
+
+ | No upcoming sessions found. |
+
+
+ |
+
+
+ -
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
Upcoming Events
+
+
+
+
+
+ | Event |
+ Date |
+ Venue |
+ Role |
+
+
+
+
+ | No upcoming events found. |
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
Upcoming Rehearsals
+
+
+
+
+
+ | Date |
+ Time |
+ Event |
+ Room |
+ Status |
+
+
+
+
+ | No upcoming rehearsals found. |
+
+
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
Fees / Invoices
+
+
+
+
+
+ | Invoice |
+ Date |
+ Batch |
+ Amount |
+ Due |
+ Status |
+ |
+
+
+
+
+ | No invoices found. |
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+ Open
+
+ |
+
+
+
+
+
+
-
-
Batches
-
-
-
- | Batch |
- Status |
- Enrollment Date |
-
-
-
-
- | No enrollments found. |
-
-
- |
- |
- |
-
-
-
-
-
Attendance
-
-
-
- | Date |
- Batch |
- Session |
- Status |
-
-
-
-
- | No attendance records found. |
-
-
- |
- |
- |
- |
-
-
-
-
-
Upcoming Sessions
-
-
-
- | Date |
- Time |
- Batch |
- Room |
- Status |
-
-
-
-
- | No upcoming sessions found. |
-
-
- |
-
-
- -
- |
- |
- |
- |
-
-
-
-
-
Upcoming Events
-
-
-
- | Event |
- Date |
- Venue |
- Role |
-
-
-
-
- | No upcoming events found. |
-
-
- |
- |
- |
- |
-
-
-
-
-
Upcoming Rehearsals
-
-
-
- | Date |
- Time |
- Event |
- Room |
- Status |
-
-
-
-
- | No upcoming rehearsals found. |
-
-
- |
- |
- |
- |
- |
-
-
-
-
-
Fees
-
-
-
- | Invoice |
- Date |
- Batch |
- Amount |
- Due |
- Status |
- |
-
-
-
-
- | No invoices found. |
-
-
- |
- |
- |
-
-
- |
-
-
- |
- |
-
- Open
- |
-
-
-
diff --git a/addons/dance_reports/static/src/dance_global.scss b/addons/dance_reports/static/src/dance_global.scss
index f8405aa..9e4cd2d 100644
--- a/addons/dance_reports/static/src/dance_global.scss
+++ b/addons/dance_reports/static/src/dance_global.scss
@@ -3,10 +3,11 @@
// Brand: #875A7B (mauve-purple) | accent: #5e3d57
// ── Top nav bar ────────────────────────────────────────────────────────────
+.o_navbar,
.o_main_navbar {
- background: linear-gradient(135deg, #875A7B 0%, #5e3d57 100%) !important;
- border-bottom: none !important;
- box-shadow: 0 2px 12px rgba(135, 90, 123, 0.3) !important;
+ background: linear-gradient(135deg, #1B132C 0%, #2C1A4D 50%, #3B1E63 100%) !important;
+ border-bottom: 2px solid #7434CA !important;
+ box-shadow: 0 2px 12px rgba(27, 19, 44, 0.4) !important;
.o_menu_brand {
color: #fff !important;
@@ -14,9 +15,12 @@
letter-spacing: -0.2px !important;
}
+ /* Top navbar main links */
.o_nav_entry,
- .o_menu_sections > .o_nav_entry > a,
- .o_menu_sections .o_dropdown_menu .o_nav_entry > a {
+ .o_menu_sections > .nav-link,
+ .o_menu_sections > .dropdown > .dropdown-toggle,
+ .o_menu_sections > a,
+ .o_menu_sections > button {
color: rgba(255, 255, 255, 0.9) !important;
&:hover {
@@ -25,12 +29,54 @@
}
}
- // Active breadcrumb / current app highlight
- .o_menu_sections .o_nav_entry.active > a,
- .o_menu_sections .o_nav_entry > a.active {
- color: #fff !important;
- background: rgba(255, 255, 255, 0.2) !important;
- border-radius: 6px;
+ /* Top navbar dropdown menus (Performance dropdown, etc.) */
+ .dropdown-menu {
+ background-color: #ffffff !important;
+ border: 1px solid #ede4f7 !important;
+ box-shadow: 0 10px 30px rgba(27, 19, 44, 0.18) !important;
+ border-radius: 10px !important;
+
+ .dropdown-item,
+ a.dropdown-item,
+ span.dropdown-item {
+ color: #1B132C !important;
+ background-color: transparent !important;
+ font-weight: 500 !important;
+ padding: 8px 18px !important;
+
+ &:hover,
+ &:focus {
+ background-color: #f3ebfa !important;
+ color: #7434CA !important;
+ }
+
+ &.active,
+ &:active {
+ background-color: #7434CA !important;
+ color: #ffffff !important;
+ }
+ }
+ }
+
+ // Active tab / current page highlight - theme pill with crisp white text
+ .o_menu_sections > .active,
+ .o_menu_sections > .nav-link.active,
+ .o_menu_sections > .o_nav_entry.active,
+ .o_menu_sections > a.active,
+ .o_menu_sections > button.active,
+ .o_menu_sections > [aria-current="page"],
+ .o_menu_sections > .dropdown.show > .dropdown-toggle,
+ .o_menu_sections > .dropdown > .dropdown-toggle.active,
+ .o_menu_sections .o_nav_entry:focus,
+ .o_menu_sections .o_nav_entry:active,
+ .o_menu_sections a:focus,
+ .o_menu_sections a:active {
+ background-color: rgba(255, 255, 255, 0.22) !important;
+ background: rgba(255, 255, 255, 0.22) !important;
+ color: #ffffff !important;
+ font-weight: 700 !important;
+ border-bottom: 3px solid #ffffff !important;
+ border-radius: 4px !important;
}
// Icons and caret
@@ -175,10 +221,24 @@
}
// Notebook tabs
- .o_notebook .nav-tabs .nav-link.active {
- color: #875A7B !important;
- border-bottom-color: #875A7B !important;
- font-weight: 600;
+ .o_notebook .nav-tabs .nav-link.active,
+ .nav-tabs .nav-link.active {
+ background: linear-gradient(135deg, #7434CA 0%, #5B1FA8 100%) !important;
+ color: #ffffff !important;
+ border-color: #7434CA !important;
+ font-weight: 700 !important;
+ border-radius: 8px 8px 0 0 !important;
+ }
+
+ .o_notebook .nav-tabs .nav-link:not(.active),
+ .nav-tabs .nav-link:not(.active) {
+ color: #495057 !important;
+ font-weight: 600 !important;
+
+ &:hover {
+ color: #7434CA !important;
+ background-color: #f8f6fc !important;
+ }
}
}
diff --git a/addons/dance_student/__manifest__.py b/addons/dance_student/__manifest__.py
index 07405fb..9db1279 100644
--- a/addons/dance_student/__manifest__.py
+++ b/addons/dance_student/__manifest__.py
@@ -1,7 +1,7 @@
{
"name": "Dance Student Management",
"summary": "Student profile fields on contacts for dance school ERP",
- "version": "17.0.1.0.0",
+ "version": "17.0.1.0.5",
"category": "Education",
"author": "Metetroncube",
"license": "LGPL-3",
@@ -10,6 +10,13 @@
"data/student_sequence.xml",
"views/res_partner_student_views.xml",
],
+ "assets": {
+ "web.assets_backend": [
+ "dance_student/static/src/students_page/students_page.js",
+ "dance_student/static/src/students_page/students_page.xml",
+ "dance_student/static/src/students_page/students_page.scss",
+ ],
+ },
"application": True,
"installable": True,
}
diff --git a/addons/dance_student/static/src/students_page/students_page.js b/addons/dance_student/static/src/students_page/students_page.js
new file mode 100644
index 0000000..8e55d12
--- /dev/null
+++ b/addons/dance_student/static/src/students_page/students_page.js
@@ -0,0 +1,104 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Component, useState, onWillStart } from "@odoo/owl";
+
+export class DanceStudentsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ students: [],
+ loading: true,
+ searchQuery: "",
+ });
+
+ onWillStart(async () => {
+ await this.loadStudents();
+ });
+ }
+
+ async loadStudents() {
+ this.state.loading = true;
+ let domain = [['is_student', '=', true]];
+ if (this.state.searchQuery) {
+ domain.push(['name', 'ilike', this.state.searchQuery]);
+ }
+
+ this.state.students = await this.orm.searchRead(
+ "res.partner",
+ domain,
+ ["id", "name", "student_code", "phone", "email", "dance_style", "level", "student_status", "join_date", "image_1920"]
+ );
+ this.state.loading = false;
+ }
+
+ onSearch(ev) {
+ if (ev.key === "Enter" || ev.type === "click") {
+ this.loadStudents();
+ }
+ }
+
+ async openStudent(id) {
+ let viewId = false;
+ try {
+ viewId = await this.orm.call("ir.model.data", "_xmlid_to_res_id", ["dance_student.view_dance_student_form"]);
+ } catch (e) {
+ console.warn("Could not load custom student form view ID", e);
+ }
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'res.partner',
+ res_id: id,
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ });
+ }
+
+ async createStudent() {
+ let viewId = false;
+ try {
+ viewId = await this.orm.call("ir.model.data", "_xmlid_to_res_id", ["dance_student.view_dance_student_form"]);
+ } catch (e) {
+ console.warn("Could not load custom student form view ID", e);
+ }
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'res.partner',
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ context: {
+ default_is_student: true,
+ default_student_status: 'trial'
+ }
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.client", name: "Leave Requests", tag: "dance_leave.action_leave_requests_dashboard" },
+ attendance: { type: "ir.actions.client", name: "Attendance", tag: "dance_attendance.action_attendance_dashboard" },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" },
+ grade_cards: { type: "ir.actions.client", name: "Grade Cards", tag: "dance_performance.action_grade_cards_dashboard" },
+ performance_criteria: { type: "ir.actions.client", name: "Performance Criteria", tag: "dance_performance.action_performance_criteria_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceStudentsPage.template = "dance_student.DanceStudentsPage";
+registry.category("actions").add("dance_student.action_students_dashboard", DanceStudentsPage);
diff --git a/addons/dance_student/static/src/students_page/students_page.scss b/addons/dance_student/static/src/students_page/students_page.scss
new file mode 100644
index 0000000..ef266e5
--- /dev/null
+++ b/addons/dance_student/static/src/students_page/students_page.scss
@@ -0,0 +1,686 @@
+/* Custom Student Page Styling */
+.stu-wrap {
+ height: 100vh;
+ display: flex;
+ flex-direction: row;
+ background-color: #F8F9FA;
+ overflow: hidden;
+}
+
+/* Sidebar */
+.stu-sidebar {
+ width: 250px;
+ background: linear-gradient(180deg, #1B132C 0%, #301f4c 100%);
+ color: white;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 4px 0 15px rgba(0,0,0,0.1);
+ z-index: 10;
+}
+
+.stu-sidebar-brand {
+ padding: 25px 20px;
+ font-size: 1.5rem;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.stu-sidebar-nav {
+ padding: 20px 0;
+ flex: 1;
+ overflow-y: auto;
+}
+
+/* Hide scrollbar for Chrome, Safari and Opera */
+.stu-sidebar-nav::-webkit-scrollbar {
+ display: none;
+}
+/* Hide scrollbar for IE, Edge and Firefox */
+.stu-sidebar-nav {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+}
+
+.stu-nav-item {
+ padding: 15px 25px;
+ font-size: 1.1rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: rgba(255,255,255,0.7);
+ transition: all 0.2s;
+}
+
+.stu-nav-item:hover, .stu-nav-item.active {
+ background-color: rgba(255,255,255,0.1);
+ color: white;
+ border-left: 4px solid #7434CA;
+}
+
+/* Sidebar Submenu */
+.stu-nav-group {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 5px;
+}
+.stu-nav-group-title {
+ padding: 15px 25px 5px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: rgba(255,255,255,0.4);
+ letter-spacing: 1px;
+ text-transform: uppercase;
+}
+.stu-nav-subitem {
+ padding: 10px 25px 10px 45px;
+ font-size: 1rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: rgba(255,255,255,0.7);
+ transition: all 0.2s;
+ border-left: 4px solid transparent;
+}
+.stu-nav-subitem:hover, .stu-nav-subitem.active {
+ background-color: rgba(255,255,255,0.05);
+ color: white;
+ border-left: 4px solid #7434CA;
+}
+
+/* Main Content Wrapper */
+.stu-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+/* Top Bar */
+.stu-top-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 30px;
+ background-color: white;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+ z-index: 5;
+}
+
+.stu-top-title {
+ font-size: 1.4rem;
+ font-weight: 700;
+ color: #171238;
+}
+
+.stu-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+/* Search Box */
+.stu-search {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+.stu-search input {
+ padding: 10px 15px 10px 35px;
+ border-radius: 20px;
+ border: 1px solid #EAECEF;
+ outline: none;
+ width: 250px;
+ font-size: 0.95rem;
+ background-color: #F8F9FA;
+ color: #333;
+ transition: all 0.2s ease;
+}
+.stu-search input::placeholder {
+ color: #ADB5BD;
+}
+.stu-search input:focus {
+ background-color: white;
+ border-color: #7434CA;
+ box-shadow: 0 0 0 3px rgba(116,52,202,0.1);
+}
+.stu-search i {
+ position: absolute;
+ left: 12px;
+ color: #ADB5BD;
+}
+
+/* Buttons */
+.stu-btn {
+ border: none;
+ border-radius: 6px;
+ padding: 10px 20px;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: all 0.2s ease;
+}
+.stu-btn-primary {
+ background-color: #7434CA;
+ color: white;
+}
+.stu-btn-primary:hover {
+ background-color: #5d29a3;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 10px rgba(116, 52, 202, 0.3);
+}
+.stu-btn-secondary {
+ background-color: #EAECEF;
+ color: #495057;
+ padding: 10px 15px;
+}
+.stu-btn-secondary:hover {
+ background-color: #DEE2E6;
+}
+
+/* Content Area */
+.stu-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 30px;
+}
+
+/* Loading & Empty States */
+.stu-loading, .stu-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #6C757D;
+ gap: 15px;
+}
+
+/* Table Card Layout */
+.stu-table-card {
+ background-color: white;
+ border-radius: 12px;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.05);
+ border: 1px solid #EAECEF;
+ overflow: hidden;
+}
+
+.stu-table {
+ width: 100%;
+ border-collapse: collapse;
+ text-align: left;
+}
+
+.stu-table thead {
+ background-color: #F8F9FA;
+ border-bottom: 2px solid #EAECEF;
+}
+
+.stu-table th {
+ padding: 15px 20px;
+ color: #6C757D;
+ font-weight: 600;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.stu-table tbody tr {
+ border-bottom: 1px solid #F1F3F5;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.stu-table tbody tr:hover {
+ background-color: #F8F9FA;
+}
+
+.stu-table tbody tr:last-child {
+ border-bottom: none;
+}
+
+.stu-table td {
+ padding: 15px 20px;
+ vertical-align: middle;
+ color: #495057;
+ font-size: 0.95rem;
+}
+
+.stu-td-id {
+ font-weight: 600;
+ color: #171238;
+}
+
+.stu-td-name {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-weight: 600;
+ color: #171238;
+}
+
+.stu-table-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ background-color: #EAECEF;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ADB5BD;
+}
+.stu-table-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+/* Badges */
+.stu-badge {
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ display: inline-block;
+}
+.stu-badge-active { background-color: #D1E7DD; color: #0F5132; }
+.stu-badge-trial { background-color: #FFF3CD; color: #854000; }
+.stu-badge-hold { background-color: #E2E3E5; color: #41464B; }
+
+/* ── Top Navbar Theme & Active Tab Contrast Fix ── */
+.o_navbar,
+.o_main_navbar {
+ background: linear-gradient(135deg, #1B132C 0%, #2C1A4D 50%, #3B1E63 100%) !important;
+ border-bottom: 2px solid #7434CA !important;
+ box-shadow: 0 2px 10px rgba(27, 19, 44, 0.4) !important;
+}
+
+.o_main_navbar .o_menu_brand {
+ color: #ffffff !important;
+ font-weight: 700 !important;
+}
+
+.o_main_navbar .o_menu_sections > .o_nav_entry,
+.o_main_navbar .o_menu_sections > .nav-link,
+.o_main_navbar .o_menu_sections > .dropdown > .dropdown-toggle,
+.o_main_navbar .o_menu_sections > a,
+.o_main_navbar .o_menu_sections > button {
+ color: rgba(255, 255, 255, 0.88) !important;
+ font-weight: 500 !important;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.15) !important;
+ color: #ffffff !important;
+ }
+}
+
+/* Active / Current Tab on navbar: Theme pill with crisp white text */
+.o_main_navbar .o_menu_sections > .active,
+.o_main_navbar .o_menu_sections > .nav-link.active,
+.o_main_navbar .o_menu_sections > .o_nav_entry.active,
+.o_main_navbar .o_menu_sections > a.active,
+.o_main_navbar .o_menu_sections > button.active,
+.o_main_navbar .o_menu_sections > [aria-current="page"],
+.o_main_navbar .o_menu_sections > .dropdown.show > .dropdown-toggle,
+.o_main_navbar .o_menu_sections > .dropdown > .dropdown-toggle.active,
+.o_main_navbar .o_menu_sections .o_nav_entry:focus,
+.o_main_navbar .o_menu_sections .o_nav_entry:active,
+.o_main_navbar .o_menu_sections a:focus,
+.o_main_navbar .o_menu_sections a:active {
+ background-color: rgba(255, 255, 255, 0.22) !important;
+ background: rgba(255, 255, 255, 0.22) !important;
+ color: #ffffff !important;
+ font-weight: 700 !important;
+ border-bottom: 3px solid #ffffff !important;
+ border-radius: 4px !important;
+}
+
+/* Top Navbar Dropdown Menus (Performance, etc. - Visible dark text on white menu) */
+.o_main_navbar .dropdown-menu,
+.o_navbar .dropdown-menu,
+.o_menu_sections .dropdown-menu {
+ background-color: #ffffff !important;
+ border: 1px solid #ede4f7 !important;
+ box-shadow: 0 10px 30px rgba(27, 19, 44, 0.18) !important;
+ border-radius: 10px !important;
+ padding: 8px 0 !important;
+
+ .dropdown-item,
+ a.dropdown-item,
+ span.dropdown-item,
+ .o_nav_entry {
+ color: #1B132C !important;
+ background-color: transparent !important;
+ font-weight: 600 !important;
+ padding: 9px 20px !important;
+ font-size: 0.95rem !important;
+
+ &:hover,
+ &:focus {
+ background-color: #f3ebfa !important;
+ color: #7434CA !important;
+ }
+
+ &.active,
+ &:active {
+ background-color: #7434CA !important;
+ color: #ffffff !important;
+ }
+ }
+
+ .dropdown-header {
+ color: #7434CA !important;
+ font-weight: 700 !important;
+ font-size: 0.8rem !important;
+ text-transform: uppercase !important;
+ padding: 8px 20px 4px !important;
+ }
+}
+
+/* ── Dance Academy Form View Styling (New Student & New Guardian Pages) ── */
+.o_dance_form_view,
+.o_form_view {
+ background-color: transparent !important;
+
+ .o_form_sheet_bg {
+ background-color: #f7f5fa !important;
+ padding: 24px 32px !important;
+ }
+
+ .o_form_sheet {
+ background-color: #ffffff !important;
+ border-radius: 16px !important;
+ box-shadow: 0 10px 30px rgba(27, 19, 44, 0.08) !important;
+ border: 1px solid rgba(116, 52, 202, 0.12) !important;
+ padding: 32px 40px !important;
+ }
+
+ /* Statusbar */
+ .o_form_statusbar {
+ background: #ffffff !important;
+ border-bottom: 2px solid #ede4f7 !important;
+ padding: 10px 18px !important;
+ border-radius: 14px 14px 0 0 !important;
+
+ .o_statusbar_status {
+ .o_arrow_button {
+ font-weight: 600 !important;
+ font-size: 0.85rem !important;
+ border-radius: 6px !important;
+ margin-left: 4px !important;
+ transition: all 0.2s ease !important;
+
+ &.btn-primary,
+ &.disabled.btn-primary {
+ background: linear-gradient(135deg, #7434CA 0%, #531C96 100%) !important;
+ border-color: #7434CA !important;
+ color: #ffffff !important;
+ box-shadow: 0 3px 8px rgba(116, 52, 202, 0.25) !important;
+ }
+
+ &.btn-secondary {
+ color: #495057 !important;
+ background-color: #f8f6fc !important;
+ border: 1px solid #ede4f7 !important;
+
+ &:hover {
+ color: #7434CA !important;
+ background-color: #ede4f7 !important;
+ }
+ }
+ }
+ }
+ }
+
+ /* Section Cards */
+ .o_dance_section_card {
+ background: #faf8fd;
+ border: 1px solid #ede6f7;
+ border-radius: 12px;
+ padding: 20px 24px 10px;
+ margin-bottom: 20px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: rgba(116, 52, 202, 0.3);
+ box-shadow: 0 4px 14px rgba(116, 52, 202, 0.06);
+ }
+ }
+
+ .o_dance_section_header {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: #1B132C;
+ letter-spacing: 0.4px;
+ text-transform: uppercase;
+ padding-bottom: 12px;
+ margin-bottom: 16px;
+ border-bottom: 2px solid #e9dff7;
+ display: flex;
+ align-items: center;
+
+ i {
+ color: #7434CA;
+ font-size: 1.1rem;
+ }
+ }
+
+ /* Avatar styling */
+ .oe_avatar {
+ border-radius: 16px !important;
+ border: 2px dashed #CBB5E8 !important;
+ background-color: #faf8fd !important;
+ padding: 6px !important;
+ transition: all 0.2s ease !important;
+
+ img {
+ border-radius: 12px !important;
+ }
+
+ &:hover {
+ border-color: #7434CA !important;
+ background-color: #f3ebfa !important;
+ transform: scale(1.02);
+ }
+ }
+
+ /* Title / Name field */
+ .oe_title {
+ h1 input.o_input,
+ h1 .o_field_char {
+ font-size: 1.8rem !important;
+ font-weight: 700 !important;
+ color: #1B132C !important;
+ }
+ }
+
+ /* Field Labels */
+ .o_form_label,
+ label {
+ color: #3B2A58 !important;
+ font-weight: 600 !important;
+ font-size: 0.85rem !important;
+ letter-spacing: 0.2px !important;
+ }
+
+ /* Inputs, selects, textareas */
+ input.o_input,
+ textarea.o_input,
+ select.o_input,
+ .o_field_widget input,
+ .o_field_widget textarea,
+ .o_field_widget select {
+ border: 1px solid #D9CEE8 !important;
+ border-radius: 8px !important;
+ padding: 8px 12px !important;
+ background-color: #ffffff !important;
+ color: #171238 !important;
+ font-size: 0.95rem !important;
+ transition: all 0.2s ease !important;
+
+ &:focus {
+ border-color: #7434CA !important;
+ box-shadow: 0 0 0 3px rgba(116, 52, 202, 0.15) !important;
+ background-color: #ffffff !important;
+ outline: none !important;
+ }
+ }
+
+ /* Radio buttons & checkboxes */
+ input[type="radio"],
+ input[type="checkbox"] {
+ accent-color: #7434CA !important;
+ cursor: pointer;
+ }
+
+ /* Notebook Tabs */
+ .o_notebook .nav-tabs {
+ border-bottom: 2px solid #ede4f7 !important;
+ gap: 8px !important;
+
+ .nav-link {
+ border: none !important;
+ border-radius: 8px 8px 0 0 !important;
+ padding: 10px 22px !important;
+ color: #555 !important;
+ font-weight: 600 !important;
+ background: transparent !important;
+ transition: all 0.2s ease !important;
+
+ &:hover {
+ color: #7434CA !important;
+ background-color: #faf8fd !important;
+ }
+
+ &.active {
+ background: linear-gradient(135deg, #7434CA 0%, #5B1FA8 100%) !important;
+ color: #ffffff !important;
+ box-shadow: 0 4px 12px rgba(116, 52, 202, 0.25) !important;
+ }
+ }
+ }
+
+ /* Chatter Action Buttons */
+ .oe_chatter {
+ .btn-primary,
+ .o_Chatter_sendButton {
+ background: linear-gradient(135deg, #7434CA 0%, #531C96 100%) !important;
+ border-color: #7434CA !important;
+ color: #ffffff !important;
+ border-radius: 8px !important;
+ font-weight: 600 !important;
+ box-shadow: 0 3px 8px rgba(116, 52, 202, 0.2) !important;
+ }
+
+ .btn-secondary,
+ .o_Chatter_logButton,
+ .o_Chatter_activityButton {
+ border-color: #D9CEE8 !important;
+ color: #3B2A58 !important;
+ border-radius: 8px !important;
+ font-weight: 600 !important;
+
+ &:hover {
+ background-color: #faf8fd !important;
+ border-color: #7434CA !important;
+ color: #7434CA !important;
+ }
+ }
+ }
+}
+
+/* Control Panel Buttons and Breadcrumbs */
+.o_control_panel {
+ background: linear-gradient(135deg, #f8f6fc 0%, #ede6f7 100%) !important;
+ border-bottom: 2px solid #7434CA !important;
+ padding: 12px 24px !important;
+
+ .breadcrumb-item a {
+ color: #7434CA !important;
+ font-weight: 600 !important;
+ }
+
+ .breadcrumb-item.active {
+ color: #1B132C !important;
+ font-weight: 700 !important;
+ }
+
+ .o_form_button_save,
+ .btn-primary {
+ background: linear-gradient(135deg, #7434CA 0%, #531C96 100%) !important;
+ border-color: #7434CA !important;
+ color: #ffffff !important;
+ font-weight: 600 !important;
+ border-radius: 6px !important;
+ box-shadow: 0 4px 10px rgba(116, 52, 202, 0.25) !important;
+ transition: all 0.2s ease !important;
+
+ &:hover {
+ transform: translateY(-1px) !important;
+ box-shadow: 0 6px 14px rgba(116, 52, 202, 0.35) !important;
+ }
+ }
+
+ .o_form_button_cancel,
+ .btn-secondary {
+ border: 1px solid #D9CEE8 !important;
+ color: #495057 !important;
+ border-radius: 6px !important;
+ font-weight: 600 !important;
+
+ &:hover {
+ background-color: #ede4f7 !important;
+ color: #7434CA !important;
+ }
+ }
+
+ .btn.o_arrow_button {
+ font-weight: 600 !important;
+ }
+}
+
+/* Badges */
+.bg-purple-soft {
+ background-color: #ede4f7 !important;
+}
+.text-purple {
+ color: #7434CA !important;
+}
+
+/* ── Global Notebook Tab White Text Contrast (All Form Pages: Students, Guardians, Batches, Events, etc.) ── */
+.o_notebook .nav-tabs .nav-link.active,
+.o_notebook .nav-tabs .nav-item.show .nav-link,
+.o_notebook .nav-link.active,
+.nav-tabs .nav-link.active,
+.o_form_view .o_notebook .nav-tabs .nav-link.active,
+.o_form_view .nav-tabs .nav-link.active,
+div.o_notebook ul.nav-tabs > li.nav-item > a.nav-link.active,
+div.o_notebook ul.nav-tabs > li.nav-item > button.nav-link.active,
+.o_notebook .nav-tabs .nav-link.active span,
+.nav-tabs .nav-link.active span {
+ background: linear-gradient(135deg, #7434CA 0%, #5B1FA8 100%) !important;
+ color: #ffffff !important;
+ font-weight: 700 !important;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) !important;
+ border-color: #7434CA !important;
+ border-radius: 8px 8px 0 0 !important;
+ box-shadow: 0 4px 12px rgba(116, 52, 202, 0.3) !important;
+}
+
+.o_notebook .nav-tabs .nav-link:not(.active),
+.nav-tabs .nav-link:not(.active) {
+ color: #495057 !important;
+ font-weight: 600 !important;
+
+ &:hover {
+ color: #7434CA !important;
+ background-color: #faf8fd !important;
+ }
+}
diff --git a/addons/dance_student/static/src/students_page/students_page.xml b/addons/dance_student/static/src/students_page/students_page.xml
new file mode 100644
index 0000000..a3b0407
--- /dev/null
+++ b/addons/dance_student/static/src/students_page/students_page.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Students Management
+
+
+
+
+
+
+
Loading Students...
+
+
+
+
+
No Students Found
+
Try adjusting your search or create a new student.
+
+
+
+
+
+
+
+
+ | ID |
+ Student Name |
+ Phone |
+ Dance Style |
+ Level |
+ Status |
+
+
+
+
+ |
+
+
+ ![Avatar]()
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_student/views/res_partner_student_views.xml b/addons/dance_student/views/res_partner_student_views.xml
index 9b91f10..ad8f503 100644
--- a/addons/dance_student/views/res_partner_student_views.xml
+++ b/addons/dance_student/views/res_partner_student_views.xml
@@ -77,8 +77,118 @@
-
+
+
Students
+ dance_student.action_students_dashboard
+
+
+
+
+ dance.student.form
+ res.partner
+
+
+
+
+
+
+
+ Students List
res.partner
tree,form
[('is_student', '=', True)]
@@ -86,9 +196,9 @@
+ (0, 0, {'view_mode': 'form', 'view_id': ref('view_dance_student_form')})]"/>
-
+
diff --git a/addons/dance_web/__manifest__.py b/addons/dance_web/__manifest__.py
index cd8be78..c434319 100644
--- a/addons/dance_web/__manifest__.py
+++ b/addons/dance_web/__manifest__.py
@@ -23,6 +23,10 @@
"web.assets_frontend": [
"dance_web/static/src/css/dance_web.css",
],
+ "web.assets_backend": [
+ "dance_web/static/src/enrollment_requests_page/enrollment_requests_page.js",
+ "dance_web/static/src/enrollment_requests_page/enrollment_requests_page.xml",
+ ],
},
"installable": True,
"application": False,
diff --git a/addons/dance_web/models/__pycache__/res_users.cpython-310.pyc b/addons/dance_web/models/__pycache__/res_users.cpython-310.pyc
index 4bc906c..36995c0 100644
Binary files a/addons/dance_web/models/__pycache__/res_users.cpython-310.pyc and b/addons/dance_web/models/__pycache__/res_users.cpython-310.pyc differ
diff --git a/addons/dance_web/models/res_users.py b/addons/dance_web/models/res_users.py
index a49d112..50a07e3 100644
--- a/addons/dance_web/models/res_users.py
+++ b/addons/dance_web/models/res_users.py
@@ -30,7 +30,7 @@ class ResUsers(models.Model):
("is_student", "=", True),
"|",
("guardian_id", "=", partner.id),
- ("guardian_ids", "in", partner.id),
+ ("guardian_ids", "in", [partner.id]),
]
)
)
@@ -41,5 +41,5 @@ class ResUsers(models.Model):
if self._dance_is_teacher():
return "/teacher"
if self._dance_is_parent():
- return "/parent"
+ return "/my/home"
return False
diff --git a/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.js b/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.js
new file mode 100644
index 0000000..a802732
--- /dev/null
+++ b/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.js
@@ -0,0 +1,93 @@
+/** @odoo-module **/
+
+import { Component, useState, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+
+export class DanceEnrollmentRequestsPage extends Component {
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+
+ this.state = useState({
+ requests: [],
+ loading: false,
+ searchQuery: ""
+ });
+
+ onWillStart(async () => {
+ await this.loadRequests();
+ });
+ }
+
+ async loadRequests() {
+ this.state.loading = true;
+ try {
+ let domain = [["state", "!=", "converted"], ["state", "!=", "rejected"]];
+ if (this.state.searchQuery) {
+ domain = [
+ "&",
+ ["state", "!=", "converted"],
+ "|", "|", "|",
+ ["reference", "ilike", this.state.searchQuery],
+ ["student_name", "ilike", this.state.searchQuery],
+ ["guardian_name", "ilike", this.state.searchQuery],
+ ["guardian_phone", "ilike", this.state.searchQuery]
+ ];
+ }
+
+ this.state.requests = await this.orm.searchRead(
+ "dance.enrollment.request",
+ domain,
+ ["reference", "student_name", "guardian_name", "guardian_phone", "dance_style", "batch_id", "create_date", "state"],
+ { limit: 50, order: "create_date DESC" }
+ );
+ } catch (e) {
+ console.error("Error loading enrollment requests:", e);
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ onSearch(ev) {
+ if (ev.type === "keyup" && ev.key !== "Enter") {
+ return;
+ }
+ this.loadRequests();
+ }
+
+ openRequest(requestId) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "dance.enrollment.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ target: "current"
+ });
+ }
+
+ navigate(route) {
+ const routes = {
+ dashboard: "dance_reports.dance_school_dashboard_action",
+ students: { type: "ir.actions.client", name: "Students", tag: "dance_student.action_students_dashboard" },
+ guardians: { type: "ir.actions.client", name: "Guardians", tag: "dance_guardian.action_guardians_dashboard" },
+ batches: { type: "ir.actions.client", name: "Batches", tag: "dance_batch.action_batches_dashboard" },
+ enrollments: { type: "ir.actions.client", name: "Enrollments", tag: "dance_batch.action_enrollments_dashboard" },
+ events: { type: "ir.actions.client", name: "Events & Recitals", tag: "dance_event.action_events_dashboard" },
+ enrollment_requests: { type: "ir.actions.client", name: "Enrollment Requests", tag: "dance_web.action_enrollment_requests_dashboard" },
+ sessions: { type: "ir.actions.client", name: "Sessions", tag: "dance_attendance.action_sessions_dashboard" },
+ leave_requests: { type: "ir.actions.act_window", name: "Leave Requests", res_model: "dance.leave.request", views: [[false, "list"], [false, "form"]] },
+ attendance: { type: "ir.actions.act_window", name: "Attendance", res_model: "dance.attendance", views: [[false, "list"], [false, "form"]] },
+ performance: { type: "ir.actions.client", name: "Performances", tag: "dance_event.action_performances_dashboard" },
+ rehearsals: { type: "ir.actions.client", name: "Rehearsals", tag: "dance_event.action_rehearsals_dashboard" }
+ };
+
+ if (routes[route]) {
+ this.action.doAction(routes[route]);
+ }
+ }
+}
+
+DanceEnrollmentRequestsPage.template = "dance_web.DanceEnrollmentRequestsPage";
+
+registry.category("actions").add("dance_web.action_enrollment_requests_dashboard", DanceEnrollmentRequestsPage);
diff --git a/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.xml b/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.xml
new file mode 100644
index 0000000..f62c9aa
--- /dev/null
+++ b/addons/dance_web/static/src/enrollment_requests_page/enrollment_requests_page.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
Enrollment Requests
+
+
+
+
+
+
+
Loading Requests...
+
+
+
+
+
No Enrollment Requests Found
+
No new requests match your search.
+
+
+
+
+
+
+ | Reference |
+ Student Name |
+ Parent / Guardian |
+ Phone |
+ Dance Style |
+ Preferred Batch |
+ Created On |
+ State |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+ New Request
+ Contacted
+ Approved
+ Converted
+ Rejected
+ UNKNOWN
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/addons/dance_web/views/enrollment_request_views.xml b/addons/dance_web/views/enrollment_request_views.xml
index 5051564..c9006a5 100644
--- a/addons/dance_web/views/enrollment_request_views.xml
+++ b/addons/dance_web/views/enrollment_request_views.xml
@@ -120,6 +120,11 @@
+
+ Enrollment Requests Dashboard
+ dance_web.action_enrollment_requests_dashboard
+
+
Enrollment Requests
dance.enrollment.request
@@ -134,7 +139,7 @@
diff --git a/parent_portal_design.html b/parent_portal_design.html
new file mode 100644
index 0000000..6d16814
--- /dev/null
+++ b/parent_portal_design.html
@@ -0,0 +1,628 @@
+
+
+
+
+
+ Parent Portal - Dance Vibe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
test student
+ STU005
+
+
+
+ Active
+
+
+
+
+
+
+
Dance Style
+
Bharatanatyam
+
+
+
+
+
+
+
+
+
+
+
+
0.0%
+
+
+
+
+ 1 Session
+ 1 Absence
+
+
+
+
+
+
+
+
+
1
+
+ Student invoice pending or generated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Enrolled Batches
+ 1 Batch
+
+
+
+
+
+
+ | Batch Name |
+ Status |
+ Enrollment Date |
+
+
+
+
+ |
+
+ |
+
+ Active
+ |
+
+ 2026-09-28
+ |
+
+
+
+
+
+
+
+
+
+
+
Leave Requests
+
+
+
+
+
+
+ | From |
+ To |
+ Reason |
+ Status |
+
+
+
+
+ | 2026-09-29 |
+ 2026-09-29 |
+ nnn |
+
+ Approved
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+