Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC

This commit is contained in:
CI Publish Bot
2026-07-26 21:17:45 +00:00
commit 4f7b594ec8
1335 changed files with 191620 additions and 0 deletions

View File

@ -0,0 +1,90 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Component, useState, onWillStart } from "@odoo/owl";
import { session } from "@web/session";
export class ExecutorBoard extends Component {
setup() {
this.orm = useService("orm");
this.userId = session.user_id || session.uid || (session.storeData?.Store?.settings?.user_id?.id);
this.rawData = {
stages: [],
directives: []
};
this.state = useState({
columns: [],
loading: true,
});
onWillStart(async () => {
if (!this.userId) {
const partner = session.storeData?.["res.partner"]?.find(p => p.userId);
this.userId = partner ? partner.userId : false;
}
await this.loadData();
});
}
async loadData() {
if (!this.userId) return;
this.rawData.stages = await this.orm.searchRead(
"directive.stage",
[],
["id", "name", "code"],
);
// 2. Грузим директивы
this.rawData.directives = await this.orm.searchRead(
"directive.directive",
[["executor_user_id", "=", this.userId]],
[
"id", "name", "executor_stage_id",
"deadline_display", "time_left_display",
"planned_hours", "priority", "priority_stars", "is_overdue"
]
);
this.processData();
this.state.loading = false;
}
processData() {
this.state.columns = this.rawData.stages.map(stage => {
return {
id: stage.id,
name: stage.name,
code: stage.code,
directives: this.rawData.directives.filter(d => d.executor_stage_id && d.executor_stage_id[0] === stage.id)
};
});
}
async quickAction(ev, directiveId, actionType) {
ev.stopPropagation();
let targetCode = '';
if (actionType === 'start') targetCode = 'in_progress';
if (actionType === 'pause') targetCode = 'paused';
if (actionType === 'finish') targetCode = 'done';
if (targetCode) {
await this.orm.call("directive.directive", "action_quick_change_stage", [directiveId, targetCode]);
await this.loadData();
}
}
async openDirective(directiveId) {
}
async logout() {
window.location.href = "/web/session/logout";
}
}
ExecutorBoard.template = "directive_layer.ExecutorBoard";
registry.category("actions").add("directive_layer.executor_dashboard_action", ExecutorBoard);

View File

