admin dashboard - changes
This commit is contained in:
parent
ea0da2241a
commit
6217df60b2
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Binary file not shown.
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 -->
|
||||
|
||||
1
addons/theme_student_erp_custom/__init__.py
Normal file
1
addons/theme_student_erp_custom/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
30
addons/theme_student_erp_custom/__manifest__.py
Normal file
30
addons/theme_student_erp_custom/__manifest__.py
Normal 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',
|
||||
}
|
||||
Binary file not shown.
50
addons/theme_student_erp_custom/static/src/js/sidebar.js
Normal file
50
addons/theme_student_erp_custom/static/src/js/sidebar.js
Normal 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";
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
94
addons/theme_student_erp_custom/static/src/xml/sidebar.xml
Normal file
94
addons/theme_student_erp_custom/static/src/xml/sidebar.xml
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user