Fix top navbar active tab styling, custom OWL list/card pages, and dedicated student/guardian form views

This commit is contained in:
srikanth01234 2026-10-12 22:33:07 +05:30
parent 3a67039506
commit c50d4f8b4d
64 changed files with 5335 additions and 527 deletions

View File

@ -14,4 +14,12 @@
], ],
"application": False, "application": False,
"installable": True, "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",
],
},
} }

View File

@ -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);

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_attendance.DanceAttendancePage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'attendance'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Attendance</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search attendance..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-secondary" t-on-click="loadAttendance" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Attendance...</p>
</div>
<div t-elif="!state.attendanceRecords.length" class="stu-empty">
<i class="fa fa-check-square-o fa-4x"/>
<h3>No Attendance Records Found</h3>
<p>Try adjusting your search.</p>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Date</th>
<th>Batch</th>
<th>Session</th>
<th>Student</th>
<th>Status</th>
<th>Remarks</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.attendanceRecords" t-as="record" t-key="record.id" t-on-click="() => this.openAttendance(record.id)">
<td t-esc="record.date || '-'"/>
<td t-esc="record.batch_id ? record.batch_id[1] : '-'"/>
<td t-esc="record.session_id ? record.session_id[1] : '-'"/>
<td class="stu-td-name" t-esc="record.student_id ? record.student_id[1] : '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{record.status}">
<t t-esc="record.status ? record.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
<td t-esc="record.remarks || '-'"/>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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);

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_attendance.DanceSessionsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'sessions'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Sessions</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search sessions..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createSession">
<i class="fa fa-plus"/> New Session
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadSessions" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Sessions...</p>
</div>
<div t-elif="!state.sessions.length" class="stu-empty">
<i class="fa fa-calendar fa-4x"/>
<h3>No Sessions Found</h3>
<p>Try adjusting your search or create a new session.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createSession">Create Session</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Date</th>
<th>Batch</th>
<th>Start Time</th>
<th>End Time</th>
<th>Room</th>
<th>Instructor</th>
<th>Attendance Count</th>
<th>Present Count</th>
<th>Absent Count</th>
<th>Late Count</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.sessions" t-as="session" t-key="session.id" t-on-click="() => this.openSession(session.id)">
<td t-esc="session.date || '-'"/>
<td t-esc="session.batch_id ? session.batch_id[1] : '-'"/>
<td t-esc="formatTime(session.start_time)"/>
<td t-esc="formatTime(session.end_time)"/>
<td t-esc="session.room || '-'"/>
<td t-esc="session.instructor_id ? session.instructor_id[1] : '-'"/>
<td t-esc="session.attendance_count || 0"/>
<td t-esc="session.present_count || 0"/>
<td t-esc="session.absent_count || 0"/>
<td t-esc="session.late_count || 0"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{session.status}">
<t t-esc="session.status ? session.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -61,6 +61,11 @@
</field> </field>
</record> </record>
<record id="action_attendance_dashboard" model="ir.actions.client">
<field name="name">Attendance Dashboard</field>
<field name="tag">dance_attendance.action_attendance_dashboard</field>
</record>
<record id="action_dance_attendance" model="ir.actions.act_window"> <record id="action_dance_attendance" model="ir.actions.act_window">
<field name="name">Attendance</field> <field name="name">Attendance</field>
<field name="res_model">dance.attendance</field> <field name="res_model">dance.attendance</field>
@ -68,5 +73,5 @@
<field name="search_view_id" ref="view_dance_attendance_search"/> <field name="search_view_id" ref="view_dance_attendance_search"/>
</record> </record>
<menuitem id="menu_dance_attendance" name="Attendance" parent="dance_student.menu_dance_root" action="action_dance_attendance" sequence="50"/> <menuitem id="menu_dance_attendance" name="Attendance" parent="dance_student.menu_dance_root" action="action_attendance_dashboard" sequence="50"/>
</odoo> </odoo>

View File

@ -115,6 +115,11 @@
</field> </field>
</record> </record>
<record id="action_sessions_dashboard" model="ir.actions.client">
<field name="name">Sessions Dashboard</field>
<field name="tag">dance_attendance.action_sessions_dashboard</field>
</record>
<record id="action_dance_sessions" model="ir.actions.act_window"> <record id="action_dance_sessions" model="ir.actions.act_window">
<field name="name">Sessions</field> <field name="name">Sessions</field>
<field name="res_model">dance.session</field> <field name="res_model">dance.session</field>
@ -122,5 +127,5 @@
<field name="search_view_id" ref="view_dance_session_search"/> <field name="search_view_id" ref="view_dance_session_search"/>
</record> </record>
<menuitem id="menu_dance_sessions" name="Sessions" parent="dance_student.menu_dance_root" action="action_dance_sessions" sequence="40"/> <menuitem id="menu_dance_sessions" name="Sessions" parent="dance_student.menu_dance_root" action="action_sessions_dashboard" sequence="40"/>
</odoo> </odoo>

View File

@ -12,6 +12,14 @@
"views/dance_batch_views.xml", "views/dance_batch_views.xml",
"views/dance_enrollment_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, "application": False,
"installable": True, "installable": True,
} }

View File

@ -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);

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_batch.DanceBatchesPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'batches'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Batches</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search batches..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createBatch">
<i class="fa fa-plus"/> New Batch
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadBatches" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Batches...</p>
</div>
<div t-elif="!state.batches.length" class="stu-empty">
<i class="fa fa-cubes fa-4x"/>
<h3>No Batches Found</h3>
<p>Try adjusting your search or create a new batch.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createBatch">Create Batch</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Name</th>
<th>Dance Style</th>
<th>Level</th>
<th>Instructor</th>
<th>Schedule Days</th>
<th>Room</th>
<th>Current Strength</th>
<th>Capacity</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.batches" t-as="batch" t-key="batch.id" t-on-click="() => this.openBatch(batch.id)">
<td t-esc="batch.name || '-'"/>
<td style="text-transform: capitalize;" t-esc="batch.dance_style || '-'"/>
<td style="text-transform: capitalize;" t-esc="batch.level || '-'"/>
<td t-esc="batch.instructor_id ? batch.instructor_id[1] : '-'"/>
<td t-esc="batch.schedule_days || '-'"/>
<td t-esc="batch.room || '-'"/>
<td t-esc="batch.active_enrollment_count || 0"/>
<td t-esc="batch.capacity || 0"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{batch.status}">
<t t-esc="batch.status ? batch.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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);

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_batch.DanceEnrollmentsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'enrollments'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Enrollments</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search enrollments..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createEnrollment">
<i class="fa fa-plus"/> New Enrollment
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadEnrollments" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Enrollments...</p>
</div>
<div t-elif="!state.enrollments.length" class="stu-empty">
<i class="fa fa-check-circle fa-4x"/>
<h3>No Enrollments Found</h3>
<p>Try adjusting your search or create a new enrollment.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createEnrollment">Create Enrollment</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Student</th>
<th>Batch</th>
<th>Enrollment Date</th>
<th>Status</th>
<th t-if="state.hasInvoiceCount">Invoice Count</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.enrollments" t-as="enrollment" t-key="enrollment.id" t-on-click="() => this.openEnrollment(enrollment.id)">
<td t-esc="enrollment.student_id ? enrollment.student_id[1] : '-'"/>
<td t-esc="enrollment.batch_id ? enrollment.batch_id[1] : '-'"/>
<td t-esc="enrollment.enrollment_date || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{enrollment.status}">
<t t-esc="enrollment.status ? enrollment.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
<td t-if="state.hasInvoiceCount" t-esc="enrollment.invoice_count || 0"/>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -95,6 +95,11 @@
</field> </field>
</record> </record>
<record id="action_batches_dashboard" model="ir.actions.client">
<field name="name">Batches Dashboard</field>
<field name="tag">dance_batch.action_batches_dashboard</field>
</record>
<record id="action_dance_batches" model="ir.actions.act_window"> <record id="action_dance_batches" model="ir.actions.act_window">
<field name="name">Batches</field> <field name="name">Batches</field>
<field name="res_model">dance.batch</field> <field name="res_model">dance.batch</field>
@ -102,5 +107,5 @@
<field name="search_view_id" ref="view_dance_batch_search"/> <field name="search_view_id" ref="view_dance_batch_search"/>
</record> </record>
<menuitem id="menu_dance_batches" name="Batches" parent="dance_student.menu_dance_root" action="action_dance_batches" sequence="20"/> <menuitem id="menu_dance_batches" name="Batches" parent="dance_student.menu_dance_root" action="action_batches_dashboard" sequence="20"/>
</odoo> </odoo>

View File

@ -56,6 +56,11 @@
</field> </field>
</record> </record>
<record id="action_enrollments_dashboard" model="ir.actions.client">
<field name="name">Enrollments Dashboard</field>
<field name="tag">dance_batch.action_enrollments_dashboard</field>
</record>
<record id="action_dance_enrollments" model="ir.actions.act_window"> <record id="action_dance_enrollments" model="ir.actions.act_window">
<field name="name">Enrollments</field> <field name="name">Enrollments</field>
<field name="res_model">dance.enrollment</field> <field name="res_model">dance.enrollment</field>
@ -63,5 +68,5 @@
<field name="search_view_id" ref="view_dance_enrollment_search"/> <field name="search_view_id" ref="view_dance_enrollment_search"/>
</record> </record>
<menuitem id="menu_dance_enrollments" name="Enrollments" parent="dance_student.menu_dance_root" action="action_dance_enrollments" sequence="30"/> <menuitem id="menu_dance_enrollments" name="Enrollments" parent="dance_student.menu_dance_root" action="action_enrollments_dashboard" sequence="30"/>
</odoo> </odoo>

View File

@ -15,4 +15,14 @@
], ],
"application": False, "application": False,
"installable": True, "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",
],
},
} }

View File

@ -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);

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_event.DanceEventsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'events'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Events &amp; Recitals</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search events..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createEvent">
<i class="fa fa-plus"/> New Event
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadEvents" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Events...</p>
</div>
<div t-elif="!state.events.length" class="stu-empty">
<i class="fa fa-star fa-4x"/>
<h3>No Events Found</h3>
<p>Try adjusting your search or create a new event.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createEvent">Create Event</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Event Name</th>
<th>Type</th>
<th>Date</th>
<th>Venue</th>
<th>Participant Count</th>
<th>Performance Count</th>
<th>Rehearsal Count</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.events" t-as="event" t-key="event.id" t-on-click="() => this.openEvent(event.id)">
<td t-esc="event.name || '-'"/>
<td style="text-transform: capitalize;" t-esc="event.event_type ? event.event_type.replace('_', ' ') : '-'"/>
<td t-esc="event.date || '-'"/>
<td t-esc="event.venue || '-'"/>
<td t-esc="event.participant_count || 0"/>
<td t-esc="event.performance_count || 0"/>
<td t-esc="event.rehearsal_count || 0"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{event.state}">
<t t-esc="event.state ? event.state.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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);

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_event.DancePerformancesPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'performance'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Performances &amp; Costumes</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search performances..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createPerformance">
<i class="fa fa-plus"/> New Performance
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadPerformances" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Performances...</p>
</div>
<div t-elif="!state.performances.length" class="stu-empty">
<i class="fa fa-user-circle-o fa-4x"/>
<h3>No Performances Found</h3>
<p>Try adjusting your search or create a new performance.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createPerformance">Create Performance</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Event</th>
<th>Student</th>
<th>Role</th>
<th>Costume Needed</th>
<th>Costume Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.performances" t-as="perf" t-key="perf.id" t-on-click="() => this.openPerformance(perf.id)">
<td t-esc="perf.event_id ? perf.event_id[1] : '-'"/>
<td t-esc="perf.student_id ? perf.student_id[1] : '-'"/>
<td style="text-transform: capitalize;" t-esc="perf.role || '-'"/>
<td t-esc="perf.costume_needed || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{perf.costume_status ? perf.costume_status.replace('_', '-') : 'unknown'}">
<t t-esc="perf.costume_status ? perf.costume_status.replace('_', ' ').toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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);

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_event.DanceRehearsalsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'rehearsals'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Rehearsals</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search rehearsals..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createRehearsal">
<i class="fa fa-plus"/> New Rehearsal
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadRehearsals" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Rehearsals...</p>
</div>
<div t-elif="!state.rehearsals.length" class="stu-empty">
<i class="fa fa-clock-o fa-4x"/>
<h3>No Rehearsals Found</h3>
<p>Try adjusting your search or schedule a new rehearsal.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createRehearsal">Schedule Rehearsal</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Event</th>
<th>Date</th>
<th>Display Time</th>
<th>Room</th>
<th>Attendance Count</th>
<th>Present Count</th>
<th>Absent Count</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.rehearsals" t-as="rehearsal" t-key="rehearsal.id" t-on-click="() => this.openRehearsal(rehearsal.id)">
<td t-esc="rehearsal.event_id ? rehearsal.event_id[1] : '-'"/>
<td t-esc="rehearsal.date || '-'"/>
<td t-esc="rehearsal.display_time || '-'"/>
<td t-esc="rehearsal.room || '-'"/>
<td t-esc="rehearsal.attendance_count || 0"/>
<td t-esc="rehearsal.present_count || 0"/>
<td t-esc="rehearsal.absent_count || 0"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{rehearsal.status}">
<t t-esc="rehearsal.status ? rehearsal.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -60,6 +60,11 @@
</field> </field>
</record> </record>
<record id="action_performances_dashboard" model="ir.actions.client">
<field name="name">Performances Dashboard</field>
<field name="tag">dance_event.action_performances_dashboard</field>
</record>
<record id="action_dance_event_performances" model="ir.actions.act_window"> <record id="action_dance_event_performances" model="ir.actions.act_window">
<field name="name">Performances &amp; Costumes</field> <field name="name">Performances &amp; Costumes</field>
<field name="res_model">dance.event.performance</field> <field name="res_model">dance.event.performance</field>
@ -67,5 +72,5 @@
<field name="search_view_id" ref="view_dance_event_performance_search"/> <field name="search_view_id" ref="view_dance_event_performance_search"/>
</record> </record>
<menuitem id="menu_dance_event_performances" name="Performances &amp; Costumes" parent="menu_dance_event_root" action="action_dance_event_performances" sequence="20"/> <menuitem id="menu_dance_event_performances" name="Performance" parent="dance_student.menu_dance_root" action="action_performances_dashboard" sequence="100"/>
</odoo> </odoo>

