Add CEO dashboard company selector

This commit is contained in:
metatroncubeswdev 2026-08-31 11:01:49 -04:00
parent 315ab7497a
commit 27778a7901
4 changed files with 79 additions and 15 deletions

View File

@ -181,13 +181,13 @@ class McsOperatingDashboard(models.Model):
return True
@api.model
def get_ceo_dashboard_v2_data(self):
def get_ceo_dashboard_v2_data(self, selected_company_ids=None):
today = fields.Date.context_today(self)
month_start = today.replace(day=1)
month_end = month_start + relativedelta(months=1, days=-1)
week_start = today - relativedelta(days=today.weekday())
week_end = week_start + relativedelta(days=6)
company_ids = self._dashboard_company_ids()
company_ids = self._dashboard_company_ids(selected_company_ids)
Lead = self.env["crm.lead"]
Project = self.env["project.project"]
@ -275,7 +275,7 @@ class McsOperatingDashboard(models.Model):
cash_collected = sum(
invoice.amount_total - invoice.amount_residual for invoice in invoices_month
)
operating_cost = self._monthly_operating_cost()
operating_cost = self._monthly_operating_cost(company_ids)
revenue_gap = max(monthly_target - contracted_revenue, 0)
open_pipeline_value = sum(open_pipeline.mapped("expected_revenue"))
weighted_pipeline = sum(
@ -319,6 +319,12 @@ class McsOperatingDashboard(models.Model):
"week_end": fields.Date.to_string(week_end),
"company_ids": company_ids,
"companies": self.env["res.company"].browse(company_ids).mapped("name"),
"company_options": [
{"id": company.id, "name": company.name}
for company in self.env["res.company"].browse(
self._dashboard_company_ids()
)
],
},
"company_pulse": {
"revenue_coverage": self._format_percent(revenue_coverage),
@ -377,9 +383,20 @@ class McsOperatingDashboard(models.Model):
def _format_percent(self, value):
return round(value or 0, 1)
def _dashboard_company_ids(self):
company_ids = self.env.context.get("allowed_company_ids") or self.env.companies.ids
return company_ids or [self.env.company.id]
def _dashboard_company_ids(self, selected_company_ids=None):
allowed_company_ids = (
self.env.context.get("allowed_company_ids") or self.env.companies.ids
)
allowed_company_ids = allowed_company_ids or [self.env.company.id]
if not selected_company_ids:
return allowed_company_ids
selected_company_ids = [int(company_id) for company_id in selected_company_ids]
selected_company_ids = [
company_id
for company_id in selected_company_ids
if company_id in allowed_company_ids
]
return selected_company_ids or allowed_company_ids
def _company_domain(self, Model, company_ids, include_shared=True):
if "company_id" not in Model._fields:
@ -388,16 +405,15 @@ class McsOperatingDashboard(models.Model):
return ["|", ("company_id", "=", False), ("company_id", "in", company_ids)]
return [("company_id", "in", company_ids)]
def _monthly_operating_cost(self):
def _monthly_operating_cost(self, company_ids=None):
try:
Contract = self.env["hr.contract"]
except KeyError:
return 0
company_ids = company_ids or self._dashboard_company_ids()
contracts = Contract.sudo().search(
[("state", "=", "open"), ("employee_id.active", "=", True)]
+ self._company_domain(
Contract, self._dashboard_company_ids(), include_shared=True
)
+ self._company_domain(Contract, company_ids, include_shared=True)
)
return sum(contracts.mapped("wage"))

View File

@ -8,7 +8,7 @@ export class McsCeoDashboardV2 extends Component {
setup() {
this.action = useService("action");
this.orm = useService("orm");
this.state = useState({ data: null, loading: true });
this.state = useState({ data: null, loading: true, selectedCompanyId: "all" });
onWillStart(async () => {
await this.loadDashboard();
@ -17,14 +17,31 @@ export class McsCeoDashboardV2 extends Component {
async loadDashboard() {
this.state.loading = true;
const selectedCompanyIds = this.selectedCompanyIds();
this.state.data = await this.orm.call(
"mcs.operating.dashboard",
"get_ceo_dashboard_v2_data",
[]
[selectedCompanyIds]
);
this.state.loading = false;
}
selectedCompanyIds() {
if (this.state.selectedCompanyId === "all") {
return [];
}
return [Number(this.state.selectedCompanyId)];
}
async selectCompany(companyId) {
this.state.selectedCompanyId = String(companyId);
await this.loadDashboard();
}
isCompanySelected(companyId) {
return this.state.selectedCompanyId === String(companyId);
}
openAction(actionKey) {
const action = this.state.data?.actions?.[actionKey];
if (action) {

View File

@ -33,6 +33,25 @@
}
}
.o_mcs_header_controls,
.o_mcs_company_switcher {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.o_mcs_company_switcher {
justify-content: flex-end;
.btn {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.o_mcs_pulse {
display: grid;
grid-template-columns: minmax(220px, 1.4fr) repeat(4, minmax(140px, 1fr));

View File

@ -13,9 +13,21 @@
<t t-esc="state.data.period.month_start"/>
to
<t t-esc="state.data.period.month_end"/>
-
<t t-esc="state.data.period.companies.join(', ')"/>
</span>
</div>
<button class="btn btn-primary" t-on-click="loadDashboard">Refresh</button>
<div class="o_mcs_header_controls">
<div t-if="state.data.period.company_options.length > 1" class="o_mcs_company_switcher">
<button t-attf-class="btn {{ state.selectedCompanyId === 'all' ? 'btn-primary' : 'btn-secondary' }}" t-on-click="() => this.selectCompany('all')">All</button>
<t t-foreach="state.data.period.company_options" t-as="company" t-key="company.id">
<button t-attf-class="btn {{ this.isCompanySelected(company.id) ? 'btn-primary' : 'btn-secondary' }}" t-on-click="() => this.selectCompany(company.id)">
<t t-esc="company.name"/>
</button>
</t>
</div>
<button class="btn btn-primary" t-on-click="loadDashboard">Refresh</button>
</div>
</div>
<section class="o_mcs_pulse">
@ -153,7 +165,7 @@
<t t-foreach="state.data.ceo_actions" t-as="action" t-key="action.id">
<button class="o_mcs_action_item" t-on-click="() => this.openAction('ceo_actions')">
<strong t-esc="action.title"/>
<span><t t-esc="action.category"/> · <t t-esc="action.priority"/> · <t t-esc="action.state"/></span>
<span><t t-esc="action.category"/> - <t t-esc="action.priority"/> - <t t-esc="action.state"/></span>
<small t-esc="action.next_action"/>
</button>
</t>
@ -169,7 +181,7 @@
<t t-foreach="state.data.internal_bets" t-as="project" t-key="project.id">
<button class="o_mcs_internal_item" t-on-click="() => this.openProject(project.id)">
<strong t-esc="project.name"/>
<span><t t-esc="project.stage"/> · <t t-esc="project.hours"/>h invested</span>
<span><t t-esc="project.stage"/> - <t t-esc="project.hours"/>h invested</span>
<small>Next: <t t-esc="project.next_milestone"/></small>
</button>
</t>