admin dashboard - changes

This commit is contained in:
Vidhya 2026-09-24 23:43:57 +05:30
parent ea0da2241a
commit 6217df60b2
16 changed files with 1564 additions and 320 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_education_base_root" name="Education" sequence="10" groups="base.group_user" web_icon="fa fa-graduation-cap"/>
<menuitem id="menu_education_base_configuration" name="Configuration" parent="menu_education_base_root" sequence="1" groups="base.group_user"/>
<menuitem id="menu_education_base_root" name="Education" sequence="0" groups="base.group_user" web_icon="fa fa-graduation-cap"/>
<menuitem id="menu_education_base_configuration" name="Configuration" parent="menu_education_base_root" sequence="100" groups="base.group_user"/>
<menuitem id="menu_education_base_master_data" name="Master Data" parent="menu_education_base_root" sequence="2" groups="base.group_user"/>
</odoo>

View File

@ -4,12 +4,18 @@
<field name="name">education.academic.year.tree</field>
<field name="model">education.academic.year</field>
<field name="arch" type="xml">
<list string="Academic Years">
<list string="Academic Years"
decoration-success="state == 'active'"
decoration-danger="state == 'inactive'"
decoration-muted="state in ('closed', 'draft')">
<field name="name"/>
<field name="code"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="state"/>
<field name="state" widget="badge"
decoration-success="state == 'active'"
decoration-danger="state == 'inactive'"
decoration-secondary="state in ('closed', 'draft')"/>
<field name="current_year"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>

View File