View File

@ -108,6 +108,11 @@
</field> </field>
</record> </record>
<record id="action_events_dashboard" model="ir.actions.client">
<field name="name">Events Dashboard</field>
<field name="tag">dance_event.action_events_dashboard</field>
</record>
<record id="action_dance_events" model="ir.actions.act_window"> <record id="action_dance_events" model="ir.actions.act_window">
<field name="name">Events &amp; Recitals</field> <field name="name">Events &amp; Recitals</field>
<field name="res_model">dance.event</field> <field name="res_model">dance.event</field>
@ -115,6 +120,5 @@
<field name="search_view_id" ref="view_dance_event_search"/> <field name="search_view_id" ref="view_dance_event_search"/>
</record> </record>
<menuitem id="menu_dance_event_root" name="Events &amp; Recitals" parent="dance_student.menu_dance_root" sequence="35"/> <menuitem id="menu_dance_event_root" name="Events &amp; Recitals" parent="dance_student.menu_dance_root" action="action_events_dashboard" sequence="35"/>
<menuitem id="menu_dance_events" name="Events" parent="menu_dance_event_root" action="action_dance_events" sequence="10"/>
</odoo> </odoo>

View File

@ -89,6 +89,11 @@
</field> </field>
</record> </record>
<record id="action_rehearsals_dashboard" model="ir.actions.client">
<field name="name">Rehearsals Dashboard</field>
<field name="tag">dance_event.action_rehearsals_dashboard</field>
</record>
<record id="action_dance_rehearsals" model="ir.actions.act_window"> <record id="action_dance_rehearsals" model="ir.actions.act_window">
<field name="name">Rehearsals</field> <field name="name">Rehearsals</field>
<field name="res_model">dance.rehearsal</field> <field name="res_model">dance.rehearsal</field>
@ -96,5 +101,5 @@
<field name="search_view_id" ref="view_dance_rehearsal_search"/> <field name="search_view_id" ref="view_dance_rehearsal_search"/>
</record> </record>
<menuitem id="menu_dance_rehearsals" name="Rehearsals" parent="menu_dance_event_root" action="action_dance_rehearsals" sequence="30"/> <menuitem id="menu_dance_rehearsals" name="Rehearsals" parent="dance_student.menu_dance_root" action="action_rehearsals_dashboard" sequence="110"/>
</odoo> </odoo>

View File

@ -1,7 +1,7 @@
{ {
"name": "Dance Guardian Management", "name": "Dance Guardian Management",
"summary": "Guardian contacts and student linking for dance school portal", "summary": "Guardian contacts and student linking for dance school portal",
"version": "17.0.1.0.0", "version": "17.0.1.0.3",
"category": "Education", "category": "Education",
"author": "Metetroncube", "author": "Metetroncube",
"license": "LGPL-3", "license": "LGPL-3",
@ -9,6 +9,12 @@
"data": [ "data": [
"views/res_partner_guardian_views.xml", "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, "application": False,
"installable": True, "installable": True,
} }

View File

@ -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);

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_guardian.DanceGuardiansPage" owl="1">
<div class="stu-wrap">
<!-- Sidebar Area -->
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'guardians'"/>
</t>
<!-- Main Content Area -->
<div class="stu-main">
<!-- Top Actions Bar -->
<div class="stu-top-bar">
<div class="stu-top-title">Guardians Management</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search guardians..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createGuardian">
<i class="fa fa-plus"/> New Guardian
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadGuardians" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Guardians...</p>
</div>
<div t-elif="!state.guardians.length" class="stu-empty">
<i class="fa fa-users fa-4x"/>
<h3>No Guardians Found</h3>
<p>Try adjusting your search or create a new guardian.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createGuardian">Create Guardian</button>
</div>
<!-- Classic Table Structure -->
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Guardian Name</th>
<th>Phone</th>
<th>Mobile</th>
<th>Email</th>
<th>Linked Students</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.guardians" t-as="guardian" t-key="guardian.id" t-on-click="() => this.openGuardian(guardian.id)">
<td class="stu-td-name">
<div class="stu-table-avatar">
<i class="fa fa-user"/>
</div>
<span t-esc="guardian.name"/>
</td>
<td t-esc="guardian.phone || '-'"/>
<td t-esc="guardian.mobile || '-'"/>
<td t-esc="guardian.email || '-'"/>
<td>
<span class="stu-badge stu-badge-active" t-if="guardian.guardian_student_ids and guardian.guardian_student_ids.length > 0">
<t t-esc="guardian.guardian_student_ids.length"/> Student(s)
</span>
<span class="stu-badge stu-badge-hold" t-else="">
None
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -51,8 +51,88 @@
</field> </field>
</record> </record>
<record id="action_dance_guardians" model="ir.actions.act_window"> <record id="action_dance_guardians_dashboard" model="ir.actions.client">
<field name="name">Guardians</field> <field name="name">Guardians</field>
<field name="tag">dance_guardian.action_guardians_dashboard</field>
</record>
<!-- Dedicated Clean Dance Guardian Form View -->
<record id="view_dance_guardian_form" model="ir.ui.view">
<field name="name">dance.guardian.form</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<form string="Dance Guardian / Parent" class="o_dance_form_view">
<sheet>
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'avatar_128'}"/>
<div class="oe_title">
<span class="o_form_label text-muted fw-bold">PARENT / GUARDIAN NAME</span>
<h1>
<field name="name" default_focus="1" placeholder="e.g. Priya Sharma" required="1"/>
</h1>
<div class="d-flex align-items-center gap-2 mt-2">
<field name="is_guardian" invisible="1"/>
<field name="is_student" invisible="1"/>
</div>
</div>
<div class="o_dance_section_card mt-4">
<div class="o_dance_section_header">
<i class="fa fa-phone me-2"/> Contact Information &amp; Address
</div>
<group>
<group>
<field name="phone" widget="phone" required="1" placeholder="Primary phone..."/>
<field name="mobile" widget="phone" placeholder="Mobile / WhatsApp..."/>
<field name="email" widget="email" placeholder="Email address..."/>
</group>
<group>
<span class="o_form_label o_td_label fw-bold" name="address_name">
Residential Address
</span>
<div class="o_address_format">
<field name="street" placeholder="Street..." class="o_address_street"/>
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
<field name="city" placeholder="City" class="o_address_city"/>
<field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_quick_create': True}"/>
</div>
</group>
</group>
</div>
<notebook class="mt-4">
<page string="Linked Students" name="students_page">
<field name="guardian_student_ids">
<tree string="Linked Students" create="false" delete="false">
<field name="student_code"/>
<field name="name"/>
<field name="dance_style"/>
<field name="level"/>
<field name="student_status"/>
</tree>
</field>
</page>
<page string="Additional Linked Students" name="additional_students_page">
<field name="additional_student_ids" widget="many2many_tags" placeholder="Other related students..."/>
</page>
<page string="Internal Notes" name="internal_notes">
<field name="comment" placeholder="Administrative notes regarding this guardian..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Keep original action for references -->
<record id="action_dance_guardians" model="ir.actions.act_window">
<field name="name">Guardians List</field>
<field name="res_model">res.partner</field> <field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="domain">[('is_guardian', '=', True), ('is_student', '=', False)]</field> <field name="domain">[('is_guardian', '=', True), ('is_student', '=', False)]</field>
@ -60,7 +140,7 @@
<field name="search_view_id" ref="view_partner_guardian_search"/> <field name="search_view_id" ref="view_partner_guardian_search"/>
<field name="view_ids" eval="[(5, 0, 0), <field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('view_partner_guardian_tree')}), (0, 0, {'view_mode': 'tree', 'view_id': ref('view_partner_guardian_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('base.view_partner_form')})]"/> (0, 0, {'view_mode': 'form', 'view_id': ref('view_dance_guardian_form')})]"/>
</record> </record>
<record id="view_partner_student_guardian_domain_form" model="ir.ui.view"> <record id="view_partner_student_guardian_domain_form" model="ir.ui.view">
@ -79,5 +159,6 @@
</field> </field>
</record> </record>
<menuitem id="menu_dance_guardians" name="Guardians" parent="dance_student.menu_dance_root" action="action_dance_guardians" sequence="15"/> <menuitem id="menu_dance_guardians" name="Guardians" parent="dance_student.menu_dance_root" action="action_dance_guardians_dashboard" sequence="15"/>
</odoo> </odoo>
<!-- Forced hash update -->

View File

@ -13,4 +13,10 @@
], ],
"application": False, "application": False,
"installable": True, "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",
],
},
} }

View File

@ -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);

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_leave.DanceLeaveRequestsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'leave_requests'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Leave Requests</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search leaves..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createLeaveRequest">
<i class="fa fa-plus"/> New Request
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadLeaveRequests" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Leave Requests...</p>
</div>
<div t-elif="!state.leaveRequests.length" class="stu-empty">
<i class="fa fa-bed fa-4x"/>
<h3>No Leave Requests Found</h3>
<p>Try adjusting your search or create a new request.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createLeaveRequest">Create Request</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Student</th>
<th>Guardian</th>
<th>From Date</th>
<th>To Date</th>
<th>Status</th>
<th>Reviewed By</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.leaveRequests" t-as="req" t-key="req.id" t-on-click="() => this.openLeaveRequest(req.id)">
<td class="stu-td-name" t-esc="req.student_id ? req.student_id[1] : '-'"/>
<td t-esc="req.guardian_id ? req.guardian_id[1] : '-'"/>
<td t-esc="req.from_date || '-'"/>
<td t-esc="req.to_date || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{req.status}">
<t t-esc="req.status ? req.status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
<td t-esc="req.reviewed_by_id ? req.reviewed_by_id[1] : '-'"/>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -75,6 +75,11 @@
</field> </field>
</record> </record>
<record id="action_leave_requests_dashboard" model="ir.actions.client">
<field name="name">Leave Requests Dashboard</field>
<field name="tag">dance_leave.action_leave_requests_dashboard</field>
</record>
<record id="action_dance_leave_requests" model="ir.actions.act_window"> <record id="action_dance_leave_requests" model="ir.actions.act_window">
<field name="name">Leave Requests</field> <field name="name">Leave Requests</field>
<field name="res_model">dance.leave.request</field> <field name="res_model">dance.leave.request</field>
@ -82,5 +87,5 @@
<field name="search_view_id" ref="view_dance_leave_request_search"/> <field name="search_view_id" ref="view_dance_leave_request_search"/>
</record> </record>
<menuitem id="menu_dance_leave_requests" name="Leave Requests" parent="dance_student.menu_dance_root" action="action_dance_leave_requests" sequence="45"/> <menuitem id="menu_dance_leave_requests" name="Leave Requests" parent="dance_student.menu_dance_root" action="action_leave_requests_dashboard" sequence="45"/>
</odoo> </odoo>

View File

@ -5,33 +5,43 @@
<div t-if="request.params.get('leave_submitted')" class="alert alert-success"> <div t-if="request.params.get('leave_submitted')" class="alert alert-success">
Leave request submitted for review. Leave request submitted for review.
</div> </div>
<div class="mb-3"> <div class="mb-4 mt-2">
<a t-attf-href="/my/students/#{student.id}/leave" class="btn btn-primary">Request Leave</a> <a t-attf-href="/my/students/#{student.id}/leave" class="btn-gradient">
<i class="fa-regular fa-paper-plane"></i> Request Leave
</a>
</div> </div>
</xpath> </xpath>
<xpath expr="//h5[normalize-space()='Attendance']" position="before"> <xpath expr="//h3[hasclass('section-title') and normalize-space()='Attendance Details']" position="before">
<h5 class="mt-4">Leave Requests</h5> <h3 class="section-title">Leave Requests</h3>
<t t-call="portal.portal_table"> <div class="modern-table-container">
<thead> <div class="table-responsive">
<tr> <table class="table modern-table">
<th>From</th> <thead>
<th>To</th> <tr>
<th>Reason</th> <th>From</th>
<th>Status</th> <th>To</th>
</tr> <th>Reason</th>
</thead> <th>Status</th>
<tbody> </tr>
<tr t-if="not leave_requests"> </thead>
<td colspan="4" class="text-muted">No leave requests found.</td> <tbody>
</tr> <tr t-if="not leave_requests">
<tr t-foreach="leave_requests" t-as="leave"> <td colspan="4" class="text-muted">No leave requests found.</td>
<td><t t-out="leave.from_date"/></td> </tr>
<td><t t-out="leave.to_date"/></td> <tr t-foreach="leave_requests" t-as="leave">
<td><t t-out="leave.reason"/></td> <td class="text-dark fw-bold"><t t-out="leave.from_date"/></td>
<td><t t-out="leave.status"/></td> <td class="text-dark fw-bold"><t t-out="leave.to_date"/></td>
</tr> <td class="text-muted"><t t-out="leave.reason"/></td>
</tbody> <td>
</t> <span style="background: rgba(59, 130, 246, 0.1); color: #2563eb; padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">
<t t-out="leave.status"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</xpath> </xpath>
</template> </template>
@ -42,36 +52,44 @@
<t t-set="title">Request Leave</t> <t t-set="title">Request Leave</t>
</t> </t>
<div t-if="error" class="alert alert-danger"> <!-- Inject the dance portal styles for this page too -->
<t t-out="error"/> <t t-call="dance_portal.dance_portal_styles"/>
</div>
<form method="post" class="card"> <div class="dance-portal-wrapper">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> <div class="dance-shape-1"></div>
<div class="card-body"> <div class="dance-shape-2"></div>
<h5 class="card-title"> <div class="dance-content">
<t t-out="student.name"/> <div t-if="error" class="alert alert-danger glass-card border-0">
</h5> <t t-out="error"/>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" for="from_date">From Date</label>
<input id="from_date" name="from_date" type="date" class="form-control" required="required" t-att-value="post.get('from_date')"/>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="to_date">To Date</label>
<input id="to_date" name="to_date" type="date" class="form-control" required="required" t-att-value="post.get('to_date')"/>
</div>
<div class="col-12">
<label class="form-label" for="reason">Reason</label>
<textarea id="reason" name="reason" class="form-control" rows="4" required="required"><t t-out="post.get('reason') or ''"/></textarea>
</div>
</div> </div>
<form method="post" class="glass-card">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<h1 class="page-title mb-4">Request Leave for <t t-out="student.name"/></h1>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label text-muted fw-bold text-uppercase" style="font-size: 0.8rem; letter-spacing: 1px;" for="from_date">From Date</label>
<input id="from_date" name="from_date" type="date" class="form-control" style="background: rgba(255,255,255,0.5); border: 1px solid rgba(0,0,0,0.1); border-radius: 12px;" required="required" t-att-value="post.get('from_date')"/>
</div>
<div class="col-12 col-md-6">
<label class="form-label text-muted fw-bold text-uppercase" style="font-size: 0.8rem; letter-spacing: 1px;" for="to_date">To Date</label>
<input id="to_date" name="to_date" type="date" class="form-control" style="background: rgba(255,255,255,0.5); border: 1px solid rgba(0,0,0,0.1); border-radius: 12px;" required="required" t-att-value="post.get('to_date')"/>
</div>
<div class="col-12">
<label class="form-label text-muted fw-bold text-uppercase" style="font-size: 0.8rem; letter-spacing: 1px;" for="reason">Reason</label>
<textarea id="reason" name="reason" class="form-control" style="background: rgba(255,255,255,0.5); border: 1px solid rgba(0,0,0,0.1); border-radius: 12px;" rows="4" required="required"><t t-out="post.get('reason') or ''"/></textarea>
</div>
</div>
<div class="d-flex gap-3 mt-4 pt-3 border-top border-light">
<button type="submit" class="btn-gradient">Submit Request</button>
<a t-attf-href="/my/students/#{student.id}" class="btn text-muted fw-bold">Cancel</a>
</div>
</form>
</div> </div>
<div class="card-footer d-flex gap-2"> </div>
<button type="submit" class="btn btn-primary">Submit</button>
<a t-attf-href="/my/students/#{student.id}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</t> </t>
</template> </template>
</odoo> </odoo>

