Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC
This commit is contained in:
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
93
directive_layer/static/src/scss/directive_kanban.scss
Normal file
93
directive_layer/static/src/scss/directive_kanban.scss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user