@ -0,0 +1,222 @@
.o_executor_dashboard {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
background-color: #f4f5f7; // Общий фон
font-family: 'Inter', system-ui, sans-serif;
// --- Header ---
.executor_header {
height: 60px;
flex-shrink: 0;
background: #ffffff;
border-bottom: 1px solid #e1e4e8;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.brand {
font-size: 1.2rem;
font-weight: 700;
color: #172b4d;
display: flex;
align-items: center;
}
.header_actions {
display: flex;
gap: 10px;
}
}
// --- Board Container ---
.executor_board_container {
flex: 1;
display: flex;
overflow-x: auto;
overflow-y: hidden;
padding: 20px;
gap: 16px;
align-items: flex-start;
// Mobile: Scroll Snap Logic
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
// --- Column ---
.kanban_column {
min-width: 300px;
max-width: 300px;
background: #ebecf0; // Цвет колонки (светлее)
border-radius: 12px;
display: flex;
flex-direction: column;
max-height: 100%;
scroll-snap-align: center; // Центровка на мобильных
.column_header {
padding: 14px 16px;
font-weight: 600;
color: #5e6c84;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid transparent; // Для цветной полоски
}
// Цветные полоски для статусов
&:nth-child(1) .column_header { border-bottom-color: #6c757d; } // Черновик
&:nth-child(2) .column_header { border-bottom-color: #0dcaf0; } // В работе
&:nth-child(3) .column_header { border-bottom-color: #ffc107; } // Ожидание
&:nth-child(4) .column_header { border-bottom-color: #198754; } // Готово
.column_content {
padding: 12px 8px 8px 8px; // ВЕРХНИЙ ОТСТУП (1)
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px; // Расстояние между карточками
}
}
}
// --- Card ---
.directive_card {
background: #ffffff; // Белый фон карточки
border-radius: 8px;
padding: 12px;
box-shadow: 0 1px 2px rgba(9, 30, 66, 0.25); // Тень
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
&:hover {
background-color: #fafbfc;
}
&.card_overdue {
border-left: 4px solid #dc3545;
background-color: #fff8f8;
}
.card_top {
display: flex;
justify-content: space-between;
align-items: center; // Выравнивание
margin-bottom: 8px;
font-size: 0.85rem;
color: #97a0af;
.card_stars {
color: #f0ad4e; // Цвет звезд (оранжевый)
font-size: 1rem;
letter-spacing: 2px;
}
}
.card_title {
font-weight: 600;
color: #172b4d;
font-size: 0.95rem;
margin-bottom: 12px;
line-height: 1.4;
}
.card_meta {
display: flex;
gap: 12px;
font-size: 0.8rem;
color: #5e6c84;
margin-bottom: 10px;
.meta_item {
display: flex;
align-items: center;
gap: 4px;
&.deadline.text-danger {
color: #dc3545;
font-weight: 600;
}
}
}
.card_actions {
display: flex;
justify-content: space-between;
gap: 8px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #ebecf0;
opacity: 0;
transition: opacity 0.2s;
.btn-action {
flex: 1;
border: none;
border-radius: 6px;
padding: 6px 10px;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
&.btn-run { background: #e6f7ff; color: #007bff; }
&.btn-run:hover { background: #007bff; color: white; }
&.btn-pause { background: #fff3cd; color: #856404; }
&.btn-pause:hover { background: #ffc107; color: white; }
&.btn-done { background: #d4edda; color: #155724; }
&.btn-done:hover { background: #28a745; color: white; }
}
}
&:hover .card_actions {
opacity: 1;
}
}
@media (max-width: 768px) {
.executor_header {
padding: 0 10px;
.brand span { display: none; }
}
.executor_board_container {
padding: 10px;
gap: 10px;
.kanban_column {
min-width: 85vw;
margin-right: 0;
}
}
.directive_card {
padding: 16px;
.card_title {
font-size: 1.05rem;
}
.card_actions {
opacity: 1 !important;
justify-content: space-between;
.btn-action {
flex: 1;
padding: 12px;
font-size: 1rem;
}
}
}
}
}

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="directive_layer.ExecutorBoard">
<div class="o_executor_dashboard">
<div class="executor_header">
<div class="brand">
<i class="fa fa-cube me-2 text-primary"/>
<span>Мои Директивы</span>
</div>
<div class="header_actions">
<button class="btn btn-sm btn-outline-secondary" t-on-click="logout">
<i class="fa fa-sign-out"/> Выход
</button>
</div>
</div>
<div class="executor_board_container">
<t t-if="state.loading">
<div class="d-flex justify-content-center w-100 mt-5">
<div class="spinner-border text-primary"/>
</div>
</t>
<t t-else="">
<t t-foreach="state.columns" t-as="column" t-key="column.id">
<div class="kanban_column">
<div class="column_header">
<span><t t-esc="column.name"/></span>
<span class="badge rounded-pill bg-white text-dark shadow-sm">
<t t-esc="column.directives.length"/>
</span>
</div>
<div class="column_content">
<t t-foreach="column.directives" t-as="card" t-key="card.id">
<div class="directive_card"
t-att-class="card.is_overdue ? 'card_overdue' : ''"
t-on-click="() => this.openDirective(card.id)">
<div class="card_top">
<span class="text-muted">#<t t-esc="card.id"/></span>
<span class="card_stars" title="Приоритет">
<t t-esc="card.priority_stars"/>
</span>
</div>
<div class="card_title">
<t t-esc="card.name"/>
</div>
<div class="card_meta">
<div class="meta_item deadline" t-att-class="card.is_overdue ? 'text-danger' : ''">
<i class="fa fa-clock-o"/>
<t t-esc="card.deadline_display"/>
</div>
<div class="meta_item" t-if="card.time_left_display">
<i class="fa fa-hourglass-start"/>
<t t-esc="card.time_left_display"/>
</div>
</div>
<div class="card_actions" t-on-click.stop="">
<t t-if="column.code == 'planned' or column.code == 'paused'">
<button class="btn-action btn-run"
t-on-click="(ev) => this.quickAction(ev, card.id, 'start')">
<i class="fa fa-play me-1"/> В работу
</button>
</t>
<t t-if="column.code == 'in_progress'">
<button class="btn-action btn-pause"
t-on-click="(ev) => this.quickAction(ev, card.id, 'pause')">
<i class="fa fa-pause me-1"/> Пауза
</button>
<button class="btn-action btn-done"
t-on-click="(ev) => this.quickAction(ev, card.id, 'finish')">
<i class="fa fa-check me-1"/> Готово
</button>
</t>
</div>
</div>
</t>
</div>
</div>
</t>
</t>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,93 @@
.o_kanban_view .directive_card {
border-radius: 16px;
padding: 18px 16px 16px 16px;
border: 1px solid rgba(0,0,0,.08);
box-shadow: 0 8px 20px rgba(0,0,0,.08);
transition: transform .15s ease, box-shadow .15s ease;
min-height: 140px;
}
.o_kanban_view .directive_card:hover {
transform: translateY(-3px);
box-shadow: 0 14px 28px rgba(0,0,0,.12);
}
.o_kanban_view .directive_title {
font-weight: 800;
font-size: 18px;
line-height: 1.3;
margin-bottom: 8px;
}
.o_kanban_view .directive_deadline_label {
font-weight: 600;
font-size: 14px;
color: #555;
}
.o_kanban_view .directive_deadline_value {
font-weight: 800;
font-size: 15px;
color: #333;
}
.o_kanban_view .directive_priority .fa-star {
color: #ffc107 !important;
}
.o_kanban_view .directive_meta {
opacity: 0.9;
font-size: 14px; /* крупнее */
}
.o_kanban_view .directive_row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.o_kanban_view .meta_label {
color: #666;
font-weight: 500;
}
.o_kanban_view .meta_value {
font-weight: 700;
color: #333;
}
.o_kanban_view .title_row {
margin-bottom: 12px;
}
.o_kanban_view .deadline_row {
margin-bottom: 12px;
}
.o_kanban_view .directive_tl {
position: absolute;
bottom: 12px;
right: 12px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #28a745;
box-shadow: 0 0 0 4px rgba(40,167,69,.25);
}
.o_kanban_large_cards .o_kanban_record {
width: 320px !important;
}
.o_kanban_view .directive_tl {
opacity: 0.95;
}
.o_kanban_view .directive_card:hover .directive_tl {
box-shadow: 0 0 0 5px rgba(40,167,69,.35);
}
.o_kanban_view .o_kanban_record.directive_card {
margin-bottom: 12px;
}