View File

@ -19,6 +19,12 @@
"dance_new_theme/static/src/css/contact_page.css", "dance_new_theme/static/src/css/contact_page.css",
"dance_new_theme/static/src/js/dance_new_theme.js", "dance_new_theme/static/src/js/dance_new_theme.js",
], ],
"web._assets_primary_variables": [
"dance_new_theme/static/src/scss/backend_variables.scss",
],
"web.assets_backend": [
"dance_new_theme/static/src/scss/backend.scss",
],
}, },
"installable": True, "installable": True,
"auto_install": False, "auto_install": False,

View File

@ -2,11 +2,6 @@ from odoo import http # pyrefly: ignore [missing-import]
from odoo.http import request # pyrefly: ignore [missing-import] from odoo.http import request # pyrefly: ignore [missing-import]
class RegistrationController(http.Controller):
@http.route('/registration', type='http', auth='public', website=True)
def registration_page(self, **post):
return request.render('dance_new_theme.registration_page_template')
class ContactController(http.Controller): class ContactController(http.Controller):
@http.route('/contact', type='http', auth='public', website=True) @http.route('/contact', type='http', auth='public', website=True)
@ -17,4 +12,45 @@ class ContactController(http.Controller):
class EventsController(http.Controller): class EventsController(http.Controller):
@http.route('/events', type='http', auth='public', website=True) @http.route('/events', type='http', auth='public', website=True)
def events_page(self, **post): def events_page(self, **post):
return request.render('dance_new_theme.events_page_template') events = request.env['dance.event'].sudo().search([('state', 'in', ['planning', 'rehearsal', 'done'])], order='date desc')
return request.render('dance_new_theme.events_page_template', {'events': events})
class RegistrationController(http.Controller):
@http.route('/registration', type='http', auth='public', website=True)
def registration_page(self, **post):
return request.render('dance_new_theme.registration_page_template')
@http.route('/registration/submit', type='http', auth='public', methods=['POST'], website=True, csrf=False)
def registration_submit(self, **post):
try:
# Create Guardian
guardian_vals = {
'name': post.get('parent_name'),
'phone': post.get('parent_phone'),
'email': post.get('parent_email'),
'mobile': post.get('parent_whatsapp'),
'street': post.get('parent_address'),
'city': post.get('parent_city'),
'zip': post.get('parent_postal_code'),
'is_student': False,
}
guardian = request.env['res.partner'].sudo().create(guardian_vals)
# Create Student
student_vals = {
'name': post.get('student_name'),
'is_student': True,
'date_of_birth': post.get('student_dob'),
'gender': post.get('student_gender').lower() if post.get('student_gender') else False,
'dance_style': post.get('student_dance_style').lower() if post.get('student_dance_style') else False,
'level': post.get('student_skill_level').lower() if post.get('student_skill_level') else False,
'medical_notes': post.get('student_medical_notes'),
'guardian_id': guardian.id,
'guardian_phone': guardian.phone,
}
student = request.env['res.partner'].sudo().create(student_vals)
return "success"
except Exception as e:
return request.make_response(str(e), status=400)

View File

@ -0,0 +1,427 @@
/* Admin Panel Custom Theme (Same as Website Theme) */
/* 1. Main Navbar Styling */
.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;
}
/* Ensure text and icons on the navbar are visible */
.o_navbar .o_nav_entry,
.o_navbar .dropdown-toggle,
.o_navbar .o_menu_brand,
.o_navbar .o_menu_sections .nav-link,
.o_navbar .o_menu_systray .nav-link,
.o_navbar i.fa,
.o_main_navbar .o_nav_entry,
.o_main_navbar .dropdown-toggle,
.o_main_navbar .o_menu_brand,
.o_main_navbar .o_menu_sections .nav-link,
.o_main_navbar .o_menu_systray .nav-link,
.o_main_navbar i.fa,
.o_main_navbar .nav-link {
color: rgba(255, 255, 255, 0.9) !important;
}
/* Hovered links on navbar */
.o_navbar .nav-link:hover,
.o_navbar .dropdown-toggle:hover,
.o_navbar .o_menu_brand:hover,
.o_main_navbar .nav-link:hover,
.o_main_navbar .dropdown-toggle:hover,
.o_main_navbar .o_menu_brand:hover,
.o_main_navbar .o_menu_sections .o_nav_entry:hover,
.o_main_navbar .o_menu_sections a:hover,
.o_main_navbar .o_menu_sections button:hover {
background-color: rgba(255, 255, 255, 0.15) !important;
color: #ffffff !important;
}
/* Active / Current Tab and Focused Tab on navbar - theme pill with crisp white text */
.o_main_navbar .o_menu_sections {
.o_nav_entry,
.nav-link,
a,
button {
background-color: transparent !important;
color: rgba(255, 255, 255, 0.9) !important;
&:hover {
background-color: rgba(255, 255, 255, 0.15) !important;
color: #ffffff !important;
}
&:focus,
&:active,
&.focus,
&.active,
&[aria-current="page"] {
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;
}
}
> .active,
> .nav-link.active,
> .o_nav_entry.active,
> a.active,
> button.active,
> [aria-current="page"],
> .dropdown.show > .dropdown-toggle,
> .dropdown > .dropdown-toggle.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;
}
}
/* 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;
}
}
/* 2. Logo placement in the admin panel */
/* Add logo only to the brand menu to avoid duplicates */
.o_main_navbar .o_menu_brand {
display: flex !important;
align-items: center !important;
}
.o_main_navbar .o_menu_brand::before {
content: "";
display: inline-block;
width: 80px;
height: 30px;
background-image: url('/dance_new_theme/static/src/img/new_logo.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center left;
margin-right: 15px;
}
/* 3. App Switcher (Home Menu) Background */
.o_home_menu_background {
background-color: #f8f1ff !important;
/* dc-lavender */
background-image: url('/dance_new_theme/static/src/img/hero_bg.png') !important;
background-size: cover !important;
background-position: center center !important;
background-blend-mode: overlay;
}
/* Home menu app icons text */
.o_home_menu .o_app .o_caption {
color: #171238 !important;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8) !important;
font-weight: 600 !important;
}
/* 4. Full Page Colorful Theme Backgrounds */
.o_web_client,
.o_content,
.o_view_controller {
background-color: #f8f1ff !important;
/* Lavender background */
}
/* Colorful Control Panel (Top Header area above lists/forms) */
.o_control_panel {
background: linear-gradient(135deg, #f8f1ff 0%, #e8d5fc 100%) !important;
border-bottom: 2px solid #872bd5 !important;
padding-bottom: 10px !important;
}
.o_breadcrumb .active {
color: #872bd5 !important;
font-weight: bold;
}
/* 5. Vibrant List Views (Tables) */
.o_list_view {
background-color: transparent !important;
}
.o_list_table {
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(135, 43, 213, 0.1);
margin-top: 10px;
}
/* Colorful Table Headers */
.o_list_table thead {
background: linear-gradient(to right, #872bd5, #6a1aab) !important;
color: #ffffff !important;
}
.o_list_table thead th {
color: #ffffff !important;
border-bottom: none !important;
font-weight: 600;
text-transform: uppercase;
padding: 12px 8px !important;
}
/* Table Rows Styling */
.o_list_table tbody tr {
transition: all 0.2s ease;
}
.o_list_table tbody tr:nth-child(even) {
background-color: rgba(248, 241, 255, 0.6) !important;
/* light lavender rows */
}
.o_list_table tbody tr:hover {
background-color: rgba(135, 43, 213, 0.15) !important;
transform: scale(1.002);
}
/* 6. Form Views (Cards) */
.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;
}
}
}
}
}
/* Form 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;
}
}
/* Inputs, selects, textareas */
.o_form_view input.o_input,
.o_form_view textarea.o_input,
.o_form_view select.o_input,
.o_form_view .o_field_widget input,
.o_form_view .o_field_widget textarea,
.o_form_view .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 */
.o_form_view input[type="radio"],
.o_form_view input[type="checkbox"] {
accent-color: #7434CA !important;
cursor: pointer;
}
/* Notebook Tabs in Forms */
.o_notebook .nav-tabs {
border-bottom: 2px solid #ede4f7 !important;
gap: 8px !important;
}
.o_notebook .nav-tabs .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;
}
}
/* Primary Buttons - Make them pop! */
.btn-primary {
background: linear-gradient(135deg, #7434CA 0%, #531C96 100%) !important;
border: none !important;
color: #ffffff !important;
box-shadow: 0 4px 10px rgba(116, 52, 202, 0.25) !important;
transition: all 0.3s ease !important;
border-radius: 6px !important;
}
.btn-primary:hover {
background: linear-gradient(135deg, #6a1aab, #4f0f85) !important;
box-shadow: 0 6px 15px rgba(135, 43, 213, 0.4) !important;
transform: translateY(-1px);
}
/* Search bar styling */
.o_searchview {
border: 2px solid #872bd5 !important;
border-radius: 20px !important;
background-color: #ffffff !important;
padding: 2px 15px !important;
box-shadow: 0 2px 8px rgba(135, 43, 213, 0.1) !important;
}
/* ── Global Notebook Tab White Text Contrast (All Form Pages) ── */
.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;
}
}

View File

@ -0,0 +1,11 @@
// backend_variables.scss
$o-brand-odoo: #872bd5;
$o-brand-primary: #872bd5;
$o-brand-secondary: #171238;
$primary: #872bd5;
$secondary: #171238;
$info: #f8f1ff;
// General Bootstrap variable overrides
$body-bg: #f8f1ff;
$body-color: #171238;

View File

@ -388,91 +388,43 @@
<!-- Events Grid --> <!-- Events Grid -->
<section class="events-grid"> <section class="events-grid">
<t t-foreach="events" t-as="event">
<!-- Card 1 --> <div class="event-card">
<div class="event-card"> <div class="event-image-wrapper">
<div class="event-image-wrapper"> <t t-if="event.event_type == 'competition'">
<span class="event-badge badge-competition">Competition</span> <span class="event-badge badge-competition">Competition</span>
<img src="https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&amp;fit=crop&amp;w=600&amp;q=80" alt="Winter Showcase"/> </t>
</div> <t t-elif="event.event_type == 'workshop'">
<div class="event-card-body"> <span class="event-badge badge-workshop">Workshop</span>
<h3 class="event-card-title">Winter Showcase 2026</h3> </t>
<div class="event-meta"> <t t-elif="event.event_type == 'recital'">
<i class="fa fa-calendar"></i> 15 Jan 2026 • Friday <span class="event-badge badge-recital">Recital</span>
</t>
<t t-else="">
<span class="event-badge badge-annual">Annual Day</span>
</t>
<img src="https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&amp;fit=crop&amp;w=600&amp;q=80" alt="Event Cover"/>
</div> </div>
<div class="event-meta"> <div class="event-card-body">
<i class="fa fa-map-marker"></i> Chennai Auditorium <h3 class="event-card-title"><t t-esc="event.name"/></h3>
<div class="event-meta">
<i class="fa fa-calendar"></i> <t t-esc="event.date"/>
</div>
<div class="event-meta">
<i class="fa fa-map-marker"></i> <t t-esc="event.venue or 'Venue TBA'"/>
</div>
<p class="event-desc">
<t t-esc="event.notes or 'Join us for this exciting dance event.'"/>
</p>
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
</div> </div>
<p class="event-desc">
An exciting inter-academy competition featuring various dance styles.
</p>
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
</div> </div>
</div> </t>
<t t-if="not events">
<!-- Card 2 --> <div class="col-12 text-center py-5">
<div class="event-card"> <h4>No upcoming events found.</h4>
<div class="event-image-wrapper">
<span class="event-badge badge-workshop">Workshop</span>
<img src="https://images.unsplash.com/photo-1547153760-18fc86324498?auto=format&amp;fit=crop&amp;w=600&amp;q=80" alt="Masterclass"/>
</div> </div>
<div class="event-card-body"> </t>
<h3 class="event-card-title">Contemporary Dance Masterclass</h3>
<div class="event-meta">
<i class="fa fa-calendar"></i> 22 Feb 2026 • Sunday
</div>
<div class="event-meta">
<i class="fa fa-map-marker"></i> Studio Hall A
</div>
<p class="event-desc">
A specialized workshop to enhance your contemporary dance techniques.
</p>
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
</div>
</div>
<!-- Card 3 -->
<div class="event-card">
<div class="event-image-wrapper">
<span class="event-badge badge-recital">Recital</span>
<img src="https://images.unsplash.com/photo-1623018035782-b269248d2899?auto=format&amp;fit=crop&amp;w=600&amp;q=80" alt="Recital"/>
</div>
<div class="event-card-body">
<h3 class="event-card-title">Bharatanatyam Arangetram</h3>
<div class="event-meta">
<i class="fa fa-calendar"></i> 05 Mar 2026 • Thursday
</div>
<div class="event-meta">
<i class="fa fa-map-marker"></i> Nritya Main Stage
</div>
<p class="event-desc">
A graceful solo recital presentation by our senior student.
</p>
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
</div>
</div>
<!-- Card 4 -->
<div class="event-card">
<div class="event-image-wrapper">
<span class="event-badge badge-annual">Annual Day</span>
<img src="https://images.unsplash.com/photo-1514306191717-452ec28c7814?auto=format&amp;fit=crop&amp;w=600&amp;q=80" alt="Annual Day"/>
</div>
<div class="event-card-body">
<h3 class="event-card-title">Annual Day Celebration</h3>
<div class="event-meta">
<i class="fa fa-calendar"></i> 25 Apr 2026 • Saturday
</div>
<div class="event-meta">
<i class="fa fa-map-marker"></i> Grand Convention Hall
</div>
<p class="event-desc">
A grand celebration showcasing the talent and dedication of our dancers.
</p>
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
</div>
</div>
</section> </section>
</div> </div>

