Fix top navbar active tab styling, custom OWL list/card pages, and dedicated student/guardian form views
This commit is contained in:
parent
3a67039506
commit
c50d4f8b4d
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
98
addons/dance_batch/static/src/batches_page/batches_page.js
Normal file
98
addons/dance_batch/static/src/batches_page/batches_page.js
Normal 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);
|
||||||
78
addons/dance_batch/static/src/batches_page/batches_page.xml
Normal file
78
addons/dance_batch/static/src/batches_page/batches_page.xml
Normal 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>
|
||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
98
addons/dance_event/static/src/events_page/events_page.js
Normal file
98
addons/dance_event/static/src/events_page/events_page.js
Normal 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);
|
||||||
76
addons/dance_event/static/src/events_page/events_page.xml
Normal file
76
addons/dance_event/static/src/events_page/events_page.xml
Normal 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 & 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>
|
||||||
@ -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);
|
||||||
@ -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 & 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>
|
||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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 & Costumes</field>
|
<field name="name">Performances & 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 & 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>
|
||||||
|
|||||||
@ -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 & Recitals</field>
|
<field name="name">Events & 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 & Recitals" parent="dance_student.menu_dance_root" sequence="35"/>
|
<menuitem id="menu_dance_event_root" name="Events & 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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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 & 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 -->
|
||||||
|
|||||||
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -5,13 +5,17 @@
|
|||||||
<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">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>From</th>
|
<th>From</th>
|
||||||
@ -25,13 +29,19 @@
|
|||||||
<td colspan="4" class="text-muted">No leave requests found.</td>
|
<td colspan="4" class="text-muted">No leave requests found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="leave_requests" t-as="leave">
|
<tr t-foreach="leave_requests" t-as="leave">
|
||||||
<td><t t-out="leave.from_date"/></td>
|
<td class="text-dark fw-bold"><t t-out="leave.from_date"/></td>
|
||||||
<td><t t-out="leave.to_date"/></td>
|
<td class="text-dark fw-bold"><t t-out="leave.to_date"/></td>
|
||||||
<td><t t-out="leave.reason"/></td>
|
<td class="text-muted"><t t-out="leave.reason"/></td>
|
||||||
<td><t t-out="leave.status"/></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="leave.status"/>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</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-call="dance_portal.dance_portal_styles"/>
|
||||||
|
|
||||||
|
<div class="dance-portal-wrapper">
|
||||||
|
<div class="dance-shape-1"></div>
|
||||||
|
<div class="dance-shape-2"></div>
|
||||||
|
<div class="dance-content">
|
||||||
|
<div t-if="error" class="alert alert-danger glass-card border-0">
|
||||||
<t t-out="error"/>
|
<t t-out="error"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" class="card">
|
<form method="post" class="glass-card">
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">
|
<h1 class="page-title mb-4">Request Leave for <t t-out="student.name"/></h1>
|
||||||
<t t-out="student.name"/>
|
|
||||||
</h5>
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label class="form-label" for="from_date">From Date</label>
|
<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" required="required" t-att-value="post.get('from_date')"/>
|
<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>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label class="form-label" for="to_date">To Date</label>
|
<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" required="required" t-att-value="post.get('to_date')"/>
|
<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>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label" for="reason">Reason</label>
|
<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" rows="4" required="required"><t t-out="post.get('reason') or ''"/></textarea>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card-footer d-flex gap-2">
|
<div class="d-flex gap-3 mt-4 pt-3 border-top border-light">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" class="btn-gradient">Submit Request</button>
|
||||||
<a t-attf-href="/my/students/#{student.id}" class="btn btn-outline-secondary">Cancel</a>
|
<a t-attf-href="/my/students/#{student.id}" class="btn text-muted fw-bold">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Binary file not shown.
@ -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)
|
||||||
|
|||||||
427
addons/dance_new_theme/static/src/scss/backend.scss
Normal file
427
addons/dance_new_theme/static/src/scss/backend.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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&fit=crop&w=600&q=80" alt="Winter Showcase"/>
|
</t>
|
||||||
</div>
|
<t t-elif="event.event_type == 'workshop'">
|
||||||
<div class="event-card-body">
|
|
||||||
<h3 class="event-card-title">Winter Showcase 2026</h3>
|
|
||||||
<div class="event-meta">
|
|
||||||
<i class="fa fa-calendar"></i> 15 Jan 2026 • Friday
|
|
||||||
</div>
|
|
||||||
<div class="event-meta">
|
|
||||||
<i class="fa fa-map-marker"></i> Chennai Auditorium
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- Card 2 -->
|
|
||||||
<div class="event-card">
|
|
||||||
<div class="event-image-wrapper">
|
|
||||||
<span class="event-badge badge-workshop">Workshop</span>
|
<span class="event-badge badge-workshop">Workshop</span>
|
||||||
<img src="https://images.unsplash.com/photo-1547153760-18fc86324498?auto=format&fit=crop&w=600&q=80" alt="Masterclass"/>
|
</t>
|
||||||
</div>
|
<t t-elif="event.event_type == 'recital'">
|
||||||
<div class="event-card-body">
|
|
||||||
<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>
|
<span class="event-badge badge-recital">Recital</span>
|
||||||
<img src="https://images.unsplash.com/photo-1623018035782-b269248d2899?auto=format&fit=crop&w=600&q=80" alt="Recital"/>
|
</t>
|
||||||
</div>
|
<t t-else="">
|
||||||
<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>
|
<span class="event-badge badge-annual">Annual Day</span>
|
||||||
<img src="https://images.unsplash.com/photo-1514306191717-452ec28c7814?auto=format&fit=crop&w=600&q=80" alt="Annual Day"/>
|
</t>
|
||||||
|
<img src="https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&w=600&q=80" alt="Event Cover"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="event-card-body">
|
<div class="event-card-body">
|
||||||
<h3 class="event-card-title">Annual Day Celebration</h3>
|
<h3 class="event-card-title"><t t-esc="event.name"/></h3>
|
||||||
<div class="event-meta">
|
<div class="event-meta">
|
||||||
<i class="fa fa-calendar"></i> 25 Apr 2026 • Saturday
|
<i class="fa fa-calendar"></i> <t t-esc="event.date"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="event-meta">
|
<div class="event-meta">
|
||||||
<i class="fa fa-map-marker"></i> Grand Convention Hall
|
<i class="fa fa-map-marker"></i> <t t-esc="event.venue or 'Venue TBA'"/>
|
||||||
</div>
|
</div>
|
||||||
<p class="event-desc">
|
<p class="event-desc">
|
||||||
A grand celebration showcasing the talent and dedication of our dancers.
|
<t t-esc="event.notes or 'Join us for this exciting dance event.'"/>
|
||||||
</p>
|
</p>
|
||||||
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
|
<a href="#" class="event-btn">View Details <i class="fa fa-arrow-right ms-1"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="not events">
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<h4>No upcoming events found.</h4>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
<div class="dc-event-icon-wrap">
|
<div class="dc-event-icon-wrap">
|
||||||
|
<t t-if="event.event_type == 'competition'">
|
||||||
<i class="fa fa-trophy"/>
|
<i class="fa fa-trophy"/>
|
||||||
</div>
|
</t>
|
||||||
<div class="dc-event-info">
|
<t t-elif="event.event_type == 'workshop'">
|
||||||
<h4>Inter Academy Dance Competition</h4>
|
<i class="fa fa-television"/>
|
||||||
<div class="dc-event-location">
|
</t>
|
||||||
<i class="fa fa-map-marker"/>
|
<t t-elif="event.event_type == 'recital'">
|
||||||
<span>Madurai Convention Hall</span>
|
|
||||||
</div>
|
|
||||||
<span class="dc-event-badge dc-badge-purple">Registration Open</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="dc-event-action">
|
|
||||||
<i class="fa fa-arrow-right"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event 2 -->
|
|
||||||
<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">OCT</span>
|
|
||||||
<span class="dc-day">05</span>
|
|
||||||
</div>
|
|
||||||
<div class="dc-event-icon-wrap">
|
|
||||||
<i class="fa fa-child"/>
|
<i class="fa fa-child"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-calendar-o"/>
|
||||||
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="dc-event-info">
|
<div class="dc-event-info">
|
||||||
<h4>Bharatanatyam Arangetram</h4>
|
<h4><t t-esc="event.name"/></h4>
|
||||||
<div class="dc-event-location">
|
<div class="dc-event-location">
|
||||||
<i class="fa fa-map-marker"/>
|
<i class="fa fa-map-marker"/>
|
||||||
<span>Nritya Main Stage</span>
|
<span><t t-esc="event.venue or 'Venue TBA'"/></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="dc-event-badge dc-badge-orange">Limited Seats</span>
|
<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>
|
||||||
|
<span class="dc-event-badge dc-badge-green" t-else="">Completed</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" class="dc-event-action">
|
<a href="/events" class="dc-event-action">
|
||||||
<i class="fa fa-arrow-right"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event 3 -->
|
|
||||||
<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>
|
|
||||||
<span class="dc-event-badge dc-badge-green">Family Event</span>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="dc-event-action">
|
|
||||||
<i class="fa fa-arrow-right"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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"/>
|
<i class="fa fa-arrow-right"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<?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">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
@ -18,18 +20,24 @@
|
|||||||
<td colspan="5" class="text-muted">No shared grade cards found.</td>
|
<td colspan="5" class="text-muted">No shared grade cards found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="performance_records" t-as="record">
|
<tr t-foreach="performance_records" t-as="record">
|
||||||
<td><t t-out="record.assessment_date"/></td>
|
<td class="text-dark fw-bold"><t t-out="record.assessment_date"/></td>
|
||||||
<td><t t-out="record.batch_id.name"/></td>
|
<td><t t-out="record.batch_id.name"/></td>
|
||||||
<td class="text-end">
|
<td class="text-end text-muted">
|
||||||
<t t-out="record.total_score"/> / <t t-out="record.total_max_score"/>
|
<span class="fw-bold text-dark"><t t-out="record.total_score"/></span> / <t t-out="record.total_max_score"/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end fw-bold" style="color: var(--primary-dark);">
|
||||||
<t t-out="'%.2f' % record.percentage"/>%
|
<t t-out="'%.2f' % record.percentage"/>%
|
||||||
</td>
|
</td>
|
||||||
<td><t t-out="record.grade_label"/></td>
|
<td>
|
||||||
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Binary file not shown.
@ -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):
|
||||||
|
|||||||
@ -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&family=Outfit:wght@300;400;500;600&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,36 +279,53 @@
|
|||||||
<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"/>
|
||||||
|
|
||||||
|
<div class="mt-4 mb-5">
|
||||||
|
<h1 class="page-title mb-4">My Students</h1>
|
||||||
|
|
||||||
|
<div t-if="not students" class="alert alert-info glass-card border-0">
|
||||||
No students are linked to your portal account yet.
|
No students are linked to your portal account yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-if="students" class="row g-3">
|
<div t-if="students" class="row g-4">
|
||||||
<div t-foreach="students" t-as="student" class="col-12 col-md-6">
|
<div t-foreach="students" t-as="student" class="col-12 col-md-6">
|
||||||
<a t-attf-href="/my/students/#{student.id}" class="text-decoration-none text-reset">
|
<a t-attf-href="/my/students/#{student.id}" class="text-decoration-none text-reset">
|
||||||
<div class="card h-100">
|
<div class="glass-card h-100">
|
||||||
<div class="card-body">
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||||
<div class="d-flex justify-content-between gap-3">
|
<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>
|
<div>
|
||||||
<h5 class="card-title mb-1">
|
<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"/>
|
<t t-out="student.name"/>
|
||||||
</h5>
|
</h4>
|
||||||
<div class="text-muted small" t-if="student.student_code">
|
<span class="text-muted" style="font-family: monospace; font-size: 0.9rem;">
|
||||||
<t t-out="student.student_code"/>
|
<t t-out="student.student_code"/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="badge text-bg-light align-self-start">
|
|
||||||
<t t-out="student.student_status or 'Student'"/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3 small">
|
</div>
|
||||||
|
<div class="status-badge status-active">
|
||||||
|
<span class="status-pulse"></span> <t t-out="student.student_status or 'Student'"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
@ -71,6 +333,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -81,52 +344,98 @@
|
|||||||
<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="mt-4 mb-5">
|
||||||
|
<h1 class="page-title mb-5">Student Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<!-- Profile Card -->
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<div class="card h-100">
|
<div class="glass-card">
|
||||||
<div class="card-body">
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||||
<h5 class="card-title">Student</h5>
|
<div class="d-flex align-items-center gap-3">
|
||||||
<dl class="row mb-0">
|
<div class="stat-icon icon-purple">
|
||||||
<dt class="col-5">Code</dt>
|
<i class="fa-solid fa-user-graduate"></i>
|
||||||
<dd class="col-7"><t t-out="student.student_code or '-'"/></dd>
|
</div>
|
||||||
<dt class="col-5">Status</dt>
|
<div>
|
||||||
<dd class="col-7"><t t-out="student.student_status or '-'"/></dd>
|
<h4 class="fw-bold mb-0" style="color: var(--dark-bg); font-size: 1.3rem; font-family: 'Outfit', sans-serif;">
|
||||||
<dt class="col-5">Style</dt>
|
<t t-out="student.name"/>
|
||||||
<dd class="col-7"><t t-out="student.dance_style or '-'"/></dd>
|
</h4>
|
||||||
<dt class="col-5">Level</dt>
|
<span class="text-muted" style="font-family: monospace; font-size: 0.9rem;">
|
||||||
<dd class="col-7"><t t-out="student.level or '-'"/></dd>
|
<t t-out="student.student_code or '-'"/>
|
||||||
</dl>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-badge status-active">
|
||||||
|
<span class="status-pulse"></span> <t t-out="student.student_status or '-'"/>
|
||||||
|
</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;">
|
||||||
|
<t t-out="student.dance_style or '-'"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-6">
|
||||||
<div class="card h-100">
|
<div style="background: rgba(255,255,255,0.4); padding: 12px; border-radius: 12px;">
|
||||||
<div class="card-body">
|
<div style="font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted); font-weight: 700; margin-bottom: 4px; letter-spacing: 1px;">Level</div>
|
||||||
<h5 class="card-title">Attendance</h5>
|
<div class="fw-bold text-dark" style="font-size: 0.95rem; font-family: 'Cormorant Garamond', serif; font-size: 1.2rem; font-style: italic;">
|
||||||
<div class="display-6">
|
<t t-out="student.level or '-'"/>
|
||||||
<t t-out="attendance_percent"/>%
|
|
||||||
</div>
|
|
||||||
<div class="text-muted">
|
|
||||||
<t t-out="len(attendance)"/> sessions, <t t-out="absent_count"/> absences
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Fees</h5>
|
|
||||||
<div class="display-6">
|
|
||||||
<t t-out="len(invoices)"/>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted">student invoices</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Batches</h5>
|
<!-- Attendance Card -->
|
||||||
<t t-call="portal.portal_table">
|
<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"><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>
|
||||||
|
|
||||||
|
<h3 class="section-title">Enrolled Batches</h3>
|
||||||
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Batch</th>
|
<th>Batch</th>
|
||||||
@ -139,15 +448,23 @@
|
|||||||
<td colspan="3" class="text-muted">No enrollments found.</td>
|
<td colspan="3" class="text-muted">No enrollments found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="enrollments" t-as="enrollment">
|
<tr t-foreach="enrollments" t-as="enrollment">
|
||||||
<td><t t-out="enrollment.batch_id.name"/></td>
|
<td class="fw-bold text-dark"><t t-out="enrollment.batch_id.name"/></td>
|
||||||
<td><t t-out="enrollment.status"/></td>
|
<td>
|
||||||
<td><t t-out="enrollment.enrollment_date"/></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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-4">Attendance</h5>
|
<h3 class="section-title">Attendance Details</h3>
|
||||||
<t t-call="portal.portal_table">
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
@ -161,16 +478,20 @@
|
|||||||
<td colspan="4" class="text-muted">No attendance records found.</td>
|
<td colspan="4" class="text-muted">No attendance records found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="attendance" t-as="line">
|
<tr t-foreach="attendance" t-as="line">
|
||||||
<td><t t-out="line.date"/></td>
|
<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.batch_id.name"/></td>
|
||||||
<td><t t-out="line.session_id.name"/></td>
|
<td><t t-out="line.session_id.name"/></td>
|
||||||
<td><t t-out="line.status"/></td>
|
<td><t t-out="line.status"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-4">Upcoming Sessions</h5>
|
<h3 class="section-title">Upcoming Sessions</h3>
|
||||||
<t t-call="portal.portal_table">
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
@ -185,7 +506,7 @@
|
|||||||
<td colspan="5" class="text-muted">No upcoming sessions found.</td>
|
<td colspan="5" class="text-muted">No upcoming sessions found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="upcoming_sessions" t-as="session">
|
<tr t-foreach="upcoming_sessions" t-as="session">
|
||||||
<td><t t-out="session.date"/></td>
|
<td class="text-dark fw-bold"><t t-out="session.date"/></td>
|
||||||
<td>
|
<td>
|
||||||
<t t-if="session.start_time"><t t-out="session.start_time"/></t>
|
<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>
|
<t t-if="session.end_time"> - <t t-out="session.end_time"/></t>
|
||||||
@ -195,10 +516,14 @@
|
|||||||
<td><t t-out="session.status"/></td>
|
<td><t t-out="session.status"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-4">Upcoming Events</h5>
|
<h3 class="section-title">Upcoming Events</h3>
|
||||||
<t t-call="portal.portal_table">
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Event</th>
|
<th>Event</th>
|
||||||
@ -212,16 +537,20 @@
|
|||||||
<td colspan="4" class="text-muted">No upcoming events found.</td>
|
<td colspan="4" class="text-muted">No upcoming events found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="upcoming_events" t-as="performance">
|
<tr t-foreach="upcoming_events" t-as="performance">
|
||||||
<td><t t-out="performance.event_id.name"/></td>
|
<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.date"/></td>
|
||||||
<td><t t-out="performance.event_id.venue or '-'"/></td>
|
<td><t t-out="performance.event_id.venue or '-'"/></td>
|
||||||
<td><t t-out="performance.role"/></td>
|
<td><t t-out="performance.role"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-4">Upcoming Rehearsals</h5>
|
<h3 class="section-title">Upcoming Rehearsals</h3>
|
||||||
<t t-call="portal.portal_table">
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
@ -236,17 +565,21 @@
|
|||||||
<td colspan="5" class="text-muted">No upcoming rehearsals found.</td>
|
<td colspan="5" class="text-muted">No upcoming rehearsals found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="upcoming_rehearsals" t-as="rehearsal">
|
<tr t-foreach="upcoming_rehearsals" t-as="rehearsal">
|
||||||
<td><t t-out="rehearsal.date"/></td>
|
<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.display_time"/></td>
|
||||||
<td><t t-out="rehearsal.event_id.name"/></td>
|
<td><t t-out="rehearsal.event_id.name"/></td>
|
||||||
<td><t t-out="rehearsal.room or '-'"/></td>
|
<td><t t-out="rehearsal.room or '-'"/></td>
|
||||||
<td><t t-out="rehearsal.status"/></td>
|
<td><t t-out="rehearsal.status"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5 class="mt-4">Fees</h5>
|
<h3 class="section-title">Fees / Invoices</h3>
|
||||||
<t t-call="portal.portal_table">
|
<div class="modern-table-container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table modern-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Invoice</th>
|
<th>Invoice</th>
|
||||||
@ -263,22 +596,32 @@
|
|||||||
<td colspan="7" class="text-muted">No invoices found.</td>
|
<td colspan="7" class="text-muted">No invoices found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="invoices" t-as="invoice">
|
<tr t-foreach="invoices" t-as="invoice">
|
||||||
<td><t t-out="invoice.name"/></td>
|
<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.invoice_date"/></td>
|
||||||
<td><t t-out="invoice.dance_batch_id.name or '-'"/></td>
|
<td><t t-out="invoice.dance_batch_id.name or '-'"/></td>
|
||||||
<td class="text-end">
|
<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}"/>
|
<span t-field="invoice.amount_total" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end text-danger fw-bold">
|
||||||
<span t-field="invoice.amount_residual" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
|
<span t-field="invoice.amount_residual" t-options="{'widget': 'monetary', 'display_currency': invoice.currency_id}"/>
|
||||||
</td>
|
</td>
|
||||||
<td><t t-out="invoice.payment_state"/></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">
|
<td class="text-end">
|
||||||
<a t-att-href="invoice.get_portal_url()" class="btn btn-sm btn-outline-primary">Open</a>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</t>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
104
addons/dance_student/static/src/students_page/students_page.js
Normal file
104
addons/dance_student/static/src/students_page/students_page.js
Normal 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);
|
||||||
686
addons/dance_student/static/src/students_page/students_page.scss
Normal file
686
addons/dance_student/static/src/students_page/students_page.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
150
addons/dance_student/static/src/students_page/students_page.xml
Normal file
150
addons/dance_student/static/src/students_page/students_page.xml
Normal 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 & Productions</div>
|
||||||
|
<div t-attf-class="stu-nav-subitem #{activeTab == 'events' ? 'active' : ''}" t-on-click="() => this.navigate('events')">
|
||||||
|
<i class="fa fa-star"/> Events & Recitals
|
||||||
|
</div>
|
||||||
|
<div t-attf-class="stu-nav-subitem #{activeTab == 'performance' ? 'active' : ''}" t-on-click="() => this.navigate('performance')">
|
||||||
|
<i class="fa fa-magic"/> Performances & 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>
|
||||||
@ -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 & 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 & 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 & 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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Binary file not shown.
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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>
|
||||||
@ -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
628
parent_portal_design.html
Normal 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> +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>
|
||||||
Loading…
x
Reference in New Issue
Block a user