Add CEO dashboard company selector
This commit is contained in:
parent
315ab7497a
commit
27778a7901
@ -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"))
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -13,10 +13,22 @@
|
||||
<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>
|
||||
<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">
|
||||
<div t-attf-class="o_mcs_coverage o_mcs_status_{{ state.data.company_pulse.revenue_coverage_status }}">
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user