View File

@ -1046,10 +1046,10 @@
<div class="input-with-icon"> <div class="input-with-icon">
<i class="fa fa-users left-icon"></i> <i class="fa fa-users left-icon"></i>
<select id="student_gender" class="form-select"> <select id="student_gender" class="form-select">
<option selected="1">Select gender</option> <option selected="1" value="">Select gender</option>
<option>Male</option> <option value="male">Male</option>
<option>Female</option> <option value="female">Female</option>
<option>Other</option> <option value="other">Other</option>
</select> </select>
</div> </div>
</div> </div>
@ -1077,10 +1077,12 @@
<div class="input-with-icon"> <div class="input-with-icon">
<i class="fa fa-male left-icon"></i> <i class="fa fa-male left-icon"></i>
<select id="student_dance_style" class="form-select"> <select id="student_dance_style" class="form-select">
<option selected="1">Select dance style</option> <option selected="1" value="">Select dance style</option>
<option>Ballet</option> <option value="bharatanatyam">Bharatanatyam</option>
<option>Hip Hop</option> <option value="kathak">Kathak</option>
<option>Contemporary</option> <option value="hiphop">Hip-Hop</option>
<option value="contemporary">Contemporary</option>
<option value="zumba">Zumba</option>
</select> </select>
</div> </div>
</div> </div>
@ -1089,10 +1091,10 @@
<div class="input-with-icon"> <div class="input-with-icon">
<i class="fa fa-bar-chart left-icon"></i> <i class="fa fa-bar-chart left-icon"></i>
<select id="student_skill_level" class="form-select"> <select id="student_skill_level" class="form-select">
<option selected="1">Select level</option> <option selected="1" value="">Select level</option>
<option>Beginner</option> <option value="beginner">Beginner</option>
<option>Intermediate</option> <option value="intermediate">Intermediate</option>
<option>Advanced</option> <option value="advanced">Advanced</option>
</select> </select>
</div> </div>
</div> </div>
@ -1580,7 +1582,7 @@
<div class="form-actions mt-4 pt-3 border-top"> <div class="form-actions mt-4 pt-3 border-top">
<button type="button" class="btn btn-back" onclick="showStep(3)"><i class="fa fa-arrow-left"></i> Back: Program Selection</button> <button type="button" class="btn btn-back" onclick="showStep(3)"><i class="fa fa-arrow-left"></i> Back: Program Selection</button>
<button type="submit" class="btn btn-next">Submit Registration <i class="fa fa-arrow-right"></i></button> <button type="button" class="btn btn-next" onclick="submitRegistration()">Submit Registration <i class="fa fa-arrow-right"></i></button>
</div> </div>
</div> </div>
</div> </div>
@ -2079,6 +2081,49 @@
showStep(nextStep); showStep(nextStep);
} }
function submitRegistration() {
var btn = document.querySelector('.btn-next[onclick="submitRegistration()"]');
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Submitting...';
btn.disabled = true;
var data = new FormData();
data.append('student_name', document.getElementById('student_name').value);
data.append('student_dob', document.getElementById('student_dob').value);
data.append('student_gender', document.getElementById('student_gender').value);
data.append('student_dance_style', document.getElementById('student_dance_style').value);
data.append('student_skill_level', document.getElementById('student_skill_level').value);
data.append('student_medical_notes', document.getElementById('student_medical_notes').value);
data.append('parent_name', document.getElementById('parent_name').value);
data.append('parent_relationship', document.getElementById('parent_relationship').value);
data.append('parent_phone', document.getElementById('parent_phone').value);
data.append('parent_email', document.getElementById('parent_email').value);
data.append('parent_whatsapp', document.getElementById('parent_whatsapp').value);
data.append('parent_address', document.getElementById('parent_address').value);
data.append('parent_city', document.getElementById('parent_city').value);
data.append('parent_postal_code', document.getElementById('parent_postal_code').value);
fetch('/registration/submit', {
method: 'POST',
body: data
})
.then(response => {
if (response.ok) {
window.location.href = '/';
} else {
response.text().then(text => {
alert('Registration failed: ' + text);
btn.innerHTML = 'Submit Registration <i class="fa fa-arrow-right"></i>';
btn.disabled = false;
});
}
})
.catch(error => {
console.error('Error:', error);
alert('Registration failed. Please try again.');
btn.innerHTML = 'Submit Registration <i class="fa fa-arrow-right"></i>';
btn.disabled = false;
});
}
]]> ]]>
</script> </script>
</t> </t>

View File

@ -3348,105 +3348,44 @@
</div> </div>
<div class="dc-timeline-list"> <div class="dc-timeline-list">
<!-- Event 1 --> <t t-foreach="request.env['dance.event'].sudo().search([('state', 'in', ['planning', 'rehearsal', 'done'])], order='date desc', limit=4)" t-as="event">
<div class="dc-event-item"> <div class="dc-event-item">
<div class="dc-event-dot"></div> <div class="dc-event-dot"></div>
<div class="dc-event-card"> <div class="dc-event-card">
<div class="dc-event-card-date"> <div class="dc-event-card-date">
<span class="dc-month">SEP</span> <span class="dc-month"><t t-esc="event.date.strftime('%b').upper()" t-if="event.date"/></span>
<span class="dc-day">12</span> <span class="dc-day"><t t-esc="event.date.strftime('%d')" t-if="event.date"/></span>
</div>
<div class="dc-event-icon-wrap">
<i class="fa fa-trophy"/>
</div>
<div class="dc-event-info">
<h4>Inter Academy Dance Competition</h4>
<div class="dc-event-location">
<i class="fa fa-map-marker"/>
<span>Madurai Convention Hall</span>
</div> </div>
<span class="dc-event-badge dc-badge-purple">Registration Open</span> <div class="dc-event-icon-wrap">
</div> <t t-if="event.event_type == 'competition'">
<a href="#" class="dc-event-action"> <i class="fa fa-trophy"/>
<i class="fa fa-arrow-right"/> </t>
</a> <t t-elif="event.event_type == 'workshop'">
</div> <i class="fa fa-television"/>
</div> </t>
<t t-elif="event.event_type == 'recital'">
<!-- Event 2 --> <i class="fa fa-child"/>
<div class="dc-event-item"> </t>
<div class="dc-event-dot"></div> <t t-else="">
<div class="dc-event-card"> <i class="fa fa-calendar-o"/>
<div class="dc-event-card-date"> </t>
<span class="dc-month">OCT</span>
<span class="dc-day">05</span>
</div>
<div class="dc-event-icon-wrap">
<i class="fa fa-child"/>
</div>
<div class="dc-event-info">
<h4>Bharatanatyam Arangetram</h4>
<div class="dc-event-location">
<i class="fa fa-map-marker"/>
<span>Nritya Main Stage</span>
</div> </div>
<span class="dc-event-badge dc-badge-orange">Limited Seats</span> <div class="dc-event-info">
</div> <h4><t t-esc="event.name"/></h4>
<a href="#" class="dc-event-action"> <div class="dc-event-location">
<i class="fa fa-arrow-right"/> <i class="fa fa-map-marker"/>
</a> <span><t t-esc="event.venue or 'Venue TBA'"/></span>
</div> </div>
</div> <span class="dc-event-badge dc-badge-purple" t-if="event.state == 'planning'">Planning</span>
<span class="dc-event-badge dc-badge-orange" t-elif="event.state == 'rehearsal'">Rehearsals</span>
<!-- Event 3 --> <span class="dc-event-badge dc-badge-green" t-else="">Completed</span>
<div class="dc-event-item">
<div class="dc-event-dot"></div>
<div class="dc-event-card">
<div class="dc-event-card-date">
<span class="dc-month">DEC</span>
<span class="dc-day">18</span>
</div>
<div class="dc-event-icon-wrap">
<i class="fa fa-users"/>
</div>
<div class="dc-event-info">
<h4>Winter Cultural Festival</h4>
<div class="dc-event-location">
<i class="fa fa-map-marker"/>
<span>Open Air Theatre</span>
</div> </div>
<span class="dc-event-badge dc-badge-green">Family Event</span> <a href="/events" class="dc-event-action">
<i class="fa fa-arrow-right"/>
</a>
</div> </div>
<a href="#" class="dc-event-action">
<i class="fa fa-arrow-right"/>
</a>
</div> </div>
</div> </t>
<!-- Event 4 -->
<div class="dc-event-item">
<div class="dc-event-dot"></div>
<div class="dc-event-card">
<div class="dc-event-card-date">
<span class="dc-month">JAN</span>
<span class="dc-day">10</span>
</div>
<div class="dc-event-icon-wrap">
<i class="fa fa-smile-o"/>
</div>
<div class="dc-event-info">
<h4>New Year Dance Gala</h4>
<div class="dc-event-location">
<i class="fa fa-map-marker"/>
<span>Chennai Auditorium</span>
</div>
<span class="dc-event-badge dc-badge-blue">All Dance Styles</span>
</div>
<a href="#" class="dc-event-action">
<i class="fa fa-arrow-right"/>
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,4 +14,12 @@
], ],
"application": False, "application": False,
"installable": True, "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",
],
},
} }

View File

@ -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);

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_performance.DanceGradeCardsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'grade_cards'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Grade Cards</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search grades..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createGradeCard">
<i class="fa fa-plus"/> New Grade Card
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadGradeCards" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Grade Cards...</p>
</div>
<div t-elif="!state.gradeCards.length" class="stu-empty">
<i class="fa fa-graduation-cap fa-4x"/>
<h3>No Grade Cards Found</h3>
<p>Try adjusting your search or create a new grade card.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createGradeCard">Create Grade Card</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Assessment Date</th>
<th>Student</th>
<th>Batch</th>
<th>Instructor</th>
<th>Total Score</th>
<th>Total Max Score</th>
<th>Percentage</th>
<th>Grade</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.gradeCards" t-as="record" t-key="record.id" t-on-click="() => this.openGradeCard(record.id)">
<td t-esc="record.assessment_date || '-'"/>
<td class="stu-td-name" t-esc="record.student_id ? record.student_id[1] : '-'"/>
<td t-esc="record.batch_id ? record.batch_id[1] : '-'"/>
<td t-esc="record.instructor_id ? record.instructor_id[1] : '-'"/>
<td t-esc="record.total_score != undefined ? record.total_score.toFixed(2) : '-'"/>
<td t-esc="record.total_max_score != undefined ? record.total_max_score.toFixed(2) : '-'"/>
<td t-esc="record.percentage != undefined ? record.percentage.toFixed(2) : '-'"/>
<td t-esc="record.grade || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{record.state}">
<t t-esc="record.state ? (record.state.charAt(0).toUpperCase() + record.state.slice(1)) : 'Unknown'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -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);

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_performance.DancePerformanceCriteriaPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'performance_criteria'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Performance Criteria</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search criteria..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createCriterion">
<i class="fa fa-plus"/> New Criterion
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadCriteria" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Criteria...</p>
</div>
<div t-elif="!state.criteria.length" class="stu-empty">
<i class="fa fa-list-ol fa-4x"/>
<h3>No Performance Criteria Found</h3>
<p>Try adjusting your search or create a new criterion.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createCriterion">Create Criterion</button>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Name</th>
<th>Max Score</th>
<th>Active</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.criteria" t-as="record" t-key="record.id" t-on-click="() => this.openCriterion(record.id)">
<td class="stu-td-name">
<i class="fa fa-arrows-v" style="color:#aaa;margin-right:8px;"/>
<t t-esc="record.name || '-'"/>
</td>
<td t-esc="record.max_score != undefined ? record.max_score.toFixed(2) : '-'"/>
<td>
<input type="checkbox" t-att-checked="record.active" disabled="1"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -134,6 +134,16 @@
</field> </field>
</record> </record>
<record id="action_grade_cards_dashboard" model="ir.actions.client">
<field name="name">Grade Cards Dashboard</field>
<field name="tag">dance_performance.action_grade_cards_dashboard</field>
</record>
<record id="action_performance_criteria_dashboard" model="ir.actions.client">
<field name="name">Performance Criteria Dashboard</field>
<field name="tag">dance_performance.action_performance_criteria_dashboard</field>
</record>
<record id="action_dance_performance_records" model="ir.actions.act_window"> <record id="action_dance_performance_records" model="ir.actions.act_window">
<field name="name">Grade Cards</field> <field name="name">Grade Cards</field>
<field name="res_model">dance.performance.record</field> <field name="res_model">dance.performance.record</field>
@ -149,6 +159,6 @@
</record> </record>
<menuitem id="menu_dance_performance_root" name="Performance" parent="dance_student.menu_dance_root" sequence="50"/> <menuitem id="menu_dance_performance_root" name="Performance" parent="dance_student.menu_dance_root" sequence="50"/>
<menuitem id="menu_dance_grade_cards" name="Grade Cards" parent="menu_dance_performance_root" action="action_dance_performance_records" sequence="10"/> <menuitem id="menu_dance_grade_cards" name="Grade Cards" parent="menu_dance_performance_root" action="action_grade_cards_dashboard" sequence="10"/>
<menuitem id="menu_dance_performance_criteria" name="Criteria" parent="menu_dance_performance_root" action="action_dance_performance_criteria" sequence="20"/> <menuitem id="menu_dance_performance_criteria" name="Criteria" parent="menu_dance_performance_root" action="action_performance_criteria_dashboard" sequence="20"/>
</odoo> </odoo>

View File