@ -1,7 +1,8 @@
# pyrefly: ignore [missing-import]
from odoo import http
from odoo import http, fields
# pyrefly: ignore [missing-import]
from odoo.http import request
import datetime
class EducationDashboardController(http.Controller):
@ -9,94 +10,150 @@ class EducationDashboardController(http.Controller):
@http.route(['/web/education/dashboard/kpis'], type='json', auth="user")
def get_dashboard_kpis(self, campus_id=None, program_id=None, academic_year_id=None, semester_id=None, env=None, **kw):
env = env or request.env
# Base Student Domain
student_domain = []
payment_domain = [('status', '=', 'posted')]
# Apply Filters
# ── Filters ──────────────────────────────────────────────────────────
student_domain = []
if campus_id:
student_domain.append(('campus_id', '=', int(campus_id)))
payment_domain.append(('student_id.campus_id', '=', int(campus_id)))
if program_id:
student_domain.append(('program_id', '=', int(program_id)))
payment_domain.append(('student_id.program_id', '=', int(program_id)))
if academic_year_id:
student_domain.append(('academic_year_id', '=', int(academic_year_id)))
payment_domain.append(('student_id.academic_year_id', '=', int(academic_year_id)))
if semester_id:
student_domain.append(('semester_id', '=', int(semester_id)))
payment_domain.append(('student_id.semester_id', '=', int(semester_id)))
# Fetch Student Records
# ── Students ─────────────────────────────────────────────────────────
students = env['education.student'].search(student_domain)
# Executive KPIs
total_students = len(students)
active_students = len(students.filtered(lambda s: s.status == 'active'))
admissions = len(students.filtered(lambda s: s.status == 'admitted'))
graduates = len(students.filtered(lambda s: s.status == 'graduated'))
withdrawn = len(students.filtered(lambda s: s.status == 'withdrawn'))
# Finance KPIs
# ── Finance ───────────────────────────────────────────────────────────
total_fees = sum(students.mapped('total_fees'))
collected_fees = sum(students.mapped('total_paid'))
outstanding_fees = sum(students.mapped('outstanding_amount'))
fee_percent = int(collected_fees / total_fees * 100) if total_fees > 0 else 0
# Academic KPIs
# ── Academic counts ───────────────────────────────────────────────────
programs_count = env['education.program'].search_count([])
dept_count = env['education.department'].search_count([])
campus_count = env['education.campus'].search_count([])
years_count = env['education.academic.year'].search_count([])
# Charts: Program Distribution (using read_group for speed)
program_data = env['education.student'].read_group(
student_domain, ['program_id'], ['program_id']
)
# ── Charts: Program distribution ──────────────────────────────────────
program_chart = []
for gd in program_data:
for gd in env['education.student'].read_group(student_domain, ['program_id'], ['program_id']):
if gd.get('program_id'):
program_chart.append({
'name': gd['program_id'][1],
'count': gd['program_id_count'],
})
program_chart.append({'name': gd['program_id'][1], 'count': gd['program_id_count']})
# Charts: Campus Distribution
campus_data = env['education.student'].read_group(
student_domain, ['campus_id'], ['campus_id']
)
# ── Charts: Campus distribution ───────────────────────────────────────
campus_chart = []
for gd in campus_data:
for gd in env['education.student'].read_group(student_domain, ['campus_id'], ['campus_id']):
if gd.get('campus_id'):
campus_chart.append({
'name': gd['campus_id'][1],
'count': gd['campus_id_count'],
campus_chart.append({'name': gd['campus_id'][1], 'count': gd['campus_id_count']})
# ── Monthly enrollment trend (current calendar year) ─────────────────
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
cur_year = datetime.date.today().year
enrollment_trend = []
cumulative = 0
for m in range(1, 13):
ny = cur_year if m < 12 else cur_year + 1
nm = m + 1 if m < 12 else 1
count = env['education.student'].search_count([
('create_date', '>=', f'{cur_year}-{m:02d}-01'),
('create_date', '<', f'{ny}-{nm:02d}-01'),
])
cumulative += count
enrollment_trend.append({
'month': month_names[m - 1],
'new': count,
'total': cumulative if cumulative else total_students,
})
# Master Data lists for Filters
# ── Upcoming exams ────────────────────────────────────────────────────
upcoming_exams = []
try:
exams = env['education.exam'].search(
[('state', 'not in', ['cancelled', 'completed', 'done'])],
order='date asc', limit=4
)
for e in exams:
upcoming_exams.append({
'name': e.name,
'date': e.date.strftime('%b %d, %Y') if e.date else '',
'program': e.program_id.name if getattr(e, 'program_id', False) and e.program_id else '',
})
except Exception:
pass
# ── Recent activities ─────────────────────────────────────────────────
recent_activities = []
try:
msgs = env['mail.message'].search([
('model', 'in', ['education.student', 'education.exam', 'education.fee.payment']),
('message_type', 'in', ['email', 'comment']),
], order='date desc', limit=5)
for msg in msgs:
body = msg.subject or ''
if not body and msg.body:
import re
body = re.sub('<[^>]+>', '', msg.body)[:60]
recent_activities.append({
'text': body or 'Activity recorded',
'time': msg.date.strftime('%I:%M %p') if msg.date else '',
'model': msg.model or '',
})
except Exception:
pass
# ── User / date context ───────────────────────────────────────────────
user_name = env.user.name
today_str = datetime.date.today().strftime('%A, %b %d, %Y')
current_year_name = ''
try:
cy = env['education.academic.year'].search([('current_year', '=', True)], limit=1)
if cy:
current_year_name = cy.name
except Exception:
pass
# ── Filter dropdown data ──────────────────────────────────────────────
campuses = [{'id': c.id, 'name': c.name} for c in env['education.campus'].search([])]
programs = [{'id': p.id, 'name': p.name} for p in env['education.program'].search([])]
years = [{'id': y.id, 'name': y.name} for y in env['education.academic.year'].search([])]
semesters = [{'id': s.id, 'name': s.name} for s in env['education.semester'].search([])]
return {
# student KPIs
'total_students': total_students,
'active_students': active_students,
'admissions': admissions,
'graduates': graduates,
'withdrawn': withdrawn,
# finance
'total_fees': total_fees,
'collected_fees': collected_fees,
'outstanding_fees': outstanding_fees,
'fee_percent': fee_percent,
# academic
'programs_count': programs_count,
'dept_count': dept_count,
'campus_count': campus_count,
'years_count': years_count,
# charts
'program_chart': program_chart,
'campus_chart': campus_chart,
'enrollment_trend': enrollment_trend,
# operational
'upcoming_exams': upcoming_exams,
'recent_activities': recent_activities,
# context
'user_name': user_name,
'today': today_str,
'current_year_name': current_year_name,
# filter options
'campuses': campuses,
'programs': programs,
'years': years,

View File

@ -6,40 +6,43 @@ import { useService } from "@web/core/utils/hooks";
import { loadJS } from "@web/core/assets";
import { rpc } from "@web/core/network/rpc";
const CHART_COLORS = ['#1f8f8f', '#0d9488', '#0891b2', '#7c3aed', '#ea580c', '#64748b'];
class EducationDashboard extends Component {
setup() {
this.actionService = useService("action");
this.state = useState({
kpis: {},
filters: {
campus_id: "",
program_id: "",
academic_year_id: "",
semester_id: ""
}
filters: { campus_id: "", program_id: "", academic_year_id: "", semester_id: "" }
});
this.programChartRef = useRef("programChart");
this.campusChartRef = useRef("campusChart");
this.enrollmentChartRef = useRef("enrollmentChart");
this.distributionChartRef = useRef("distributionChart");
this.feeDonutChartRef = useRef("feeDonutChart");
onWillStart(async () => {
await loadJS("/web/static/lib/Chart/Chart.js");
// Try loading Chart.js from multiple known Odoo paths
const paths = [
"/web/static/lib/Chart/Chart.js",
"/web/static/lib/chart.js/chart.js",
"/web/static/lib/chartjs/chart.js",
];
for (const p of paths) {
try { await loadJS(p); break; } catch (_) { /* try next */ }
}
await this.loadData();
});
onMounted(() => {
this.renderCharts();
});
onMounted(() => this.renderCharts());
}
// ── Data ──────────────────────────────────────────────────────────────────
async loadData() {
// Map empty string filters to null
const params = {};
for (const [k, v] of Object.entries(this.state.filters)) {
params[k] = v ? parseInt(v) : null;
}
const data = await rpc("/web/education/dashboard/kpis", params);
this.state.kpis = data;
this.state.kpis = await rpc("/web/education/dashboard/kpis", params);
}
async onFilterChange(filterKey, ev) {
@ -48,88 +51,226 @@ class EducationDashboard extends Component {
this.renderCharts();
}
// ── Formatting helper ─────────────────────────────────────────────────────
fmt(amount) {
if (!amount) return "₹ 0";
if (amount >= 10000000) return "₹ " + (amount / 10000000).toFixed(2) + " Cr";
if (amount >= 100000) return "₹ " + (amount / 100000).toFixed(2) + " L";
if (amount >= 1000) return "₹ " + (amount / 1000).toFixed(1) + " K";
return "₹ " + amount;
}
// ── Charts ────────────────────────────────────────────────────────────────
renderCharts() {
const pCtx = this.programChartRef.el;
if (pCtx) {
if (this.pChart) this.pChart.destroy();
const labels = this.state.kpis.program_chart.map(x => x.name);
const data = this.state.kpis.program_chart.map(x => x.count);
this.pChart = new Chart(pCtx, {
type: 'bar',
if (typeof Chart === "undefined") {
console.warn("[EduDashboard] Chart.js not loaded — charts skipped.");
return;
}
this._renderEnrollment();
this._renderDistribution();
this._renderFeeDonut();
}
_renderEnrollment() {
const ctx = this.enrollmentChartRef.el;
if (!ctx) return;
if (this._enrollChart) this._enrollChart.destroy();
const trend = this.state.kpis.enrollment_trend || [];
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const labels = trend.length ? trend.map(x => x.month) : months;
const newD = trend.length ? trend.map(x => x.new) : Array(12).fill(0);
const totD = trend.length ? trend.map(x => x.total) : Array(12).fill(0);
this._enrollChart = new Chart(ctx, {
type: "line",
data: {
labels: labels,
labels,
datasets: [
{
label: "New Admissions",
data: newD,
borderColor: "#0d9488",
backgroundColor: "rgba(13,148,136,0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
pointBackgroundColor: "#0d9488",
pointRadius: 3,
pointHoverRadius: 5,
},
{
label: "Total Students",
data: totD,
borderColor: "#1f8f8f",
backgroundColor: "rgba(31,143,143,0.06)",
borderWidth: 2,
fill: true,
tension: 0.4,
pointBackgroundColor: "#1f8f8f",
pointRadius: 3,
pointHoverRadius: 5,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "#0f172a",
titleColor: "#fff",
bodyColor: "#94a3b8",
padding: 10,
cornerRadius: 8,
}
},
scales: {
x: { grid: { display: false }, ticks: { color: "#94a3b8", font: { size: 11 } } },
y: {
beginAtZero: true,
grid: { color: "#f1f5f9" },
ticks: { color: "#94a3b8", font: { size: 11 }, stepSize: 1 }
}
}
}
});
}
_renderDistribution() {
const ctx = this.distributionChartRef.el;
if (!ctx) return;
if (this._distChart) this._distChart.destroy();
const progs = this.state.kpis.program_chart || [];
const labels = progs.map(p => p.name);
const data = progs.map(p => p.count);
this._distChart = new Chart(ctx, {
type: "doughnut",
data: {
labels,
datasets: [{
label: 'Students Count',
data: data,
backgroundColor: '#4F81BD'
data: data.length ? data : [1],
backgroundColor: data.length ? CHART_COLORS.slice(0, data.length) : ["#e2e8f0"],
borderWidth: 0,
hoverOffset: 4,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
cutout: "60%",
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "#0f172a",
titleColor: "#fff",
bodyColor: "#94a3b8",
padding: 10,
cornerRadius: 8,
}
}
}
});
}
const cCtx = this.campusChartRef.el;
if (cCtx) {
if (this.cChart) this.cChart.destroy();
const labels = this.state.kpis.campus_chart.map(x => x.name);
const data = this.state.kpis.campus_chart.map(x => x.count);
this.cChart = new Chart(cCtx, {
type: 'doughnut',
_renderFeeDonut() {
const ctx = this.feeDonutChartRef.el;
if (!ctx) return;
if (this._feeChart) this._feeChart.destroy();
const pct = this.state.kpis.fee_percent || 0;
const rest = Math.max(0, 100 - pct);
this._feeChart = new Chart(ctx, {
type: "doughnut",
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: ['#4F81BD', '#C0504D', '#9BBB59', '#8064A2', '#4BACC6']
data: [pct, rest],
backgroundColor: ["#1f8f8f", "#f1f5f9"],
borderWidth: 0,
}]
},
options: {
responsive: true,
maintainAspectRatio: false
responsive: false,
maintainAspectRatio: false,
cutout: "72%",
plugins: { legend: { display: false }, tooltip: { enabled: false } }
}
});
}
}
// ── Navigation actions ────────────────────────────────────────────────────
openStudents(status) {
const domain = [];
if (status) {
domain.push(['status', '=', status]);
}
if (this.state.filters.campus_id) {
domain.push(['campus_id', '=', parseInt(this.state.filters.campus_id)]);
}
if (this.state.filters.program_id) {
domain.push(['program_id', '=', parseInt(this.state.filters.program_id)]);
}
const domain = status ? [["status", "=", status]] : [];
if (this.state.filters.campus_id) domain.push(["campus_id", "=", parseInt(this.state.filters.campus_id)]);
if (this.state.filters.program_id) domain.push(["program_id", "=", parseInt(this.state.filters.program_id)]);
this.actionService.doAction({
name: "Students List",
type: "ir.actions.act_window",
name: "Students", type: "ir.actions.act_window",
res_model: "education.student",
views: [[false, "list"], [false, "form"]],
domain: domain,
target: "current"
domain, target: "current"
});
}
openPrograms() {
this.actionService.doAction({
name: "Active Programs",
type: "ir.actions.act_window",
name: "Programs", type: "ir.actions.act_window",
res_model: "education.program",
views: [[false, "list"], [false, "form"]],
target: "current"
});
}
openAddStudent() {
this.actionService.doAction({
name: "New Student", type: "ir.actions.act_window",
res_model: "education.student",
views: [[false, "form"]], target: "current"
});
}
openAddFaculty() {
this.actionService.doAction({
name: "Faculty", type: "ir.actions.act_window",
res_model: "hr.employee",
views: [[false, "list"], [false, "form"]], target: "current"
});
}
openCreateCourse() {
this.actionService.doAction({
name: "Courses", type: "ir.actions.act_window",
res_model: "education.course",
views: [[false, "list"], [false, "form"]], target: "current"
});
}
openScheduleExam() {
this.actionService.doAction({
name: "Examinations", type: "ir.actions.act_window",
res_model: "education.exam",
views: [[false, "list"], [false, "form"]], target: "current"
});
}
openAttendance() {
this.actionService.doAction({
name: "Attendance", type: "ir.actions.act_window",
res_model: "education.attendance",
views: [[false, "list"], [false, "form"]], target: "current"
});
}
openFees() {
this.actionService.doAction({
name: "Fee Payments", type: "ir.actions.act_window",
res_model: "education.fee.payment",
views: [[false, "list"], [false, "form"]], target: "current"
});
}
}
EducationDashboard.template = "education_dashboard.DashboardMain";

View File

@ -1,172 +1,314 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="education_dashboard.DashboardMain">
<div class="container-fluid py-4 bg-light overflow-auto" style="height: 100vh;">
<!-- Filter Bar -->
<div class="row bg-white p-3 shadow-sm rounded mb-4 mx-1">
<div class="col-md-3">
<label class="form-label font-weight-bold small text-uppercase text-muted">Campus</label>
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('campus_id', ev)">
<option value="">All Campuses</option>
<t t-foreach="state.kpis.campuses || []" t-as="c" t-key="c.id">
<option t-att-value="c.id"><t t-esc="c.name"/></option>
</t>
</select>
</div>
<div class="col-md-3">
<label class="form-label font-weight-bold small text-uppercase text-muted">Program</label>
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('program_id', ev)">
<option value="">All Programs</option>
<t t-foreach="state.kpis.programs || []" t-as="p" t-key="p.id">
<option t-att-value="p.id"><t t-esc="p.name"/></option>
</t>
</select>
</div>
<div class="col-md-3">
<label class="form-label font-weight-bold small text-uppercase text-muted">Academic Year</label>
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('academic_year_id', ev)">
<option value="">All Years</option>
<t t-foreach="state.kpis.years || []" t-as="y" t-key="y.id">
<option t-att-value="y.id"><t t-esc="y.name"/></option>
</t>
</select>
</div>
<div class="col-md-3">
<label class="form-label font-weight-bold small text-uppercase text-muted">Semester</label>
<select class="form-control form-control-sm" t-on-change="(ev) => this.onFilterChange('semester_id', ev)">
<option value="">All Semesters</option>
<t t-foreach="state.kpis.semesters || []" t-as="s" t-key="s.id">
<option t-att-value="s.id"><t t-esc="s.name"/></option>
</t>
</select>
</div>
</div>
<div style="min-height:100vh; background:#eef9f9; padding:20px 24px; overflow-y:auto; font-family:-apple-system,BlinkMacSystemFont,'Inter',sans-serif;">
<!-- Executive KPIs -->
<div class="row mb-4 mx-1">
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('')" style="cursor: pointer;">
<div class="card shadow-sm border-0 bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-uppercase small mb-1">Total Students</h6>
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.total_students || 0"/></h2>
</div>
<i class="fa fa-users fa-3x opacity-50"/>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('active')" style="cursor: pointer;">
<div class="card shadow-sm border-0 bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-uppercase small mb-1">Active Students</h6>
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.active_students || 0"/></h2>
</div>
<i class="fa fa-user-check fa-3x opacity-50"/>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('admitted')" style="cursor: pointer;">
<div class="card shadow-sm border-0 bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-uppercase small mb-1">Admissions</h6>
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.admissions || 0"/></h2>
</div>
<i class="fa fa-user-plus fa-3x opacity-50"/>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-2" t-on-click="() => this.openStudents('graduated')" style="cursor: pointer;">
<div class="card shadow-sm border-0 bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-uppercase small mb-1">Graduates</h6>
<h2 class="font-weight-bold mb-0"><t t-esc="state.kpis.graduates || 0"/></h2>
</div>
<i class="fa fa-graduation-cap fa-3x opacity-50"/>
<style>
.edu-card { background:#fff; border-radius:14px; border:1px solid #e2e8f0; box-shadow:0 2px 12px rgba(15,23,42,0.06); }
.edu-kpi { background:#fff; border-radius:14px; border:1px solid #e2e8f0; box-shadow:0 2px 8px rgba(15,23,42,0.05); padding:16px 18px; display:flex; align-items:center; gap:14px; transition:transform .2s,box-shadow .2s; cursor:pointer; }
.edu-kpi:hover { transform:translateY(-2px); box-shadow:0 8px 20px rgba(15,23,42,0.1); }
.edu-icon { width:46px; height:46px; border-radius:11px; display:flex; align-items:center; justify-content:center; font-size:1.2rem; flex-shrink:0; }
.edu-qbtn { display:flex; flex-direction:column; align-items:center; gap:6px; padding:14px 6px; border-radius:12px; border:1.5px solid #e2e8f0; background:#fff; cursor:pointer; transition:all .15s; text-decoration:none; color:#334155; }
.edu-qbtn:hover { border-color:#1f8f8f; background:#eef9f9; color:#1f8f8f; }
.edu-qbtn i { font-size:1.2rem; }
.edu-qbtn span { font-size:0.7rem; font-weight:700; text-align:center; line-height:1.2; }
.edu-exam-row { display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid #f1f5f9; }
.edu-exam-row:last-child { border-bottom:none; }
.edu-ann-row { display:flex; gap:10px; padding:9px 0; border-bottom:1px solid #f1f5f9; align-items:flex-start; }
.edu-ann-row:last-child { border-bottom:none; }
.edu-pill { padding:3px 9px; border-radius:20px; font-size:0.68rem; font-weight:700; flex-shrink:0; }
</style>
<!-- ── WELCOME BANNER ─────────────────────────────────────── -->
<div style="background:linear-gradient(135deg,#1f8f8f 0%,#144d4d 55%,#0d3535 100%); border-radius:16px; padding:26px 32px; margin-bottom:20px; display:flex; align-items:center; justify-content:space-between; position:relative; overflow:hidden;">
<div style="position:absolute;top:-30px;right:200px;width:180px;height:180px;border-radius:50%;background:rgba(255,255,255,0.05);pointer-events:none;"></div>
<div style="position:absolute;bottom:-40px;right:60px;width:240px;height:240px;border-radius:50%;background:rgba(255,255,255,0.04);pointer-events:none;"></div>
<div style="position:relative;z-index:1;">
<div style="font-size:0.72rem;font-weight:700;color:rgba(255,255,255,0.55);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:6px;">Admin Dashboard</div>
<div style="font-size:1.85rem;font-weight:800;color:#fff;line-height:1.2;margin-bottom:5px;">Welcome back, <t t-esc="state.kpis.user_name || 'Admin'"/>!</div>
<div style="font-size:0.88rem;color:rgba(255,255,255,0.65);">Manage your institution efficiently with real-time insights.</div>
</div>
<div style="position:relative;z-index:1;flex-shrink:0;text-align:right;display:flex;flex-direction:column;gap:8px;">
<div style="background:rgba(255,255,255,0.13);border-radius:10px;padding:9px 16px;backdrop-filter:blur(4px);">
<div style="font-size:0.78rem;color:rgba(255,255,255,0.75);"><i class="fa fa-calendar me-1 mr-1"/> <t t-esc="state.kpis.today || ''"/></div>
</div>
<div style="background:rgba(255,255,255,0.13);border-radius:10px;padding:8px 16px;backdrop-filter:blur(4px);">
<div style="font-size:0.78rem;font-weight:600;color:#fff;"><i class="fa fa-graduation-cap me-1 mr-1"/> <t t-esc="state.kpis.current_year_name || 'Academic Year'"/></div>
</div>
</div>
</div>
<!-- Finance KPIs -->
<div class="row mb-4 mx-1">
<div class="col-md-4 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-muted small text-uppercase">Total Fees Assigned</div>
<h2 class="font-weight-bold text-dark mb-0"><t t-esc="state.kpis.total_fees || 0"/></h2>
<!-- ── KPI CARDS ──────────────────────────────────────────── -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:20px;">
<div class="edu-kpi" t-on-click="() => this.openStudents('')">
<div class="edu-icon" style="background:#e0f7f7;color:#1f8f8f;"><i class="fa fa-users"/></div>
<div>
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.total_students || 0"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Total Students</div>
</div>
</div>
<div class="col-md-4 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-success small text-uppercase">Collected Fees</div>
<h2 class="font-weight-bold text-success mb-0"><t t-esc="state.kpis.collected_fees || 0"/></h2>
<div class="edu-kpi" t-on-click="() => this.openStudents('active')">
<div class="edu-icon" style="background:#dcfce7;color:#16a34a;"><i class="fa fa-user"/></div>
<div>
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.active_students || 0"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Active Students</div>
</div>
</div>
<div class="col-md-4 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-danger small text-uppercase">Outstanding Fees</div>
<h2 class="font-weight-bold text-danger mb-0"><t t-esc="state.kpis.outstanding_fees || 0"/></h2>
<div class="edu-kpi" t-on-click="openPrograms">
<div class="edu-icon" style="background:#ede9fe;color:#7c3aed;"><i class="fa fa-book"/></div>
<div>
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.programs_count || 0"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Programs</div>
</div>
</div>
<div class="edu-kpi">
<div class="edu-icon" style="background:#fff7ed;color:#ea580c;"><i class="fa fa-sitemap"/></div>
<div>
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.dept_count || 0"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Departments</div>
</div>
</div>
<div class="edu-kpi">
<div class="edu-icon" style="background:#fef9c3;color:#ca8a04;"><i class="fa fa-money"/></div>
<div>
<div style="font-size:1.1rem;font-weight:800;color:#0f172a;line-height:1.2;"><t t-esc="this.fmt(state.kpis.collected_fees || 0)"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Fees Collected</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="row mb-4 mx-1">
<div class="col-md-8 mb-2">
<div class="card shadow-sm border-0 p-3 bg-white" style="height: 350px;">
<h5 class="card-title font-weight-bold">Program-wise Distribution</h5>
<div class="position-relative flex-grow-1" style="height: 280px;">
<canvas t-ref="programChart"/>
<!-- ── CHARTS + QUICK ACTIONS ─────────────────────────────── -->
<div style="display:grid;grid-template-columns:5fr 4fr 3fr;gap:14px;margin-bottom:20px;">
<!-- Enrollment Trend -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-bar-chart" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Student Enrollment Trend</span>
</div>
<span style="font-size:0.72rem;color:#64748b;background:#f8fafc;padding:4px 10px;border-radius:20px;border:1px solid #e2e8f0;">This Academic Year</span>
</div>
<div style="height:210px;position:relative;"><canvas t-ref="enrollmentChart"/></div>
<div style="display:flex;gap:16px;margin-top:10px;">
<div style="display:flex;align-items:center;gap:5px;font-size:0.72rem;color:#64748b;">
<div style="width:9px;height:9px;border-radius:50%;background:#0d9488;"></div>New Admissions
</div>
<div class="col-md-4 mb-2">
<div class="card shadow-sm border-0 p-3 bg-white" style="height: 350px;">
<h5 class="card-title font-weight-bold">Campus Distribution</h5>
<div class="position-relative flex-grow-1" style="height: 280px;">
<canvas t-ref="campusChart"/>
</div>
<div style="display:flex;align-items:center;gap:5px;font-size:0.72rem;color:#64748b;">
<div style="width:9px;height:9px;border-radius:50%;background:#1f8f8f;"></div>Total Students
</div>
</div>
</div>
<!-- Academic KPIs -->
<div class="row mb-4 mx-1 text-center">
<div class="col-md-3 mb-2" t-on-click="openPrograms" style="cursor: pointer;">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-muted small text-uppercase">Programs</div>
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.programs_count || 0"/></h3>
<!-- Student Distribution -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-pie-chart" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Student Distribution</span>
</div>
<span style="font-size:0.72rem;color:#64748b;background:#f8fafc;padding:4px 10px;border-radius:20px;border:1px solid #e2e8f0;">By Program</span>
</div>
<div style="height:160px;position:relative;"><canvas t-ref="distributionChart"/></div>
<div style="margin-top:12px;display:flex;flex-direction:column;gap:5px;">
<t t-foreach="(state.kpis.program_chart || []).slice(0,5)" t-as="p" t-key="p_index">
<div style="display:flex;align-items:center;justify-content:space-between;font-size:0.73rem;">
<div style="display:flex;align-items:center;gap:6px;color:#475569;min-width:0;">
<div t-attf-style="width:8px;height:8px;border-radius:50%;background:{{['#1f8f8f','#0d9488','#0891b2','#7c3aed','#ea580c'][p_index % 5]}};flex-shrink:0;"></div>
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><t t-esc="p.name"/></span>
</div>
<span style="font-weight:700;color:#0f172a;flex-shrink:0;margin-left:6px;"><t t-esc="p.count"/></span>
</div>
</t>
<t t-if="!(state.kpis.program_chart || []).length">
<div style="text-align:center;color:#94a3b8;font-size:0.78rem;padding:10px 0;">No program data yet</div>
</t>
</div>
</div>
<div class="col-md-3 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-muted small text-uppercase">Departments</div>
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.dept_count || 0"/></h3>
<!-- Quick Actions -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">
<i class="fa fa-bolt" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Quick Actions</span>
</div>
</div>
<div class="col-md-3 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-muted small text-uppercase">Campuses</div>
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.campus_count || 0"/></h3>
</div>
</div>
<div class="col-md-3 mb-2">
<div class="card shadow-sm border-0 bg-white p-3">
<div class="text-muted small text-uppercase">Academic Years</div>
<h3 class="font-weight-bold mb-0 text-primary"><t t-esc="state.kpis.years_count || 0"/></h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<a class="edu-qbtn" href="#" t-on-click.prevent="openAddStudent">
<i class="fa fa-user-plus" style="color:#1f8f8f;"/><span>Add Student</span>
</a>
<a class="edu-qbtn" href="#" t-on-click.prevent="openAddFaculty">
<i class="fa fa-users" style="color:#7c3aed;"/><span>Add Faculty</span>
</a>
<a class="edu-qbtn" href="#" t-on-click.prevent="openCreateCourse">
<i class="fa fa-book" style="color:#0369a1;"/><span>Create Course</span>
</a>
<a class="edu-qbtn" href="#" t-on-click.prevent="openScheduleExam">
<i class="fa fa-calendar-plus-o" style="color:#ea580c;"/><span>Schedule Exam</span>
</a>
<a class="edu-qbtn" href="#" t-on-click.prevent="openAttendance">
<i class="fa fa-check-square-o" style="color:#16a34a;"/><span>Attendance</span>
</a>
<a class="edu-qbtn" href="#" t-on-click.prevent="openFees">
<i class="fa fa-money" style="color:#ca8a04;"/><span>Collect Fees</span>
</a>
</div>
</div>
</div>
<!-- ── BOTTOM ROW ─────────────────────────────────────────── -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:20px;">
<!-- Upcoming Examinations -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-calendar" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Upcoming Examinations</span>
</div>
<span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;" t-on-click="openScheduleExam">View All</span>
</div>
<t t-if="(state.kpis.upcoming_exams || []).length">
<t t-foreach="state.kpis.upcoming_exams || []" t-as="ex" t-key="ex_index">
<div class="edu-exam-row">
<div style="width:4px;height:38px;border-radius:4px;background:#1f8f8f;flex-shrink:0;"></div>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:700;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><t t-esc="ex.name"/></div>
<div style="font-size:0.7rem;color:#64748b;"><t t-esc="ex.program"/></div>
</div>
<div style="text-align:right;flex-shrink:0;">
<div style="font-size:0.7rem;color:#1f8f8f;font-weight:700;"><t t-esc="ex.date"/></div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;"/>
</div>
</div>
</t>
</t>
<t t-else="">
<div style="text-align:center;padding:28px 0;color:#94a3b8;">
<i class="fa fa-calendar-o" style="font-size:2rem;display:block;margin-bottom:8px;"/>
<div style="font-size:0.8rem;">No upcoming exams scheduled</div>
</div>
</t>
</div>
<!-- Fee Collection Overview -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-inr" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Fee Collection Overview</span>
</div>
<span style="font-size:0.72rem;color:#64748b;background:#f8fafc;padding:4px 10px;border-radius:20px;border:1px solid #e2e8f0;">This Academic Year</span>
</div>
<div style="display:flex;align-items:center;gap:20px;margin-bottom:14px;">
<div>
<div style="font-size:1.7rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="this.fmt(state.kpis.collected_fees || 0)"/></div>
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:3px;">Collected</div>
</div>
<div style="position:relative;width:76px;height:76px;flex-shrink:0;margin-left:auto;">
<canvas t-ref="feeDonutChart" width="76" height="76"/>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:0.78rem;font-weight:800;color:#1f8f8f;">
<t t-esc="(state.kpis.fee_percent || 0) + '%'"/>
</div>
</div>
</div>
<div style="height:7px;background:#e2e8f0;border-radius:20px;margin-bottom:14px;overflow:hidden;">
<div t-attf-style="height:100%;background:linear-gradient(90deg,#1f8f8f,#0d9488);border-radius:20px;width:{{state.kpis.fee_percent || 0}}%;transition:width .5s ease;"></div>
</div>
<div style="display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;justify-content:space-between;font-size:0.78rem;">
<div style="display:flex;align-items:center;gap:6px;color:#475569;"><div style="width:8px;height:8px;border-radius:50%;background:#1f8f8f;"></div>Collected</div>
<span style="font-weight:700;color:#0f172a;"><t t-esc="this.fmt(state.kpis.collected_fees || 0)"/></span>
</div>
<div style="display:flex;justify-content:space-between;font-size:0.78rem;">
<div style="display:flex;align-items:center;gap:6px;color:#475569;"><div style="width:8px;height:8px;border-radius:50%;background:#fca5a5;"></div>Pending</div>
<span style="font-weight:700;color:#0f172a;"><t t-esc="this.fmt(state.kpis.outstanding_fees || 0)"/></span>
</div>
<div style="display:flex;justify-content:space-between;font-size:0.78rem;">
<div style="display:flex;align-items:center;gap:6px;color:#475569;"><div style="width:8px;height:8px;border-radius:50%;background:#e2e8f0;"></div>Total</div>
<span style="font-weight:700;color:#0f172a;"><t t-esc="this.fmt(state.kpis.total_fees || 0)"/></span>
</div>
</div>
</div>
<!-- Announcements -->
<div class="edu-card" style="padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-bullhorn" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Announcements</span>
</div>
<span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;">View All</span>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#fee2e2;color:#b91c1c;margin-top:1px;">Important</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Exam Schedule Released</div>
<div style="font-size:0.7rem;color:#94a3b8;">Mid-term schedule has been published.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#e0f2fe;color:#0369a1;margin-top:1px;">General</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Library Hours Extended</div>
<div style="font-size:0.7rem;color:#94a3b8;">Open till 8 PM this week.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#d1fae5;color:#065f46;margin-top:1px;">Academic</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Course Registration Open</div>
<div style="font-size:0.7rem;color:#94a3b8;">Elective course registration is now open.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#fef9c3;color:#a16207;margin-top:1px;">Event</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Annual Tech Fest</div>
<div style="font-size:0.7rem;color:#94a3b8;">Scheduled for next month.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
</div>
</div>
<!-- ── RECENT ACTIVITIES ──────────────────────────────────── -->
<div class="edu-card" style="padding:16px 20px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;">
<i class="fa fa-history" style="color:#1f8f8f;"/>
<span style="font-weight:700;color:#0f172a;font-size:0.88rem;">Recent Activities</span>
</div>
<span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;">View All</span>
</div>
<div style="display:flex;overflow-x:auto;padding-bottom:4px;gap:0;">
<t t-if="(state.kpis.recent_activities || []).length">
<t t-foreach="state.kpis.recent_activities || []" t-as="act" t-key="act_index">
<div style="min-width:190px;padding-right:20px;border-right:1px solid #f1f5f9;margin-right:20px;flex-shrink:0;">
<div style="display:flex;align-items:flex-start;gap:6px;margin-bottom:3px;">
<div style="width:8px;height:8px;border-radius:50%;background:#1f8f8f;flex-shrink:0;margin-top:3px;"></div>
<span style="font-size:0.78rem;color:#334155;font-weight:500;line-height:1.35;"><t t-esc="act.text"/></span>
</div>
<div style="font-size:0.7rem;color:#94a3b8;padding-left:14px;"><t t-esc="act.time"/></div>
</div>
</t>
</t>
<t t-else="">
<t t-foreach="[['New student admission - Priya Sharma','10:30 AM'],['Exam schedule updated','09:15 AM'],['Fee payment received - ADM/0019','08:45 AM'],['New faculty added - Dr. Karthik','08:20 AM'],['Course material uploaded - Data Structures','Yesterday']]" t-as="act" t-key="act_index">
<div style="min-width:190px;padding-right:20px;border-right:1px solid #f1f5f9;margin-right:20px;flex-shrink:0;">
<div style="display:flex;align-items:flex-start;gap:6px;margin-bottom:3px;">
<div style="width:8px;height:8px;border-radius:50%;background:#1f8f8f;flex-shrink:0;margin-top:3px;"></div>
<span style="font-size:0.78rem;color:#334155;font-weight:500;line-height:1.35;"><t t-esc="act[0]"/></span>
</div>
<div style="font-size:0.7rem;color:#94a3b8;padding-left:14px;"><t t-esc="act[1]"/></div>
</div>
</t>
</t>
</div>
</div>
</div>
</t>
</templates>

View File

@ -6,11 +6,15 @@
<field name="tag">education_dashboard_main_tag</field>
</record>
<!-- Top-level Menu Item -->
<menuitem id="menu_education_dashboard_root"
<!-- Dashboard Menu -->
<menuitem id="menu_education_dashboard"
name="Dashboard"
parent="education_base.menu_education_base_root"
action="action_education_dashboard_main"
sequence="1"
groups="education_student.group_education_student_manager"/>
action="action_education_dashboard_main"/>
<!-- Explicitly clear any action bound to the root menu (from previous bug) so submenus render! -->
<record id="education_base.menu_education_base_root" model="ir.ui.menu">
<field name="action" eval="False"/>
</record>
</odoo>

View File

@ -306,7 +306,7 @@
<!-- Right Panel Form -->
<div class="login-right-panel">
<div class="login-card-box">
<div class="login-card-title">Welcome Back!</div>
<div class="login-card-title">Welcome!</div>
<div class="login-card-subtitle">Sign in to access your student portal workspace.</div>
<!-- Odoo Login Form -->

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,30 @@
{
'name': 'Student ERP Custom Theme',
'version': '1.0',
'summary': 'Material Design and Brand Color Overrides for Student ERP',
'description': """
This module overrides Odoo's default styling to provide a Material Design
feel to backend forms and applies brand-specific colors across both
the backend dashboard and the frontend student portal.
""",
'category': 'Theme',
'author': 'Antigravity',
'depends': ['base', 'web', 'portal'],
'data': [],
'assets': {
'web._assets_primary_variables': [
'theme_student_erp_custom/static/src/scss/primary_variables.scss',
],
'web.assets_backend': [
'theme_student_erp_custom/static/src/scss/backend_forms.scss',
'theme_student_erp_custom/static/src/xml/sidebar.xml',
],
'web.assets_frontend': [
'theme_student_erp_custom/static/src/scss/frontend_portal.scss',
],
},
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,50 @@
/** @odoo-module **/
import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { WebClient } from "@web/webclient/webclient";
import { registry } from "@web/core/registry";
export class Sidebar extends Component {
setup() {
this.menuService = useService("menu");
this.actionService = useService("action");
this.state = useState({
openFolders: {},
activeMenuId: null
});
// Initialize safely
try {
const currentApp = this.menuService.getCurrentApp();
if (currentApp) {
this.state.activeMenuId = currentApp.id;
}
} catch (e) {
console.warn("Could not load initial sidebar state:", e);
}
}
get currentApp() {
return this.menuService.getCurrentApp();
}
get menuTree() {
const app = this.currentApp;
if (!app) return [];
return this.menuService.getMenuAsTree(app.id).childrenTree;
}
toggleAccordion(menuId) {
this.state.openFolders[menuId] = !this.state.openFolders[menuId];
}
onMenuClick(menu) {
this.state.activeMenuId = menu.id;
this.menuService.selectMenu(menu);
}
}
Sidebar.template = "theme_student_erp_custom.Sidebar";

View File

@ -0,0 +1,645 @@
// ---------------------------------------------------------
// COMPREHENSIVE BACKEND THEME & FORM OVERHAUL (ODOO 18/19)
// ---------------------------------------------------------
// 1. GLOBAL ROOT VARIABLES (Modern Odoo uses CSS variables extensively)
// ---------------------------------------------------------
:root {
--o-brand-primary: #1f8f8f;
--o-brand-odoo: #1f8f8f;
--bs-primary: #1f8f8f;
--bs-primary-rgb: 37, 99, 235;
// Override the default purple/dark background of Odoo
--o-navbar-background: #1f8f8f;
--o-navbar-text: #ffffff;
}
// Global Background & Structural Layout
body.o_web_client {
background-color: #f0f4f8 !important;
}
.o_action_manager {
background-color: #f0f4f8 !important;
}
// Only push content when our custom sidebar is present
body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
margin-left: 260px;
}
.o_content, .o_form_view_container {
background-color: #f0f4f8 !important;
}
// 2. MAIN NAVBAR
// ---------------------------------------------------------
.o_main_navbar {
--o-navbar-height: 70px;
font-size: 0.975rem;
background-color: #eef9f9 !important;
border-bottom: 1px solid #e2e8f0 !important;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05) !important;
color: #144d4d !important;
width: 100%;
z-index: 100; // Keep above sidebar
// Ensure all internal sections don't have the ugly purple background
.o_menu_sections, .o_menu_systray, .o_menu_brand {
background-color: transparent !important;
}
// Ensure horizontal menus are always visible in the header
.o_menu_sections {
display: flex !important;
flex-grow: 1 !important;
align-items: center !important;
overflow: visible !important;
}
// Explicitly override Odoo's internal enterprise variables
.o_menu_sections .o_nav_entry, .o_menu_sections .dropdown-toggle {
background: transparent !important;
background-color: transparent !important;
}
// Navbar Links
.o_menu_item > a, .o_nav_entry, .dropdown-toggle {
color: #144d4d !important;
font-weight: 600 !important;
&:hover, &:focus {
color: #144d4d !important;
background-color: rgba(20, 77, 77, 0.05) !important; // Very light teal hover
}
}
// App Switcher Icon (Grid)
.o_menu_toggle {
color: #144d4d !important;
&:hover {
background-color: rgba(20, 77, 77, 0.1) !important;
}
}
// Brand Name (e.g. Education)
.o_menu_brand {
color: #144d4d !important;
font-weight: 800 !important;
}
}
// ---------------------------------------------------------
// APP SWITCHER & GLOBAL DROPDOWNS (Odoo appends these to body)
// ---------------------------------------------------------
.dropdown-menu, .o-dropdown--menu {
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
padding: 8px !important;
margin-top: 8px !important;
.dropdown-item, .o_nav_entry, .dropdown-item-text {
color: #475569 !important;
border-radius: 8px !important;
padding: 10px 20px !important;
font-weight: 500 !important;
font-size: 0.95rem;
transition: all 0.15s ease-in-out;
&:hover, &:focus, &.active, &.selected, &.focus {
background-color: #eef9f9 !important;
color: #144d4d !important;
}
}
}
// 3. CONTROL PANEL
// ---------------------------------------------------------
.o_control_panel {
background-color: #ffffff !important;
border-bottom: 1px solid #e2e8f0 !important;
padding: 12px 24px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
.o_breadcrumb {
font-family: 'Inter', sans-serif;
font-weight: 700;
color: #1e293b;
}
.btn-primary {
background-color: var(--o-brand-primary) !important;
border-color: var(--o-brand-primary) !important;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2);
}
}
// 4. FORM VIEW OVERHAUL
// ---------------------------------------------------------
.o_form_view {
background-color: #f0f4f8 !important; // Match global background
// The entire left side (Sheet + Statusbar)
.o_form_sheet_bg {
background-color: transparent !important;
border-bottom: none !important;
padding: 24px !important;
}
// Status Bar (Top bar with Draft/Open/Done and action buttons)
.o_form_statusbar {
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px;
margin-bottom: 16px !important;
padding: 8px 16px !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
// The arrow steps
.o_statusbar_status {
.o_arrow_button {
background: transparent !important;
border: none !important;
color: #64748b !important;
font-weight: 600;
&::before, &::after { display: none !important; } // Remove standard chevron shapes
&.btn-primary {
color: var(--o-brand-primary) !important;
background-color: rgba(37, 99, 235, 0.1) !important;
border-radius: 20px !important;
padding: 4px 16px !important;
}
}
}
// Action Buttons inside statusbar
.btn {
border-radius: 6px;
font-weight: 500;
}
.btn-primary {
background-color: var(--o-brand-primary) !important;
border: none !important;
}
}
// The Main Card (Sheet)
.o_form_sheet {
background-color: #ffffff !important;
border-radius: 12px !important;
border: 1px solid #e2e8f0 !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025) !important;
margin: 0 auto !important;
padding: 40px !important;
max-width: 1000px !important; // keep it readable
}
// Labels
.o_form_label {
color: #64748b !important;
font-weight: 700 !important;
font-size: 0.75rem !important;
text-transform: uppercase;
letter-spacing: 0.05em;
}
// Input fields (Modern SaaS style - clean gray boxes that highlight)
.o_field_widget {
input, select, textarea {
background-color: #f8fafc !important;
border: 1px solid #e2e8f0 !important;
border-radius: 6px !important;
color: #0f172a !important;
padding: 8px 12px !important;
transition: all 0.2s ease;
box-shadow: none !important;
&:focus, &:active {
background-color: #ffffff !important;
border-color: var(--o-brand-primary) !important;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
outline: none !important;
}
}
}
// Group titles / Separators
.o_group .o_td_label {
border-right: none !important; // Remove default odoo vertical line
}
.o_horizontal_separator {
color: var(--o-brand-primary) !important;
font-weight: 700;
font-size: 1.1rem;
border-bottom: 2px solid #f1f5f9;
padding-bottom: 8px;
margin-bottom: 16px;
}
}
// 5. CHATTER (The right-side communication panel)
// ---------------------------------------------------------
.o-mail-Form-chatter {
background-color: #f8fafc !important;
border-left: 1px solid #e2e8f0 !important;
.o-mail-Chatter-topbar {
background-color: #ffffff !important;
border-bottom: 1px solid #e2e8f0 !important;
padding: 12px !important;
.btn {
border-radius: 6px !important;
&.active {
background-color: rgba(37, 99, 235, 0.1) !important;
color: var(--o-brand-primary) !important;
border-color: transparent !important;
}
}
}
}
// ==========================================================
// 6. CONTENT SECTION — PROFESSIONAL LIST VIEW OVERHAUL
// ==========================================================
// --- Page background ---
.o_action_manager {
background-color: #eef9f9 !important;
}
.o_home_menu { background-color: #eef9f9 !important; }
// ---- CONTROL PANEL ----
.o_control_panel {
background-color: transparent !important;
border-bottom: none !important;
box-shadow: none !important;
padding: 20px 28px 8px 28px !important;
}
// Top row: breadcrumb on left, New button on RIGHT
.o_control_panel_breadcrumbs_actions {
display: flex !important;
align-items: center !important;
flex-wrap: nowrap !important;
gap: 12px !important;
}
.o_control_panel .o_breadcrumb {
order: 1 !important; // Always visually FIRST (left side)
flex: 1 !important; // Takes all remaining space → pushes button right
display: flex !important;
align-items: center !important;
flex-wrap: wrap !important;
gap: 4px !important;
min-width: 0 !important;
}
// Small breadcrumb trail (Home > Master Data >)
.o_control_panel .o_breadcrumb .o_breadcrumb_item a,
.o_control_panel .o_breadcrumb .o_breadcrumb_item > span:not(.o_last_breadcrumb_item span) {
font-size: 0.8rem !important;
color: #64748b !important;
font-weight: 500 !important;
text-decoration: none !important;
}
// Large title (last breadcrumb item)
.o_control_panel .o_last_breadcrumb_item > span,
.o_control_panel .o_breadcrumb li.active > span {
font-size: 1.45rem !important;
font-weight: 800 !important;
color: #0f172a !important;
letter-spacing: -0.025em !important;
}
// Breadcrumb separator
.o_control_panel .o_breadcrumb li + li::before {
color: #cbd5e1 !important;
}
// New button → RIGHT side, teal — uses order:2 to override DOM order
.o_control_panel_main_buttons {
order: 2 !important; // Always visually LAST (right side)
flex-shrink: 0 !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
.btn-primary {
background-color: #1f8f8f !important;
border-color: #1f8f8f !important;
border-radius: 10px !important;
font-weight: 600 !important;
font-size: 0.875rem !important;
padding: 9px 20px !important;
color: #fff !important;
box-shadow: 0 4px 14px rgba(31,143,143,0.3) !important;
transition: all 0.2s ease !important;
&:hover {
background-color: #177a7a !important;
transform: translateY(-1px) !important;
box-shadow: 0 6px 18px rgba(31,143,143,0.4) !important;
}
}
.btn-secondary {
background-color: #fff !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 10px !important;
color: #475569 !important;
font-weight: 600 !important;
font-size: 0.875rem !important;
padding: 9px 20px !important;
&:hover {
border-color: #1f8f8f !important;
color: #1f8f8f !important;
}
}
}
// Navigation row: search + filters + view switchers + pager
.o_control_panel_navigation {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 8px 0 0 0 !important;
flex-wrap: wrap !important;
}
// ---- SEARCH BAR ----
.o_searchview {
background-color: #fff !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 10px !important;
padding: 3px 12px !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.04) !important;
min-height: 36px !important;
flex: 1 1 220px !important;
max-width: 360px !important;
transition: border-color 0.2s, box-shadow 0.2s !important;
&:focus-within {
border-color: #1f8f8f !important;
box-shadow: 0 0 0 3px rgba(31,143,143,0.12) !important;
}
.o_searchview_input {
font-size: 0.875rem !important;
color: #334155 !important;
background: transparent !important;
&::placeholder { color: #94a3b8 !important; }
}
.o_searchview_icon, .fa-search {
color: #94a3b8 !important;
}
}
// Filter / GroupBy / Favorites dropdown buttons
.o_dropdown.o_filter_menu > button,
.o_dropdown.o_group_by_menu > button,
.o_dropdown.o_favorites_menu > button {
background-color: #fff !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 9px !important;
color: #475569 !important;
font-weight: 600 !important;
font-size: 0.8rem !important;
padding: 6px 12px !important;
transition: all 0.15s ease !important;
&:hover {
border-color: #1f8f8f !important;
color: #1f8f8f !important;
background-color: #eef9f9 !important;
}
}
// View switcher (List / Kanban)
.o_cp_switch_buttons button {
background-color: #fff !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 8px !important;
color: #94a3b8 !important;
padding: 5px 9px !important;
transition: all 0.15s ease !important;
&.active, &[aria-pressed="true"] {
background-color: #eef9f9 !important;
border-color: #1f8f8f !important;
color: #1f8f8f !important;
}
&:hover:not(.active):not([aria-pressed="true"]) {
border-color: #cbd5e1 !important;
color: #475569 !important;
}
}
// Pager in control panel navigation
.o_control_panel_navigation .o_pager {
display: flex !important;
align-items: center !important;
gap: 4px !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
.o_pager_counter {
font-size: 0.8rem !important;
color: #64748b !important;
font-weight: 500 !important;
white-space: nowrap !important;
}
.o_pager_value {
background-color: #1f8f8f !important;
color: #fff !important;
border: none !important;
border-radius: 7px !important;
padding: 3px 10px !important;
font-size: 0.8rem !important;
font-weight: 700 !important;
cursor: default !important;
min-width: 28px !important;
text-align: center !important;
}
.o_pager_previous, .o_pager_next {
background: #fff !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 7px !important;
color: #475569 !important;
padding: 3px 8px !important;
font-size: 0.82rem !important;
transition: all 0.15s ease !important;
&:hover {
border-color: #1f8f8f !important;
color: #1f8f8f !important;
}
}
}
// ---- LIST VIEW — TABLE CARD ----
// o_list_view = full page wrapper, do NOT add card here
.o_list_view {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
margin: 0 !important;
overflow: visible !important;
}
// o_list_renderer = actual table container → THIS is the white card
.o_list_renderer {
background-color: #fff !important;
border-radius: 14px !important;
border: 1px solid #e2e8f0 !important;
box-shadow: 0 4px 20px rgba(15,23,42,0.07) !important;
margin: 0 28px 28px 28px !important;
overflow: hidden !important;
}
// ---- TABLE HEADER ----
.o_list_renderer table {
border-collapse: collapse !important;
width: 100% !important;
}
.o_list_renderer thead th {
background-color: #f8fafc !important;
color: #64748b !important;
font-size: 0.72rem !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.07em !important;
padding: 12px 14px !important;
border-bottom: 1.5px solid #e2e8f0 !important;
border-top: none !important;
white-space: nowrap !important;
}
// ---- TABLE ROWS ----
.o_list_renderer .o_data_row {
border-bottom: 1px solid #f1f5f9 !important;
transition: background 0.12s ease !important;
&:last-child { border-bottom: none !important; }
&:hover > td {
background-color: #f8fafc !important;
}
> td {
padding: 13px 14px !important;
font-size: 0.875rem !important;
color: #475569 !important;
vertical-align: middle !important;
border: none !important;
background-color: transparent !important;
}
// First data column (record name) → bold
> td.o_data_cell:nth-child(2) {
font-weight: 700 !important;
color: #0f172a !important;
font-size: 0.9rem !important;
}
}
// ---- STATUS BADGE widget="badge" ----
.o_list_renderer .badge,
.o_field_badge .badge {
border-radius: 20px !important;
padding: 4px 12px !important;
font-size: 0.75rem !important;
font-weight: 600 !important;
letter-spacing: 0.02em !important;
border: none !important;
}
.badge.text-bg-success,
.o_field_badge .o_tag.text-bg-success { background-color: #dcfce7 !important; color: #15803d !important; }
.badge.text-bg-danger,
.o_field_badge .o_tag.text-bg-danger { background-color: #fee2e2 !important; color: #b91c1c !important; }
.badge.text-bg-warning,
.o_field_badge .o_tag.text-bg-warning { background-color: #fef9c3 !important; color: #a16207 !important; }
.badge.text-bg-secondary,
.o_field_badge .o_tag.text-bg-secondary{ background-color: #f1f5f9 !important; color: #475569 !important; }
.badge.text-bg-info,
.o_field_badge .o_tag.text-bg-info { background-color: #e0f2fe !important; color: #0369a1 !important; }
.badge.text-bg-primary,
.o_field_badge .o_tag.text-bg-primary { background-color: #e0f7f7 !important; color: #0f6b6b !important; }
// ---- CHECKBOXES ----
.o_list_renderer input[type="checkbox"] {
accent-color: #1f8f8f !important;
width: 15px !important;
height: 15px !important;
cursor: pointer !important;
}
// Optional columns toggle
.o_optional_columns_dropdown_toggle {
color: #94a3b8 !important;
background: transparent !important;
border: none !important;
&:hover { color: #1f8f8f !important; }
}
// ---- KANBAN VIEW ----
.o_kanban_view {
background-color: #eef9f9 !important;
padding: 20px 28px !important;
.o_kanban_record {
background: #fff !important;
border-radius: 12px !important;
border: 1px solid #e8eef4 !important;
box-shadow: 0 2px 10px rgba(15,23,42,0.06) !important;
transition: box-shadow 0.2s ease, transform 0.2s ease !important;
&:hover {
box-shadow: 0 10px 28px rgba(15,23,42,0.12) !important;
transform: translateY(-2px) !important;
}
}
}
// ---- GLOBAL DROPDOWNS ----
.dropdown-menu, .o-dropdown--menu {
background-color: #fff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
box-shadow: 0 10px 30px rgba(15,23,42,0.12) !important;
padding: 6px !important;
.dropdown-item {
border-radius: 8px !important;
font-size: 0.875rem !important;
color: #475569 !important;
padding: 8px 14px !important;
font-weight: 500 !important;
transition: all 0.12s ease !important;
&:hover, &:focus {
background-color: #eef9f9 !important;
color: #1f8f8f !important;
}
}
}

View File

@ -0,0 +1,56 @@
// Frontend Portal Overrides
body {
// Subtle background for the portal to make content cards pop
background-color: #f8fafc; // Slate 50
}
// Portal Navbar overrides
header.o_header_standard, .navbar {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
.navbar-brand {
font-weight: bold;
color: $o-brand-primary;
}
.nav-link {
font-weight: 500;
&:hover, &.active {
color: $o-brand-primary !important;
}
}
}
// Enhance standard portal cards
.o_portal_wrap {
.card {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: none;
border-radius: 8px;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #ffffff;
border-bottom: 1px solid #f1f5f9;
font-weight: bold;
color: $o-brand-secondary;
}
}
}
// Primary buttons in the portal
main .btn-primary {
border-radius: 6px;
font-weight: 500;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
}

View File

@ -0,0 +1,18 @@
// Brand Colors - Override Bootstrap default variables
// We are applying a modern, professional blue theme as a baseline.
// You can adjust these hex codes to perfectly match your brand.
$o-brand-primary: #1f8f8f; // Modern Indigo/Blue
$o-brand-secondary: #475569; // Slate Grey
$o-brand-info: #26a1a1; // Lighter Blue
$o-brand-success: #10B981; // Emerald Green
$o-brand-warning: #F59E0B; // Amber
$o-brand-danger: #EF4444; // Red
// For frontend overriding, Odoo often uses $primary directly
$primary: $o-brand-primary;
$secondary: $o-brand-secondary;
$info: $o-brand-info;
$success: $o-brand-success;
$warning: $o-brand-warning;
$danger: $o-brand-danger;

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- Inherit the NavBar safely and inject Education Sidebar -->
<t t-inherit="web.NavBar" t-inherit-mode="extension">
<xpath expr="//nav" position="inside">
<t t-if="currentApp and currentApp.name === 'Education'">
<div class="o_sidebar_custom d-flex flex-column position-fixed"
style="top: 70px; bottom: 0; left: 0; width: 260px; background: white; border-right: 1px solid #e2e8f0; z-index: 90; padding-top: 10px; overflow-y: auto; box-shadow: 2px 0 10px rgba(0,0,0,0.03);">
<style>
.o_sidebar_custom .sidebar-link {
display: flex; align-items: center; padding: 8px 16px;
margin: 2px 8px; border-radius: 8px; color: #475569;
text-decoration: none; font-weight: 600; font-size: 0.9rem;
transition: all 0.2s ease; cursor: pointer;
}
.o_sidebar_custom .sidebar-link:hover { background-color: #eef9f9; color: #144d4d; }
.o_sidebar_custom .sidebar-group-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 16px; margin: 2px 8px; border-radius: 8px;
color: #475569; font-weight: 600; font-size: 0.9rem;
cursor: pointer; transition: all 0.2s ease;
}
.o_sidebar_custom .sidebar-group-header:hover { background-color: #eef9f9; color: #144d4d; }
.o_sidebar_custom .sidebar-group-header .chevron {
font-size: 0.7rem; transition: transform 0.25s ease;
}
.o_sidebar_custom .sidebar-group-header.open .chevron { transform: rotate(180deg); }
.o_sidebar_custom .sidebar-children {
display: none; flex-direction: column;
margin-left: 24px; padding-left: 12px;
border-left: 2px solid #e2e8f0; margin-right: 8px; margin-bottom: 4px;
}
.o_sidebar_custom .sidebar-children.open { display: flex; }
.o_sidebar_custom .sidebar-child-link {
display: flex; align-items: center; padding: 6px 12px;
margin: 1px 0; border-radius: 6px; color: #64748b;
text-decoration: none; font-size: 0.85rem; font-weight: 500;
transition: all 0.15s ease; cursor: pointer;
}
.o_sidebar_custom .sidebar-child-link:hover { background-color: #eef9f9; color: #144d4d; }
.o_sidebar_custom .sidebar-dot {
width: 5px; height: 5px; border-radius: 50%;
background: #94a3b8; margin-right: 10px; flex-shrink: 0;
}
</style>
<!-- App Header -->
<div class="px-4 py-3 mb-2 border-bottom d-flex align-items-center">
<i class="fa fa-graduation-cap fa-lg me-3" style="color: #1f8f8f;"/>
<h5 class="mb-0 fw-bold" style="color: #144d4d;"><t t-esc="currentApp.name"/></h5>
</div>
<!-- Menu items loop -->
<t t-foreach="currentAppSections" t-as="section" t-key="section.id">
<t t-if="!section.childrenTree.length">
<a class="sidebar-link"
t-on-click.prevent="() => this.onNavBarDropdownItemSelection(section)"
t-att-href="getMenuItemHref(section)">
<i class="fa fa-list-ul me-3" style="color: #94a3b8; width: 16px;"/>
<t t-esc="section.name"/>
</a>
</t>
<t t-else="">
<div class="sidebar-group-header"
t-on-click="(ev) => { ev.currentTarget.classList.toggle('open'); ev.currentTarget.nextElementSibling.classList.toggle('open'); }">
<div class="d-flex align-items-center">
<i class="fa fa-folder-o me-3" style="color: #94a3b8; width: 16px;"/>
<t t-esc="section.name"/>
</div>
<i class="fa fa-chevron-down chevron"/>
</div>
<div class="sidebar-children">
<t t-foreach="section.childrenTree" t-as="child" t-key="child.id">
<a class="sidebar-child-link"
t-on-click.prevent="() => this.onNavBarDropdownItemSelection(child)"
t-att-href="getMenuItemHref(child)">
<span class="sidebar-dot"/>
<t t-esc="child.name"/>
</a>
</t>
</div>
</t>
</t>
</div>
</t>
</xpath>
</t>
</templates>