@ -1,35 +1,43 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<template id="portal_my_student_detail_performance" name="My Student Performance" inherit_id="dance_portal.portal_my_student_detail"> <template id="portal_my_student_detail_performance" name="My Student Performance" inherit_id="dance_portal.portal_my_student_detail">
<xpath expr="//h5[normalize-space()='Fees']" position="before"> <xpath expr="//h3[hasclass('section-title') and normalize-space()='Fees / Invoices']" position="before">
<h5 class="mt-4">Performance</h5> <h3 class="section-title">Performance</h3>
<t t-call="portal.portal_table"> <div class="modern-table-container">
<thead> <div class="table-responsive">
<tr> <table class="table modern-table">
<th>Date</th> <thead>
<th>Batch</th> <tr>
<th class="text-end">Score</th> <th>Date</th>
<th class="text-end">Percentage</th> <th>Batch</th>
<th>Grade</th> <th class="text-end">Score</th>
</tr> <th class="text-end">Percentage</th>
</thead> <th>Grade</th>
<tbody> </tr>
<tr t-if="not performance_records"> </thead>
<td colspan="5" class="text-muted">No shared grade cards found.</td> <tbody>
</tr> <tr t-if="not performance_records">
<tr t-foreach="performance_records" t-as="record"> <td colspan="5" class="text-muted">No shared grade cards found.</td>
<td><t t-out="record.assessment_date"/></td> </tr>
<td><t t-out="record.batch_id.name"/></td> <tr t-foreach="performance_records" t-as="record">
<td class="text-end"> <td class="text-dark fw-bold"><t t-out="record.assessment_date"/></td>
<t t-out="record.total_score"/> / <t t-out="record.total_max_score"/> <td><t t-out="record.batch_id.name"/></td>
</td> <td class="text-end text-muted">
<td class="text-end"> <span class="fw-bold text-dark"><t t-out="record.total_score"/></span> / <t t-out="record.total_max_score"/>
<t t-out="'%.2f' % record.percentage"/>% </td>
</td> <td class="text-end fw-bold" style="color: var(--primary-dark);">
<td><t t-out="record.grade_label"/></td> <t t-out="'%.2f' % record.percentage"/>%
</tr> </td>
</tbody> <td>
</t> <span style="background: rgba(140, 82, 255, 0.15); color: var(--primary-dark); padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">
<t t-out="record.grade_label"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</xpath> </xpath>
</template> </template>
</odoo> </odoo>

View File

@ -19,7 +19,7 @@ class DanceCustomerPortal(CustomerPortal):
("is_student", "=", True), ("is_student", "=", True),
"|", "|",
("guardian_id", "=", partner.id), ("guardian_id", "=", partner.id),
("guardian_ids", "in", partner.id), ("guardian_ids", "in", [partner.id]),
] ]
def _get_portal_student(self, student_id): def _get_portal_student(self, student_id):

View File

@ -1,5 +1,250 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Custom CSS and Styles for Dance Portal -->
<template id="dance_portal_styles" name="Dance Portal Styles">
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;0,700;1,500;1,600&amp;family=Outfit:wght@300;400;500;600&amp;display=swap');
:root {
--primary: #8c52ff;
--primary-light: #ebd4fb;
--primary-dark: #6b21a8;
--dark-bg: #111625;
--text-main: #2d3748;
--text-muted: #4a5568;
--glass-bg: rgba(255, 255, 255, 0.65);
--glass-border: rgba(255, 255, 255, 0.4);
--shadow-soft: 0 10px 40px -10px rgba(0,0,0,0.15);
--shadow-hover: 0 15px 50px -10px rgba(140, 82, 255, 0.4);
}
/* FULL WIDTH BACKGROUND OVERRIDE FOR PORTAL */
body, #wrapwrap, main {
background: linear-gradient(120deg, var(--primary-light), var(--primary), #d4a5ff) !important;
background-size: 200% 200% !important;
animation: gradientMove 12s ease infinite !important;
font-family: 'Outfit', sans-serif !important;
color: var(--text-main) !important;
}
.o_portal_wrap {
background: transparent !important;
}
/* NAVBAR DESIGN */
header, .o_header_standard, nav.navbar {
background: rgba(255, 255, 255, 0.65) !important;
backdrop-filter: blur(20px) !important;
-webkit-backdrop-filter: blur(20px) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 30px rgba(0,0,0,0.05) !important;
}
/* Typography */
h1, h2, h3, h4, h5, .page-title, .section-title, .o_portal_my_home h3 {
font-family: 'Cormorant Garamond', serif !important;
}
.page-title {
font-weight: 700;
font-size: 3.2rem;
color: var(--dark-bg);
margin-bottom: 0;
font-style: italic;
text-shadow: 2px 2px 4px rgba(255,255,255,0.6);
}
.section-title {
font-weight: 700;
font-size: 1.8rem;
margin-bottom: 20px;
color: var(--dark-bg);
border-left: 4px solid var(--primary);
padding-left: 12px;
margin-top: 2rem;
}
/* GLASS CARDS - Applying to custom elements and Odoo default elements */
.glass-card,
.o_portal_my_home .o_portal_docs a,
.o_portal_my_home .list-group-item,
.o_portal_my_home .o_portal_my_details .card {
background: var(--glass-bg) !important;
backdrop-filter: blur(12px) !important;
-webkit-backdrop-filter: blur(12px) !important;
border: 1px solid var(--glass-border) !important;
border-radius: 20px 4px 20px 4px !important;
padding: 24px;
box-shadow: var(--shadow-soft) !important;
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1) !important;
position: relative;
overflow: hidden;
margin-bottom: 1rem;
}
.glass-card:hover,
.o_portal_my_home .o_portal_docs a:hover,
.o_portal_my_home .list-group-item:hover,
.o_portal_my_home .o_portal_my_details .card:hover {
transform: translateY(-8px) !important;
border-radius: 4px 20px 4px 20px !important;
box-shadow: var(--shadow-hover) !important;
background: rgba(255, 255, 255, 0.85) !important;
}
/* Hide Odoo's default borders and bg on list groups */
.list-group-item {
border-left: 1px solid var(--glass-border) !important;
border-right: 1px solid var(--glass-border) !important;
margin-bottom: 15px !important;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
border: 1px solid rgba(0,0,0,0.05);
}
.icon-purple { background: rgba(140, 82, 255, 0.1); color: var(--primary); }
.icon-orange { background: rgba(249, 115, 22, 0.1); color: #f97316; }
.icon-blue { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.stat-value {
font-family: 'Cormorant Garamond', serif;
font-size: 3.5rem;
font-weight: 700;
color: var(--primary-dark);
line-height: 1;
margin: 10px 0;
font-style: italic;
}
.status-badge {
padding: 6px 14px;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-active {
background-color: rgba(34, 197, 94, 0.1);
color: #16a34a;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.status-pulse {
width: 8px;
height: 8px;
background-color: #22c55e;
border-radius: 50%;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
animation: pulse 2s infinite;
}
.modern-table-container {
background: var(--glass-bg);
border-radius: 15px;
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-soft);
overflow: hidden;
margin-bottom: 20px;
}
.modern-table {
margin-bottom: 0;
width: 100%;
}
.modern-table thead {
background: rgba(0,0,0,0.02);
border-bottom: 2px solid var(--primary-light);
}
.modern-table th {
font-family: 'Outfit', sans-serif;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
font-weight: 600;
padding: 16px 24px;
border-bottom: none;
}
.modern-table td {
padding: 16px 24px;
vertical-align: middle;
font-weight: 500;
color: var(--text-main);
border-bottom: 1px solid rgba(0,0,0,0.03);
font-size: 0.95rem;
}
.modern-table tbody tr {
transition: all 0.3s;
background: transparent;
}
.modern-table tbody tr:hover {
background: rgba(140, 82, 255, 0.08) !important;
transform: scale(1.01);
}
@keyframes gradientMove {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }
70% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
@keyframes danceFloat {
0% { transform: translate(0, 0) rotate(0deg) scale(1); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; }
50% { transform: translate(50px, 100px) rotate(180deg) scale(1.1); border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
100% { transform: translate(-50px, 50px) rotate(360deg) scale(0.9); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; }
}
.dance-shape-1, .dance-shape-2 {
position: fixed;
filter: blur(80px);
z-index: -1;
opacity: 0.5;
animation: danceFloat 20s infinite alternate ease-in-out;
pointer-events: none;
}
.dance-shape-1 {
width: 600px;
height: 600px;
background: rgba(255, 255, 255, 0.4);
top: -100px;
left: -100px;
border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%;
animation-duration: 25s;
}
.dance-shape-2 {
width: 700px;
height: 700px;
background: var(--primary-light);
bottom: -150px;
right: -100px;
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
animation-duration: 18s;
animation-direction: alternate-reverse;
}
</style>
<!-- Inject Shapes globally -->
<div class="dance-shape-1"></div>
<div class="dance-shape-2"></div>
</template>
<!-- Inject Styles into the Main Portal Home Page -->
<template id="portal_my_home_dance_portal_styles" inherit_id="portal.portal_my_home" priority="1">
<xpath expr="//div[hasclass('o_portal_my_home')]" position="before">
<t t-call="dance_portal.dance_portal_styles"/>
</xpath>
</template>
<template id="portal_my_home_dance_students" name="Show Dance Students" inherit_id="portal.portal_my_home" customize_show="True" priority="35"> <template id="portal_my_home_dance_students" name="Show Dance Students" inherit_id="portal.portal_my_home" customize_show="True" priority="35">
<xpath expr="//div[hasclass('o_portal_docs')]" position="before"> <xpath expr="//div[hasclass('o_portal_docs')]" position="before">
<t t-set="portal_service_category_enable" t-value="True"/> <t t-set="portal_service_category_enable" t-value="True"/>
@ -34,41 +279,59 @@
<t t-set="title">My Students</t> <t t-set="title">My Students</t>
</t> </t>
<div t-if="not students" class="alert alert-info"> <t t-call="dance_portal.dance_portal_styles"/>
No students are linked to your portal account yet.
</div>
<div t-if="students" class="row g-3"> <div class="mt-4 mb-5">
<div t-foreach="students" t-as="student" class="col-12 col-md-6"> <h1 class="page-title mb-4">My Students</h1>
<a t-attf-href="/my/students/#{student.id}" class="text-decoration-none text-reset">
<div class="card h-100"> <div t-if="not students" class="alert alert-info glass-card border-0">
<div class="card-body"> No students are linked to your portal account yet.
<div class="d-flex justify-content-between gap-3"> </div>
<div>
<h5 class="card-title mb-1"> <div t-if="students" class="row g-4">
<t t-out="student.name"/> <div t-foreach="students" t-as="student" class="col-12 col-md-6">
</h5> <a t-attf-href="/my/students/#{student.id}" class="text-decoration-none text-reset">
<div class="text-muted small" t-if="student.student_code"> <div class="glass-card h-100">
<t t-out="student.student_code"/> <div class="d-flex justify-content-between align-items-start mb-4">
<div class="d-flex align-items-center gap-3">
<div class="stat-icon icon-purple">
<i class="fa-solid fa-user-graduate"></i>
</div>
<div>
<h4 class="fw-bold mb-0" style="color: var(--dark-bg); font-size: 1.3rem; font-family: 'Outfit', sans-serif;">
<t t-out="student.name"/>
</h4>
<span class="text-muted" style="font-family: monospace; font-size: 0.9rem;">
<t t-out="student.student_code"/>
</span>
</div> </div>
</div> </div>
<span class="badge text-bg-light align-self-start"> <div class="status-badge status-active">
<t t-out="student.student_status or 'Student'"/> <span class="status-pulse"></span> <t t-out="student.student_status or 'Student'"/>
</span> </div>
</div> </div>
<div class="row mt-3 small">
<div class="row g-3 mt-2">
<div class="col-6"> <div class="col-6">
<div class="text-muted">Dance Style</div> <div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<strong><t t-out="student.dance_style or '-'"/></strong> <div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Dance Style</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">
<t t-out="student.dance_style or '-'"/>
</div>
</div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="text-muted">Level</div> <div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<strong><t t-out="student.level or '-'"/></strong> <div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Level</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">
<t t-out="student.level or '-'"/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </a>
</a> </div>
</div> </div>
</div> </div>
</t> </t>
@ -81,204 +344,284 @@
<t t-set="title" t-value="student.name"/> <t t-set="title" t-value="student.name"/>
</t> </t>
<div class="row g-3 mb-4"> <t t-call="dance_portal.dance_portal_styles"/>
<div class="col-12 col-lg-4">
<div class="card h-100"> <div class="mt-4 mb-5">
<div class="card-body"> <h1 class="page-title mb-5">Student Dashboard</h1>
<h5 class="card-title">Student</h5>
<dl class="row mb-0"> <div class="row g-4 mb-5">
<dt class="col-5">Code</dt> <!-- Profile Card -->
<dd class="col-7"><t t-out="student.student_code or '-'"/></dd> <div class="col-12 col-lg-4">
<dt class="col-5">Status</dt> <div class="glass-card">
<dd class="col-7"><t t-out="student.student_status or '-'"/></dd> <div class="d-flex justify-content-between align-items-start mb-4">
<dt class="col-5">Style</dt> <div class="d-flex align-items-center gap-3">
<dd class="col-7"><t t-out="student.dance_style or '-'"/></dd> <div class="stat-icon icon-purple">
<dt class="col-5">Level</dt> <i class="fa-solid fa-user-graduate"></i>
<dd class="col-7"><t t-out="student.level or '-'"/></dd> </div>
</dl> <div>
</div> <h4 class="fw-bold mb-0" style="color: var(--dark-bg); font-size: 1.3rem; font-family: 'Outfit', sans-serif;">
</div> <t t-out="student.name"/>
</div> </h4>
<div class="col-12 col-lg-4"> <span class="text-muted" style="font-family: monospace; font-size: 0.9rem;">
<div class="card h-100"> <t t-out="student.student_code or '-'"/>
<div class="card-body"> </span>
<h5 class="card-title">Attendance</h5> </div>
<div class="display-6"> </div>
<t t-out="attendance_percent"/>% <div class="status-badge status-active">
<span class="status-pulse"></span> <t t-out="student.student_status or '-'"/>
</div>
</div> </div>
<div class="text-muted">
<t t-out="len(attendance)"/> sessions, <t t-out="absent_count"/> absences <div class="row g-3 mt-2">
<div class="col-6">
<div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Dance Style</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">
<t t-out="student.dance_style or '-'"/>
</div>
</div>
</div>
<div class="col-6">
<div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Level</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">
<t t-out="student.level or '-'"/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="col-12 col-lg-4"> <!-- Attendance Card -->
<div class="card h-100"> <div class="col-12 col-md-6 col-lg-4">
<div class="card-body"> <div class="glass-card">
<h5 class="card-title">Fees</h5> <div class="d-flex justify-content-between align-items-start">
<div class="display-6"> <h5 class="fw-bold" style="color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Outfit', sans-serif;">Attendance</h5>
<t t-out="len(invoices)"/> <div class="stat-icon icon-orange" style="width: 36px; height: 36px; font-size: 1rem;">
<i class="fa-solid fa-calendar-check"></i>
</div>
</div> </div>
<div class="text-muted">student invoices</div> <div class="stat-value"><t t-out="attendance_percent"/>%</div>
<div class="progress-custom">
<div class="progress-bar-custom" t-attf-style="width: #{attendance_percent}%;"></div>
</div>
<div class="d-flex gap-3 text-muted" style="font-size: 0.85rem; font-weight: 500;">
<span><i class="fa-solid fa-circle-check" style="color: #22c55e;"></i> <t t-out="len(attendance)"/> Session(s)</span>
<span><i class="fa-solid fa-circle-xmark" style="color: #ef4444;"></i> <t t-out="absent_count"/> Absence(s)</span>
</div>
</div>
</div>
<!-- Fees Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="glass-card d-flex flex-column">
<div class="d-flex justify-content-between align-items-start">
<h5 class="fw-bold" style="color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Outfit', sans-serif;">Fees Overview</h5>
<div class="stat-icon icon-blue" style="width: 36px; height: 36px; font-size: 1rem;">
<i class="fa-solid fa-file-invoice-dollar"></i>
</div>
</div>
<div class="stat-value"><t t-out="len(invoices)"/></div>
<p class="text-muted mt-2 mb-4" style="font-size: 0.9rem;">
Student invoices pending or generated.
</p>
</div> </div>
</div> </div>
</div> </div>
<h3 class="section-title">Enrolled Batches</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Batch</th>
<th>Status</th>
<th>Enrollment Date</th>
</tr>
</thead>
<tbody>
<tr t-if="not enrollments">
<td colspan="3" class="text-muted">No enrollments found.</td>
</tr>
<tr t-foreach="enrollments" t-as="enrollment">
<td class="fw-bold text-dark"><t t-out="enrollment.batch_id.name"/></td>
<td>
<span style="background: rgba(34, 197, 94, 0.1); color: #16a34a; padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">
<t t-out="enrollment.status"/>
</span>
</td>
<td class="text-muted"><i class="fa-regular fa-calendar me-2"></i><t t-out="enrollment.enrollment_date"/></td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 class="section-title">Attendance Details</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Date</th>
<th>Batch</th>
<th>Session</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not attendance">
<td colspan="4" class="text-muted">No attendance records found.</td>
</tr>
<tr t-foreach="attendance" t-as="line">
<td class="text-dark fw-bold"><t t-out="line.date"/></td>
<td><t t-out="line.batch_id.name"/></td>
<td><t t-out="line.session_id.name"/></td>
<td><t t-out="line.status"/></td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 class="section-title">Upcoming Sessions</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Batch</th>
<th>Room</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_sessions">
<td colspan="5" class="text-muted">No upcoming sessions found.</td>
</tr>
<tr t-foreach="upcoming_sessions" t-as="session">
<td class="text-dark fw-bold"><t t-out="session.date"/></td>
<td>
<t t-if="session.start_time"><t t-out="session.start_time"/></t>
<t t-if="session.end_time"> - <t t-out="session.end_time"/></t>
</td>
<td><t t-out="session.batch_id.name"/></td>
<td><t t-out="session.room or '-'"/></td>
<td><t t-out="session.status"/></td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 class="section-title">Upcoming Events</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Event</th>
<th>Date</th>
<th>Venue</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_events">
<td colspan="4" class="text-muted">No upcoming events found.</td>
</tr>
<tr t-foreach="upcoming_events" t-as="performance">
<td class="text-dark fw-bold"><t t-out="performance.event_id.name"/></td>
<td><t t-out="performance.event_id.date"/></td>
<td><t t-out="performance.event_id.venue or '-'"/></td>
<td><t t-out="performance.role"/></td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 class="section-title">Upcoming Rehearsals</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Event</th>
<th>Room</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_rehearsals">
<td colspan="5" class="text-muted">No upcoming rehearsals found.</td>
</tr>
<tr t-foreach="upcoming_rehearsals" t-as="rehearsal">
<td class="text-dark fw-bold"><t t-out="rehearsal.date"/></td>
<td><t t-out="rehearsal.display_time"/></td>
<td><t t-out="rehearsal.event_id.name"/></td>
<td><t t-out="rehearsal.room or '-'"/></td>
<td><t t-out="rehearsal.status"/></td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 class="section-title">Fees / Invoices</h3>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Invoice</th>
<th>Date</th>
<th>Batch</th>
<th class="text-end">Amount</th>
<th class="text-end">Due</th>
<th>Status</th>
<th/>
</tr>
</thead>
<tbody>
<tr t-if="not invoices">
<td colspan="7" class="text-muted">No invoices found.</td>
</tr>
<tr t-foreach="invoices" t-as="invoice">
<td class="text-dark fw-bold"><t t-out="invoice.name"/></td>
<td><t t-out="invoice.invoice_date"/></td>
<td><t t-out="invoice.dance_batch_id.name or '-'"/></td>
<td class="text-end fw-bold" style="color: var(--primary-dark);">
<span t-field="invoice.amount_total" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
</td>
<td class="text-end text-danger fw-bold">
<span t-field="invoice.amount_residual" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
</td>
<td>
<span style="background: rgba(59, 130, 246, 0.1); color: #2563eb; padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">
<t t-out="invoice.payment_state"/>
</span>
</td>
<td class="text-end">
<a t-att-href="invoice.get_portal_url()" style="color: var(--primary-dark); font-weight: 700; text-decoration: none; font-size: 0.85rem; text-transform: uppercase;">
Open <i class="fa-solid fa-arrow-right ms-1"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>
<h5>Batches</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Batch</th>
<th>Status</th>
<th>Enrollment Date</th>
</tr>
</thead>
<tbody>
<tr t-if="not enrollments">
<td colspan="3" class="text-muted">No enrollments found.</td>
</tr>
<tr t-foreach="enrollments" t-as="enrollment">
<td><t t-out="enrollment.batch_id.name"/></td>
<td><t t-out="enrollment.status"/></td>
<td><t t-out="enrollment.enrollment_date"/></td>
</tr>
</tbody>
</t>
<h5 class="mt-4">Attendance</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Date</th>
<th>Batch</th>
<th>Session</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not attendance">
<td colspan="4" class="text-muted">No attendance records found.</td>
</tr>
<tr t-foreach="attendance" t-as="line">
<td><t t-out="line.date"/></td>
<td><t t-out="line.batch_id.name"/></td>
<td><t t-out="line.session_id.name"/></td>
<td><t t-out="line.status"/></td>
</tr>
</tbody>
</t>
<h5 class="mt-4">Upcoming Sessions</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Batch</th>
<th>Room</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_sessions">
<td colspan="5" class="text-muted">No upcoming sessions found.</td>
</tr>
<tr t-foreach="upcoming_sessions" t-as="session">
<td><t t-out="session.date"/></td>
<td>
<t t-if="session.start_time"><t t-out="session.start_time"/></t>
<t t-if="session.end_time"> - <t t-out="session.end_time"/></t>
</td>
<td><t t-out="session.batch_id.name"/></td>
<td><t t-out="session.room or '-'"/></td>
<td><t t-out="session.status"/></td>
</tr>
</tbody>
</t>
<h5 class="mt-4">Upcoming Events</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Event</th>
<th>Date</th>
<th>Venue</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_events">
<td colspan="4" class="text-muted">No upcoming events found.</td>
</tr>
<tr t-foreach="upcoming_events" t-as="performance">
<td><t t-out="performance.event_id.name"/></td>
<td><t t-out="performance.event_id.date"/></td>
<td><t t-out="performance.event_id.venue or '-'"/></td>
<td><t t-out="performance.role"/></td>
</tr>
</tbody>
</t>
<h5 class="mt-4">Upcoming Rehearsals</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Event</th>
<th>Room</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-if="not upcoming_rehearsals">
<td colspan="5" class="text-muted">No upcoming rehearsals found.</td>
</tr>
<tr t-foreach="upcoming_rehearsals" t-as="rehearsal">
<td><t t-out="rehearsal.date"/></td>
<td><t t-out="rehearsal.display_time"/></td>
<td><t t-out="rehearsal.event_id.name"/></td>
<td><t t-out="rehearsal.room or '-'"/></td>
<td><t t-out="rehearsal.status"/></td>
</tr>
</tbody>
</t>
<h5 class="mt-4">Fees</h5>
<t t-call="portal.portal_table">
<thead>
<tr>
<th>Invoice</th>
<th>Date</th>
<th>Batch</th>
<th class="text-end">Amount</th>
<th class="text-end">Due</th>
<th>Status</th>
<th/>
</tr>
</thead>
<tbody>
<tr t-if="not invoices">
<td colspan="7" class="text-muted">No invoices found.</td>
</tr>
<tr t-foreach="invoices" t-as="invoice">
<td><t t-out="invoice.name"/></td>
<td><t t-out="invoice.invoice_date"/></td>
<td><t t-out="invoice.dance_batch_id.name or '-'"/></td>
<td class="text-end">
<span t-field="invoice.amount_total" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
</td>
<td class="text-end">
<span t-field="invoice.amount_residual" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
</td>
<td><t t-out="invoice.payment_state"/></td>
<td class="text-end">
<a t-att-href="invoice.get_portal_url()" class="btn btn-sm btn-outline-primary">Open</a>
</td>
</tr>
</tbody>
</t>
</t> </t>
</template> </template>
</odoo> </odoo>

View File

@ -3,10 +3,11 @@
// Brand: #875A7B (mauve-purple) | accent: #5e3d57 // Brand: #875A7B (mauve-purple) | accent: #5e3d57
// ── Top nav bar ──────────────────────────────────────────────────────────── // ── Top nav bar ────────────────────────────────────────────────────────────
.o_navbar,
.o_main_navbar { .o_main_navbar {
background: linear-gradient(135deg, #875A7B 0%, #5e3d57 100%) !important; background: linear-gradient(135deg, #1B132C 0%, #2C1A4D 50%, #3B1E63 100%) !important;
border-bottom: none !important; border-bottom: 2px solid #7434CA !important;
box-shadow: 0 2px 12px rgba(135, 90, 123, 0.3) !important; box-shadow: 0 2px 12px rgba(27, 19, 44, 0.4) !important;
.o_menu_brand { .o_menu_brand {
color: #fff !important; color: #fff !important;
@ -14,9 +15,12 @@
letter-spacing: -0.2px !important; letter-spacing: -0.2px !important;
} }
/* Top navbar main links */
.o_nav_entry, .o_nav_entry,
.o_menu_sections > .o_nav_entry > a, .o_menu_sections > .nav-link,
.o_menu_sections .o_dropdown_menu .o_nav_entry > a { .o_menu_sections > .dropdown > .dropdown-toggle,
.o_menu_sections > a,
.o_menu_sections > button {
color: rgba(255, 255, 255, 0.9) !important; color: rgba(255, 255, 255, 0.9) !important;
&:hover { &:hover {
@ -25,12 +29,54 @@
} }
} }
// Active breadcrumb / current app highlight /* Top navbar dropdown menus (Performance dropdown, etc.) */
.o_menu_sections .o_nav_entry.active > a, .dropdown-menu {
.o_menu_sections .o_nav_entry > a.active { background-color: #ffffff !important;
color: #fff !important; border: 1px solid #ede4f7 !important;
background: rgba(255, 255, 255, 0.2) !important; box-shadow: 0 10px 30px rgba(27, 19, 44, 0.18) !important;
border-radius: 6px; 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 // Icons and caret
@ -175,10 +221,24 @@
} }
// Notebook tabs // Notebook tabs
.o_notebook .nav-tabs .nav-link.active { .o_notebook .nav-tabs .nav-link.active,
color: #875A7B !important; .nav-tabs .nav-link.active {
border-bottom-color: #875A7B !important; background: linear-gradient(135deg, #7434CA 0%, #5B1FA8 100%) !important;
font-weight: 600; 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;
}
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "Dance Student Management", "name": "Dance Student Management",
"summary": "Student profile fields on contacts for dance school ERP", "summary": "Student profile fields on contacts for dance school ERP",
"version": "17.0.1.0.0", "version": "17.0.1.0.5",
"category": "Education", "category": "Education",
"author": "Metetroncube", "author": "Metetroncube",
"license": "LGPL-3", "license": "LGPL-3",
@ -10,6 +10,13 @@
"data/student_sequence.xml", "data/student_sequence.xml",
"views/res_partner_student_views.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, "application": True,
"installable": True, "installable": True,
} }

View File

@ -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);

View File

@ -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;
}
}

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<!-- Shared Sidebar Template -->
<t t-name="dance_student.Sidebar">
<div class="stu-sidebar">
<div class="stu-sidebar-brand">
<span class="stu-brand-icon">💃</span>
<div class="stu-brand-text">Dance ERP</div>
</div>
<div class="stu-sidebar-nav">
<div t-attf-class="stu-nav-item #{activeTab == 'dashboard' ? 'active' : ''}" t-on-click="() => this.navigate('dashboard')">
<i class="fa fa-bar-chart"/> Dashboard
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'students' ? 'active' : ''}" t-on-click="() => this.navigate('students')">
<i class="fa fa-graduation-cap"/> Students
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'guardians' ? 'active' : ''}" t-on-click="() => this.navigate('guardians')">
<i class="fa fa-users"/> Guardians
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'batches' ? 'active' : ''}" t-on-click="() => this.navigate('batches')">
<i class="fa fa-cubes"/> Batches
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'enrollments' ? 'active' : ''}" t-on-click="() => this.navigate('enrollments')">
<i class="fa fa-check-circle"/> Enrollments
</div>
<div class="stu-nav-group">
<div class="stu-nav-group-title">Events &amp; Productions</div>
<div t-attf-class="stu-nav-subitem #{activeTab == 'events' ? 'active' : ''}" t-on-click="() => this.navigate('events')">
<i class="fa fa-star"/> Events &amp; Recitals
</div>
<div t-attf-class="stu-nav-subitem #{activeTab == 'performance' ? 'active' : ''}" t-on-click="() => this.navigate('performance')">
<i class="fa fa-magic"/> Performances &amp; Costumes
</div>
<div t-attf-class="stu-nav-subitem #{activeTab == 'rehearsals' ? 'active' : ''}" t-on-click="() => this.navigate('rehearsals')">
<i class="fa fa-clock-o"/> Rehearsals
</div>
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'enrollment_requests' ? 'active' : ''}" t-on-click="() => this.navigate('enrollment_requests')">
<i class="fa fa-envelope-open-o"/> Enrollment Requests
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'sessions' ? 'active' : ''}" t-on-click="() => this.navigate('sessions')">
<i class="fa fa-calendar"/> Sessions
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'leave_requests' ? 'active' : ''}" t-on-click="() => this.navigate('leave_requests')">
<i class="fa fa-bed"/> Leave Requests
</div>
<div t-attf-class="stu-nav-item #{activeTab == 'attendance' ? 'active' : ''}" t-on-click="() => this.navigate('attendance')">
<i class="fa fa-check-square-o"/> Attendance
</div>
<div class="stu-nav-group">
<div class="stu-nav-group-title">PERFORMANCE</div>
<div t-attf-class="stu-nav-subitem #{activeTab == 'grade_cards' ? 'active' : ''}" t-on-click="() => this.navigate('grade_cards')">
<i class="fa fa-graduation-cap"/> Grade Cards
</div>
<div t-attf-class="stu-nav-subitem #{activeTab == 'performance_criteria' ? 'active' : ''}" t-on-click="() => this.navigate('performance_criteria')">
<i class="fa fa-list-ol"/> Criteria
</div>
</div>
</div>
</div>
</t>
<t t-name="dance_student.DanceStudentsPage" owl="1">
<div class="stu-wrap">
<!-- Sidebar Area -->
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'students'"/>
</t>
<!-- Main Content Area -->
<div class="stu-main">
<!-- Top Actions Bar -->
<div class="stu-top-bar">
<div class="stu-top-title">Students Management</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search students..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-primary" t-on-click="createStudent">
<i class="fa fa-plus"/> New Student
</button>
<button class="stu-btn stu-btn-secondary" t-on-click="loadStudents" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Students...</p>
</div>
<div t-elif="!state.students.length" class="stu-empty">
<i class="fa fa-user-times fa-4x"/>
<h3>No Students Found</h3>
<p>Try adjusting your search or create a new student.</p>
<button class="stu-btn stu-btn-primary" t-on-click="createStudent">Create Student</button>
</div>
<!-- Classic Table Structure -->
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>ID</th>
<th>Student Name</th>
<th>Phone</th>
<th>Dance Style</th>
<th>Level</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.students" t-as="student" t-key="student.id" t-on-click="() => this.openStudent(student.id)">
<td class="stu-td-id" t-esc="student.student_code || '-'"/>
<td class="stu-td-name">
<div class="stu-table-avatar">
<img t-if="student.image_1920" t-attf-src="data:image/png;base64,#{student.image_1920}" alt="Avatar"/>
<div t-else="" class="stu-avatar-placeholder">
<i class="fa fa-user"/>
</div>
</div>
<span t-esc="student.name"/>
</td>
<td t-esc="student.phone || '-'"/>
<td style="text-transform: capitalize;" t-esc="student.dance_style || '-'"/>
<td style="text-transform: capitalize;" t-esc="student.level || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{student.student_status}">
<t t-esc="student.student_status ? student.student_status.toUpperCase() : 'UNKNOWN'"/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -77,8 +77,118 @@
</field> </field>
</record> </record>
<record id="action_dance_students" model="ir.actions.act_window"> <!-- Custom OWL Client Action for Students Page -->
<record id="action_dance_students_dashboard" model="ir.actions.client">
<field name="name">Students</field> <field name="name">Students</field>
<field name="tag">dance_student.action_students_dashboard</field>
</record>
<!-- Dedicated Clean Dance Student Form View -->
<record id="view_dance_student_form" model="ir.ui.view">
<field name="name">dance.student.form</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<form string="Dance Student" class="o_dance_form_view">
<header>
<field name="student_status" widget="statusbar" statusbar_visible="trial,active,hold,alumni" options="{'clickable': '1'}"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'avatar_128'}"/>
<div class="oe_title">
<span class="o_form_label text-muted fw-bold">STUDENT FULL NAME</span>
<h1>
<field name="name" default_focus="1" placeholder="e.g. Brandon Freeman" required="1"/>
</h1>
<div class="d-flex align-items-center gap-2 mt-2">
<span class="badge rounded-pill bg-purple-soft text-purple fw-bold px-3 py-1" invisible="not student_code">
<field name="student_code" readonly="1"/>
</span>
<field name="is_student" invisible="1"/>
</div>
</div>
<div class="o_dance_section_card mt-4">
<div class="o_dance_section_header">
<i class="fa fa-music me-2"/> Dance Academy Profile
</div>
<group>
<group>
<field name="dance_style" widget="radio" options="{'horizontal': true}"/>
<field name="level"/>
</group>
<group>
<field name="join_date"/>
</group>
</group>
</div>
<div class="o_dance_section_card mt-4">
<div class="o_dance_section_header">
<i class="fa fa-users me-2"/> Guardian &amp; Emergency Contact
</div>
<group>
<group>
<field name="guardian_id" placeholder="Select or create primary guardian..."/>
<field name="guardian_phone" widget="phone"/>
</group>
<group>
<field name="guardian_ids" widget="many2many_tags" placeholder="Additional guardians..."/>
<field name="sibling_ids" widget="many2many_tags" placeholder="Linked siblings..."/>
</group>
</group>
</div>
<div class="o_dance_section_card mt-4">
<div class="o_dance_section_header">
<i class="fa fa-user me-2"/> Personal Details &amp; Address
</div>
<group>
<group>
<field name="date_of_birth"/>
<field name="age" readonly="1"/>
<field name="gender" widget="radio" options="{'horizontal': true}"/>
<field name="phone" widget="phone"/>
<field name="email" widget="email"/>
</group>
<group>
<span class="o_form_label o_td_label fw-bold" name="address_name">
Residential Address
</span>
<div class="o_address_format">
<field name="street" placeholder="Street..." class="o_address_street"/>
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
<field name="city" placeholder="City" class="o_address_city"/>
<field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_quick_create': True}"/>
</div>
</group>
</group>
</div>
<notebook class="mt-4">
<page string="Medical &amp; Care Notes" name="medical_page">
<field name="medical_notes" placeholder="Injuries, allergies, health notes, or other important care instructions for instructors..."/>
</page>
<page string="Internal Notes" name="internal_notes">
<field name="comment" placeholder="Internal remarks, enrollment feedback, etc..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Keep the act_window around in case it's needed for other links, but update the menu to use the dashboard -->
<record id="action_dance_students" model="ir.actions.act_window">
<field name="name">Students List</field>
<field name="res_model">res.partner</field> <field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="domain">[('is_student', '=', True)]</field> <field name="domain">[('is_student', '=', True)]</field>
@ -86,9 +196,9 @@
<field name="search_view_id" ref="view_partner_student_search"/> <field name="search_view_id" ref="view_partner_student_search"/>
<field name="view_ids" eval="[(5, 0, 0), <field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('view_partner_student_tree')}), (0, 0, {'view_mode': 'tree', 'view_id': ref('view_partner_student_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('base.view_partner_form')})]"/> (0, 0, {'view_mode': 'form', 'view_id': ref('view_dance_student_form')})]"/>
</record> </record>
<menuitem id="menu_dance_root" name="Dance ERP" sequence="10"/> <menuitem id="menu_dance_root" name="Dance ERP" sequence="10"/>
<menuitem id="menu_dance_students" name="Students" parent="menu_dance_root" action="action_dance_students" sequence="10"/> <menuitem id="menu_dance_students" name="Students" parent="menu_dance_root" action="action_dance_students_dashboard" sequence="10"/>
</odoo> </odoo>

View File

@ -23,6 +23,10 @@
"web.assets_frontend": [ "web.assets_frontend": [
"dance_web/static/src/css/dance_web.css", "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, "installable": True,
"application": False, "application": False,

View File

@ -30,7 +30,7 @@ class ResUsers(models.Model):
("is_student", "=", True), ("is_student", "=", True),
"|", "|",
("guardian_id", "=", partner.id), ("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(): if self._dance_is_teacher():
return "/teacher" return "/teacher"
if self._dance_is_parent(): if self._dance_is_parent():
return "/parent" return "/my/home"
return False return False

View File

@ -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);

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="dance_web.DanceEnrollmentRequestsPage" owl="1">
<div class="stu-wrap">
<t t-call="dance_student.Sidebar">
<t t-set="activeTab" t-value="'enrollment_requests'"/>
</t>
<div class="stu-main">
<div class="stu-top-bar">
<div class="stu-top-title">Enrollment Requests</div>
<div class="stu-header-actions">
<div class="stu-search">
<i class="fa fa-search" t-on-click="onSearch"/>
<input type="text" placeholder="Search requests..."
t-model="state.searchQuery"
t-on-keyup="onSearch"/>
</div>
<button class="stu-btn stu-btn-secondary" t-on-click="loadRequests" title="Refresh">
<i t-attf-class="fa fa-refresh #{state.loading ? 'fa-spin' : ''}"/>
</button>
</div>
</div>
<div class="stu-content">
<div t-if="state.loading" class="stu-loading">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p>Loading Requests...</p>
</div>
<div t-elif="!state.requests.length" class="stu-empty">
<i class="fa fa-envelope-open-o fa-4x"/>
<h3>No Enrollment Requests Found</h3>
<p>No new requests match your search.</p>
</div>
<div t-else="" class="stu-table-card">
<table class="stu-table">
<thead>
<tr>
<th>Reference</th>
<th>Student Name</th>
<th>Parent / Guardian</th>
<th>Phone</th>
<th>Dance Style</th>
<th>Preferred Batch</th>
<th>Created On</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr t-foreach="state.requests" t-as="req" t-key="req.id" t-on-click="() => this.openRequest(req.id)">
<td class="stu-td-id" t-esc="req.reference || '-'"/>
<td t-esc="req.student_name || '-'"/>
<td t-esc="req.guardian_name || '-'"/>
<td t-esc="req.guardian_phone || '-'"/>
<td style="text-transform: capitalize;" t-esc="req.dance_style || '-'"/>
<td t-esc="req.batch_id ? req.batch_id[1] : '-'"/>
<td t-esc="req.create_date || '-'"/>
<td>
<span t-attf-class="stu-badge stu-badge-#{req.state}">
<t t-if="req.state == 'new'">New Request</t>
<t t-elif="req.state == 'contacted'">Contacted</t>
<t t-elif="req.state == 'approved'">Approved</t>
<t t-elif="req.state == 'converted'">Converted</t>
<t t-elif="req.state == 'rejected'">Rejected</t>
<t t-else="">UNKNOWN</t>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@ -120,6 +120,11 @@
</field> </field>
</record> </record>
<record id="action_enrollment_requests_dashboard" model="ir.actions.client">
<field name="name">Enrollment Requests Dashboard</field>
<field name="tag">dance_web.action_enrollment_requests_dashboard</field>
</record>
<record id="action_dance_enrollment_request" model="ir.actions.act_window"> <record id="action_dance_enrollment_request" model="ir.actions.act_window">
<field name="name">Enrollment Requests</field> <field name="name">Enrollment Requests</field>
<field name="res_model">dance.enrollment.request</field> <field name="res_model">dance.enrollment.request</field>
@ -134,7 +139,7 @@
<menuitem id="menu_dance_enrollment_requests" <menuitem id="menu_dance_enrollment_requests"
name="Enrollment Requests" name="Enrollment Requests"
parent="dance_student.menu_dance_root" parent="dance_student.menu_dance_root"
action="action_dance_enrollment_request" action="action_enrollment_requests_dashboard"
sequence="35"/> sequence="35"/>
</odoo> </odoo>

628
parent_portal_design.html Normal file
View File

@ -0,0 +1,628 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Parent Portal - Dance Vibe</title>
<!-- Google Fonts for Elegant Dance Vibe -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;0,700;1,500;1,600&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- Bootstrap 5 for Layout -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- FontAwesome for Icons -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #8c52ff;
--primary-light: #ebd4fb;
--primary-dark: #6b21a8;
--dark-bg: #111625;
--text-main: #2d3748;
--text-muted: #4a5568;
--glass-bg: rgba(255, 255, 255, 0.85);
--glass-border: rgba(255, 255, 255, 0.6);
--shadow-soft: 0 10px 40px -10px rgba(0,0,0,0.15);
--shadow-hover: 0 15px 50px -10px rgba(140, 82, 255, 0.4);
}
/* ----- ANIMATED DANCE BACKGROUND ----- */
body {
font-family: 'Outfit', sans-serif; /* Modern clean body */
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
background: linear-gradient(120deg, var(--primary-light), var(--primary), #d4a5ff);
background-size: 200% 200%;
animation: gradientMove 12s ease infinite;
position: relative;
overflow-x: hidden;
}
@keyframes gradientMove {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Abstract Dancing Ribbons / Fluid Shapes in background */
.dance-shape-1, .dance-shape-2 {
position: absolute;
filter: blur(60px);
z-index: -1;
opacity: 0.6;
animation: danceFloat 20s infinite alternate ease-in-out;
}
.dance-shape-1 {
width: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.4);
top: -100px;
left: -100px;
border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; /* Organic fluid shape */
animation-duration: 25s;
}
.dance-shape-2 {
width: 500px;
height: 500px;
background: var(--primary-light);
bottom: -150px;
right: -100px;
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
animation-duration: 18s;
animation-direction: alternate-reverse;
}
@keyframes danceFloat {
0% { transform: translate(0, 0) rotate(0deg) scale(1); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; }
50% { transform: translate(50px, 100px) rotate(180deg) scale(1.1); border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
100% { transform: translate(-50px, 50px) rotate(360deg) scale(0.9); border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%; }
}
/* ----- TYPOGRAPHY (ELEGANT/CLASSIC) ----- */
h1, h2, h3, h4, h5, h6, .brand-logo, .page-title, .section-title {
/* Cormorant Garamond gives a very elegant, theatrical, graceful feel */
font-family: 'Cormorant Garamond', serif;
}
/* ----- NAVBAR ----- */
.modern-navbar {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(255,255,255,0.3);
padding: 15px 0;
box-shadow: 0 4px 30px rgba(0,0,0,0.05);
position: sticky;
top: 0;
z-index: 1000;
}
.brand-logo {
font-weight: 700;
font-size: 2rem;
color: var(--primary-dark);
text-decoration: none;
display: flex;
align-items: center;
gap: 12px;
font-style: italic; /* Adds movement to the logo */
}
.brand-icon {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
width: 46px;
height: 46px;
border-radius: 50% 50% 50% 10%; /* Subtle dancer shoe/pointe shape */
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
box-shadow: 0 4px 15px rgba(140, 82, 255, 0.3);
transform: rotate(-15deg); /* Slight tilt for dynamic feel */
}
.nav-link-custom {
color: var(--text-muted);
font-weight: 500;
transition: color 0.3s;
margin-right: 20px;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.85rem;
}
.nav-link-custom:hover {
color: var(--primary);
}
/* ----- BUTTONS ----- */
.btn-gradient {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
border-radius: 50px;
padding: 10px 28px;
font-family: 'Outfit', sans-serif;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.85rem;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Bouncy rhythmic hover */
box-shadow: 0 4px 15px rgba(140, 82, 255, 0.3);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-gradient:hover {
transform: translateY(-4px) scale(1.05); /* Dance-like bounce */
box-shadow: 0 10px 25px rgba(140, 82, 255, 0.5);
color: white;
}
/* ----- GLASS CARDS ----- */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 20px 4px 20px 4px; /* Rhythmic asymmetrical corners */
padding: 24px;
box-shadow: var(--shadow-soft);
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
height: 100%;
position: relative;
overflow: hidden;
}
.glass-card:hover {
transform: translateY(-8px);
border-radius: 4px 20px 4px 20px; /* Shift corners on hover for a dancing effect */
box-shadow: var(--shadow-hover);
}
/* Card decorative blob */
.glass-card::before {
content: '';
position: absolute;
top: -30px;
right: -30px;
width: 120px;
height: 120px;
background: linear-gradient(135deg, transparent, var(--primary-light));
opacity: 0.5;
border-radius: 50%;
transition: transform 0.8s ease;
}
.glass-card:hover::before {
transform: scale(1.8) rotate(45deg);
}
/* ----- TYPOGRAPHY & ELEMENTS ----- */
.page-title {
font-weight: 700;
font-size: 3.2rem;
color: var(--dark-bg);
margin-bottom: 0;
font-style: italic;
text-shadow: 2px 2px 4px rgba(255,255,255,0.6);
}
.section-title {
font-weight: 700;
font-size: 1.8rem;
margin-bottom: 20px;
color: var(--dark-bg);
border-left: 4px solid var(--primary);
padding-left: 12px;
}
.breadcrumb-custom {
font-size: 0.85rem;
font-weight: 500;
color: rgba(0,0,0,0.6);
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.breadcrumb-custom a {
color: rgba(0,0,0,0.6);
text-decoration: none;
transition: color 0.2s;
}
.breadcrumb-custom a:hover {
color: var(--primary-dark);
}
.breadcrumb-custom i {
font-size: 0.7rem;
margin: 0 8px;
opacity: 0.5;
}
/* ----- STATUS BADGES ----- */
.status-badge {
padding: 6px 14px;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-active {
background-color: rgba(34, 197, 94, 0.1);
color: #16a34a;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.status-pulse {
width: 8px;
height: 8px;
background-color: #22c55e;
border-radius: 50%;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }
70% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
/* ----- TABLES ----- */
.modern-table-container {
background: var(--glass-bg);
border-radius: 15px;
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.modern-table {
margin-bottom: 0;
width: 100%;
}
.modern-table thead {
background: rgba(0,0,0,0.02);
border-bottom: 2px solid var(--primary-light);
}
.modern-table th {
font-family: 'Outfit', sans-serif;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
font-weight: 600;
padding: 16px 24px;
}
.modern-table td {
padding: 16px 24px;
vertical-align: middle;
font-weight: 500;
color: var(--text-main);
border-bottom: 1px solid rgba(0,0,0,0.03);
font-size: 0.95rem;
}
.modern-table tbody tr {
transition: all 0.3s;
}
.modern-table tbody tr:hover {
background: rgba(140, 82, 255, 0.04);
transform: scale(1.01); /* Rhythmic table row scale on hover */
}
.modern-table tbody tr:last-child td {
border-bottom: none;
}
/* ----- FOOTER ----- */
.modern-footer {
background-color: var(--dark-bg);
color: white;
margin-top: auto;
padding: 60px 0 30px;
position: relative;
z-index: 10;
}
.footer-heading {
font-family: 'Cormorant Garamond', serif;
font-weight: 700;
font-size: 1.4rem;
margin-bottom: 20px;
color: white;
font-style: italic;
}
/* Stats Styling */
.stat-value {
font-family: 'Cormorant Garamond', serif;
font-size: 3.5rem;
font-weight: 700;
color: var(--primary-dark);
line-height: 1;
margin: 10px 0;
font-style: italic;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
border: 1px solid rgba(0,0,0,0.05);
}
.icon-orange { background: rgba(249, 115, 22, 0.1); color: #f97316; }
.icon-blue { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.icon-purple { background: rgba(140, 82, 255, 0.1); color: var(--primary); }
.progress-custom {
height: 6px;
border-radius: 10px;
background-color: rgba(0,0,0,0.05);
margin: 15px 0;
overflow: hidden;
}
.progress-bar-custom {
height: 100%;
background: linear-gradient(90deg, #f97316, #fb923c);
border-radius: 10px;
}
</style>
</head>
<body>
<!-- Fluid animated shapes for dance vibe -->
<div class="dance-shape-1"></div>
<div class="dance-shape-2"></div>
<!-- NAVBAR -->
<nav class="modern-navbar">
<div class="container-xl d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a href="#" class="brand-logo me-5">
<div class="brand-icon">
<i class="fa-solid fa-music text-white" style="font-size: 1rem;"></i>
</div>
YourLogo
</a>
<div class="d-none d-md-flex">
<a href="#" class="nav-link-custom">Home</a>
<a href="#" class="nav-link-custom">Contact us</a>
</div>
</div>
<div class="d-flex align-items-center gap-4">
<div class="d-none d-sm-flex align-items-center text-muted" style="font-size: 0.9rem; font-weight: 500;">
<i class="fa-solid fa-phone" style="color: var(--primary);"></i> &nbsp; +1 555-555-5556
</div>
<div class="d-flex align-items-center border-start ps-4" style="cursor: pointer;">
<img src="https://ui-avatars.com/api/?name=Test+Parent&background=ebd4fb&color=8c52ff" alt="User" class="rounded-circle me-2" width="36" height="36">
<span class="fw-bold text-dark me-2" style="font-size: 0.95rem;">test parent</span>
<i class="fa-solid fa-chevron-down text-muted" style="font-size: 0.75rem;"></i>
</div>
<a href="#" class="btn-gradient d-none d-lg-flex">Contact Us</a>
</div>
</div>
</nav>
<!-- MAIN DASHBOARD CONTENT -->
<main class="flex-grow-1 py-5">
<div class="container-xl">
<!-- Header Section -->
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end mb-5">
<div>
<div class="breadcrumb-custom">
<a href="#"><i class="fa-solid fa-house"></i> Home</a>
<i class="fa-solid fa-chevron-right"></i>
<a href="#">My Students</a>
<i class="fa-solid fa-chevron-right"></i>
<span style="color: var(--primary-dark); font-weight: 700;">test student</span>
</div>
<h1 class="page-title">Student Dashboard</h1>
</div>
<div class="mt-4 mt-md-0">
<a href="#" class="btn-gradient">
<i class="fa-regular fa-paper-plane"></i> Request Leave
</a>
</div>
</div>
<!-- KPI Cards Row -->
<div class="row g-4 mb-5">
<!-- Profile Card -->
<div class="col-12 col-lg-4">
<div class="glass-card">
<div class="d-flex justify-content-between align-items-start mb-4">
<div class="d-flex align-items-center gap-3">
<div class="stat-icon icon-purple">
<i class="fa-solid fa-user-graduate"></i>
</div>
<div>
<h4 class="fw-bold mb-0" style="color: var(--dark-bg); font-size: 1.3rem; font-family: 'Outfit', sans-serif;">test student</h4>
<span class="text-muted" style="font-family: monospace; font-size: 0.9rem;">STU005</span>
</div>
</div>
<div class="status-badge status-active">
<span class="status-pulse"></span> Active
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-6">
<div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Dance Style</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">Bharatanatyam</div>
</div>
</div>
<div class="col-6">
<div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
<div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Level</div>
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">Beginner</div>
</div>
</div>
</div>
</div>
</div>
<!-- Attendance Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="glass-card">
<div class="d-flex justify-content-between align-items-start">
<h5 class="fw-bold" style="color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Outfit', sans-serif;">Attendance</h5>
<div class="stat-icon icon-orange" style="width: 36px; height: 36px; font-size: 1rem;">
<i class="fa-solid fa-calendar-check"></i>
</div>
</div>
<div class="stat-value">0.0%</div>
<div class="progress-custom">
<div class="progress-bar-custom" style="width: 5%;"></div>
</div>
<div class="d-flex gap-3 text-muted" style="font-size: 0.85rem; font-weight: 500;">
<span><i class="fa-solid fa-circle-check" style="color: #22c55e;"></i> 1 Session</span>
<span><i class="fa-solid fa-circle-xmark" style="color: #ef4444;"></i> 1 Absence</span>
</div>
</div>
</div>
<!-- Fees Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="glass-card d-flex flex-column">
<div class="d-flex justify-content-between align-items-start">
<h5 class="fw-bold" style="color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Outfit', sans-serif;">Fees Overview</h5>
<div class="stat-icon icon-blue" style="width: 36px; height: 36px; font-size: 1rem;">
<i class="fa-solid fa-file-invoice-dollar"></i>
</div>
</div>
<div class="stat-value">1</div>
<p class="text-muted mt-2 mb-4" style="font-size: 0.9rem;">
Student invoice pending or generated.
</p>
<div class="mt-auto">
<a href="#" style="color: var(--primary-dark); font-weight: 700; text-decoration: none; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 1px;">
View all invoices <i class="fa-solid fa-arrow-right ms-1" style="font-size: 0.8rem;"></i>
</a>
</div>
</div>
</div>
</div> <!-- End KPI Cards -->
<!-- Tables Section -->
<div class="row g-5">
<!-- Batches Table -->
<div class="col-12 col-xl-6">
<div class="d-flex justify-content-between align-items-center mb-3 px-1">
<h3 class="section-title mb-0">Enrolled Batches</h3>
<span class="badge" style="background: rgba(140, 82, 255, 0.15); color: var(--primary-dark); font-size: 0.8rem; padding: 6px 12px; border-radius: 50px;">1 Batch</span>
</div>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>Batch Name</th>
<th>Status</th>
<th>Enrollment Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center gap-3">
<div style="width: 36px; height: 36px; border-radius: 50%; background: rgba(140, 82, 255, 0.15); color: var(--primary-dark); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 0.8rem; font-family: 'Cormorant Garamond', serif; font-style: italic;">
TB
</div>
<span class="fw-bold text-dark">test batch</span>
</div>
</td>
<td>
<span style="background: rgba(34, 197, 94, 0.1); color: #16a34a; padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">Active</span>
</td>
<td class="text-muted">
<i class="fa-regular fa-calendar me-2"></i>2026-09-28
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Leave Requests Table -->
<div class="col-12 col-xl-6">
<div class="d-flex justify-content-between align-items-center mb-3 px-1">
<h3 class="section-title mb-0">Leave Requests</h3>
</div>
<div class="modern-table-container">
<div class="table-responsive">
<table class="table modern-table">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Reason</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-dark fw-bold">2026-09-29</td>
<td class="text-dark fw-bold">2026-09-29</td>
<td class="text-muted">nnn</td>
<td>
<span style="background: rgba(59, 130, 246, 0.1); color: #2563eb; padding: 5px 12px; border-radius: 50px; font-size: 0.75rem; font-weight: 700;">Approved</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div> <!-- End Tables Row -->
</div>
</main>
<!-- FOOTER -->
<footer class="modern-footer">
<div class="container-xl">
<div class="row g-5 border-bottom border-secondary pb-5 mb-4">
<div class="col-12 col-md-3">
<h4 class="footer-heading">Useful Links</h4>
<ul class="list-unstyled" style="font-size: 0.9rem; color: #a0aec0; line-height: 1.8;">
<li><a href="#" style="color: #a0aec0; text-decoration: none;">Home</a></li>
<li><a href="#" style="color: #a0aec0; text-decoration: none;">About us</a></li>
<li><a href="#" style="color: #a0aec0; text-decoration: none;">Products</a></li>
<li><a href="#" style="color: #a0aec0; text-decoration: none;">Services</a></li>
</ul>
</div>
<div class="col-12 col-md-5">
<h4 class="footer-heading">About us</h4>
<p style="color: #a0aec0; font-size: 0.9rem; line-height: 1.6;">
We are a team of passionate people whose goal is to improve everyone's life through disruptive products. We build great products to solve your business problems.
</p>
</div>
<div class="col-12 col-md-4">
<h4 class="footer-heading">Connect with us</h4>
<ul class="list-unstyled mb-4" style="font-size: 0.9rem; color: #a0aec0; line-height: 1.8;">
<li class="d-flex align-items-center gap-2"><i class="fa-solid fa-envelope" style="color: var(--primary-light);"></i> Contact us</li>
<li class="d-flex align-items-center gap-2"><i class="fa-solid fa-envelope" style="color: var(--primary-light);"></i> info@yourcompany.example.com</li>
<li class="d-flex align-items-center gap-2"><i class="fa-solid fa-phone" style="color: var(--primary-light);"></i> +1 555-555-5556</li>
</ul>
</div>
</div>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center" style="font-size: 0.85rem; color: #718096;">
<p class="mb-2 mb-md-0">Copyright © Company name</p>
<p class="mb-0">Powered by <span class="text-white fw-bold">odoo</span> - Create a <a href="#" style="color: var(--primary-light); text-decoration: none;">free website</a></p>
</div>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.css"></script>
</body>
</html>