prime-ui-kit 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/chunks/{chunk-WDTSHOTD.js → chunk-E6SQBSDF.js} +140 -30
- package/dist/chunks/{chunk-CUH6DJ57.js → chunk-Z3WKKAS6.js} +6572 -5024
- package/dist/chunks/{chunk-GKX3QAUC.js → chunk-ZSJMNA46.js} +14 -5
- package/dist/color-picker.js +2 -2
- package/dist/components/chat/Chat.d.ts +11 -12
- package/dist/components/chat/chatContext.d.ts +2 -0
- package/dist/components/chat-list/ChatList.d.ts +191 -0
- package/dist/components/chat-list/formatChatTime.d.ts +6 -0
- package/dist/components/command-menu/CommandMenu.d.ts +10 -5
- package/dist/components/composer/Composer.d.ts +3 -10
- package/dist/components/data-table/DataTable.d.ts +16 -2
- package/dist/components/data-table/Row.d.ts +23 -7
- package/dist/components/data-table/StateRows.d.ts +5 -1
- package/dist/components/data-table/reorder.d.ts +3 -1
- package/dist/components/data-table/rows.d.ts +20 -4
- package/dist/components/file-upload/FileUpload.d.ts +7 -0
- package/dist/components/gantt/Gantt.d.ts +19 -1
- package/dist/components/gantt/GanttBar.d.ts +4 -1
- package/dist/components/gantt/GanttRow.d.ts +8 -1
- package/dist/components/index.d.ts +11 -7
- package/dist/components/kanban/Kanban.d.ts +17 -2
- package/dist/components/list/List.d.ts +120 -0
- package/dist/components/message/Message.d.ts +13 -0
- package/dist/components/message/MessageParts.d.ts +2 -0
- package/dist/components/modal/DialogParts.d.ts +5 -1
- package/dist/components/select/Select.d.ts +11 -5
- package/dist/components/select/SelectItem.d.ts +19 -0
- package/dist/components/select/selectContext.d.ts +4 -0
- package/dist/components/sparkline/Sparkline.d.ts +14 -3
- package/dist/components/textarea/Textarea.d.ts +11 -3
- package/dist/hooks/useUploadQueue.d.ts +5 -1
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -2
- package/dist/internal/VisuallyHidden.d.ts +6 -1
- package/dist/internal/attachmentTile.d.ts +5 -1
- package/dist/internal/richText.d.ts +7 -2
- package/dist/internal/textFormatting.d.ts +52 -0
- package/dist/internal/textFormattingToolbar.d.ts +26 -0
- package/dist/skill/api-contract.md +1 -1
- package/dist/skill/cheatsheet.md +13 -1
- package/dist/skill/choosing.md +9 -4
- package/dist/skill/components.md +7 -5
- package/dist/skill/composition.md +9 -1
- package/dist/skill/layouts.md +4 -1
- package/dist/skill/patterns/app-shell.module.css +18 -0
- package/dist/skill/patterns/app-shell.tsx +301 -0
- package/dist/skill/patterns/chat.module.css +0 -25
- package/dist/skill/patterns/chat.tsx +72 -29
- package/dist/skill/patterns/kanban-board.module.css +28 -0
- package/dist/skill/patterns/kanban-board.tsx +612 -0
- package/dist/skill/patterns/media-library.module.css +66 -0
- package/dist/skill/patterns/media-library.tsx +638 -0
- package/dist/skill/patterns/schedule.module.css +22 -0
- package/dist/skill/patterns/schedule.tsx +722 -0
- package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
- package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
- package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
- package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
- package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
- package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
- package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
- package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
- package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
- package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
- package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
- package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
- package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
- package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
- package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
- package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
- package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
- package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
- package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
- package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
- package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
- package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
- package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
- package/dist/skill/reference/components/file-upload/COMPONENT.md +12 -8
- package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
- package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
- package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
- package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
- package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
- package/dist/skill/reference/components/list/COMPONENT.md +195 -0
- package/dist/skill/reference/components/list/examples/as-link.tsx +37 -0
- package/dist/skill/reference/components/list/examples/examples.module.css +18 -0
- package/dist/skill/reference/components/list/examples/overflow.tsx +46 -0
- package/dist/skill/reference/components/list/examples/overview.tsx +63 -0
- package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
- package/dist/skill/reference/components/list/examples/sizes.tsx +35 -0
- package/dist/skill/reference/components/list/examples/states.tsx +27 -0
- package/dist/skill/reference/components/list/examples/with-icon.tsx +60 -0
- package/dist/skill/reference/components/message/COMPONENT.md +18 -2
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
- package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
- package/dist/skill/reference/components/page-content/COMPONENT.md +2 -2
- package/dist/skill/reference/components/page-content/examples/examples.module.css +5 -12
- package/dist/skill/reference/components/page-content/examples/list-detail.tsx +13 -18
- package/dist/skill/reference/components/select/COMPONENT.md +38 -7
- package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
- package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
- package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
- package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
- package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
- package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
- package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
- package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
- package/dist/skill/reference/components/visually-hidden/COMPONENT.md +6 -3
- package/dist/skill/reference/components/visually-hidden/examples/overview.tsx +2 -2
- package/package.json +2 -2
- package/src/components/chat/COMPONENT.md +13 -4
- package/src/components/chat/examples/states.tsx +100 -0
- package/src/components/chat-list/COMPONENT.md +256 -0
- package/src/components/chat-list/examples/controlled.tsx +103 -0
- package/src/components/chat-list/examples/examples.module.css +66 -0
- package/src/components/chat-list/examples/live.tsx +133 -0
- package/src/components/chat-list/examples/long-list.tsx +87 -0
- package/src/components/chat-list/examples/narrow.tsx +88 -0
- package/src/components/chat-list/examples/overview.tsx +194 -0
- package/src/components/chat-list/examples/search.tsx +71 -0
- package/src/components/chat-list/examples/sizes.tsx +61 -0
- package/src/components/chat-list/examples/states.tsx +107 -0
- package/src/components/chat-list/examples/structure.tsx +135 -0
- package/src/components/command-menu/COMPONENT.md +15 -11
- package/src/components/command-menu/examples/server-search.tsx +11 -9
- package/src/components/composer/COMPONENT.md +8 -7
- package/src/components/composer/examples/formatting.tsx +1 -1
- package/src/components/confirm-dialog/COMPONENT.md +1 -0
- package/src/components/data-table/COMPONENT.md +21 -6
- package/src/components/data-table/examples/groups.tsx +2 -1
- package/src/components/data-table/examples/trend.tsx +85 -0
- package/src/components/drawer/COMPONENT.md +7 -6
- package/src/components/drawer/examples/client-card.tsx +50 -0
- package/src/components/fieldset/COMPONENT.md +1 -0
- package/src/components/file-upload/COMPONENT.md +12 -8
- package/src/components/file-upload/examples/overview.tsx +14 -3
- package/src/components/gantt/COMPONENT.md +15 -5
- package/src/components/gantt/examples/row-link.tsx +96 -0
- package/src/components/kanban/COMPONENT.md +12 -4
- package/src/components/kanban/examples/item-link.tsx +92 -0
- package/src/components/list/COMPONENT.md +195 -0
- package/src/components/list/examples/as-link.tsx +37 -0
- package/src/components/list/examples/examples.module.css +18 -0
- package/src/components/list/examples/overflow.tsx +46 -0
- package/src/components/list/examples/overview.tsx +63 -0
- package/src/components/list/examples/reorder.tsx +48 -0
- package/src/components/list/examples/sizes.tsx +35 -0
- package/src/components/list/examples/states.tsx +27 -0
- package/src/components/list/examples/with-icon.tsx +60 -0
- package/src/components/message/COMPONENT.md +18 -2
- package/src/components/message/examples/bot-actions.tsx +51 -0
- package/src/components/modal/COMPONENT.md +6 -4
- package/src/components/page-content/COMPONENT.md +2 -2
- package/src/components/page-content/examples/examples.module.css +5 -12
- package/src/components/page-content/examples/list-detail.tsx +13 -18
- package/src/components/select/COMPONENT.md +38 -7
- package/src/components/select/examples/create-action.tsx +107 -0
- package/src/components/select/examples/saving.tsx +47 -0
- package/src/components/sheet/COMPONENT.md +4 -4
- package/src/components/sparkline/COMPONENT.md +16 -2
- package/src/components/sparkline/examples/compact.tsx +41 -0
- package/src/components/sparkline/examples/examples.module.css +12 -0
- package/src/components/textarea/COMPONENT.md +22 -2
- package/src/components/textarea/examples/formatting.tsx +20 -0
- package/src/components/visually-hidden/COMPONENT.md +6 -3
- package/src/components/visually-hidden/examples/overview.tsx +2 -2
- package/dist/components/composer/format.d.ts +0 -39
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/** A team schedule: PageToolbar with search, filters and team sections over a full-height Gantt; moves saved or refused, new work drawn and named in a Modal, details in a Drawer. */
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
ConfirmDialog,
|
|
7
|
+
Datepicker,
|
|
8
|
+
DescriptionList,
|
|
9
|
+
Drawer,
|
|
10
|
+
EmptyPage,
|
|
11
|
+
Fieldset,
|
|
12
|
+
Gantt,
|
|
13
|
+
type GanttBar,
|
|
14
|
+
type GanttDates,
|
|
15
|
+
Icon,
|
|
16
|
+
Input,
|
|
17
|
+
Modal,
|
|
18
|
+
matchesSmartFilter,
|
|
19
|
+
PageContent,
|
|
20
|
+
PageToolbar,
|
|
21
|
+
ProgressBar,
|
|
22
|
+
SegmentedControl,
|
|
23
|
+
Select,
|
|
24
|
+
SmartFilter,
|
|
25
|
+
type SmartFilterField,
|
|
26
|
+
type SmartFilterValue,
|
|
27
|
+
Typography,
|
|
28
|
+
useNotifications,
|
|
29
|
+
} from "prime-ui-kit";
|
|
30
|
+
import * as React from "react";
|
|
31
|
+
|
|
32
|
+
import styles from "./schedule.module.css";
|
|
33
|
+
|
|
34
|
+
type Hue = NonNullable<GanttBar["color"]>;
|
|
35
|
+
type TeamId = "rollout" | "analytics" | "support";
|
|
36
|
+
type ClientId = "tekhnostil" | "granit" | "mostostroy" | "kvartal";
|
|
37
|
+
type Kind = "project" | "duty" | "absence" | "milestone";
|
|
38
|
+
|
|
39
|
+
/** A bar of the chart with the fields the page needs back in its callbacks. */
|
|
40
|
+
type Assignment = GanttBar & { kind: Kind; client: ClientId | null };
|
|
41
|
+
|
|
42
|
+
type Person = {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
initials: string;
|
|
46
|
+
role: string;
|
|
47
|
+
team: TeamId;
|
|
48
|
+
color: Hue;
|
|
49
|
+
bars: Assignment[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** What the create dialog is for: a range drawn on a row, or the toolbar button. */
|
|
53
|
+
type Draft = { personId: string | null; dates: GanttDates | null };
|
|
54
|
+
|
|
55
|
+
type Errors = Partial<Record<"title" | "client" | "person" | "period", string>>;
|
|
56
|
+
|
|
57
|
+
const TEAMS: { value: TeamId; label: string }[] = [
|
|
58
|
+
{ value: "rollout", label: "Внедрение" },
|
|
59
|
+
{ value: "analytics", label: "Аналитика" },
|
|
60
|
+
{ value: "support", label: "Поддержка" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** One hue per client across the product: the bar label says the work, the hue repeats the client. */
|
|
64
|
+
const CLIENTS: Record<ClientId, { name: string; color: Hue }> = {
|
|
65
|
+
tekhnostil: { name: "АО «Техностиль»", color: "blue" },
|
|
66
|
+
granit: { name: "ООО «Гранит»", color: "teal" },
|
|
67
|
+
mostostroy: { name: "ООО «Мостострой»", color: "orange" },
|
|
68
|
+
kvartal: { name: "ООО «Зелёный квартал»", color: "pink" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const KIND: Record<Kind, string> = {
|
|
72
|
+
project: "Работа по проекту",
|
|
73
|
+
duty: "Внутренняя задача",
|
|
74
|
+
absence: "Отсутствие",
|
|
75
|
+
milestone: "Веха",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const DAY_MS = 86_400_000;
|
|
79
|
+
|
|
80
|
+
const addDays = (date: Date, days: number) => {
|
|
81
|
+
const next = new Date(date);
|
|
82
|
+
next.setDate(next.getDate() + days);
|
|
83
|
+
return next;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const TODAY = new Date(new Date().setHours(0, 0, 0, 0));
|
|
87
|
+
const WEEK = addDays(TODAY, -((TODAY.getDay() + 6) % 7));
|
|
88
|
+
const at = (days: number) => addDays(WEEK, days);
|
|
89
|
+
|
|
90
|
+
const project = (
|
|
91
|
+
id: string,
|
|
92
|
+
label: string,
|
|
93
|
+
client: ClientId,
|
|
94
|
+
from: number,
|
|
95
|
+
to: number,
|
|
96
|
+
progress?: number,
|
|
97
|
+
): Assignment => ({
|
|
98
|
+
id,
|
|
99
|
+
label,
|
|
100
|
+
kind: "project",
|
|
101
|
+
client,
|
|
102
|
+
color: CLIENTS[client].color,
|
|
103
|
+
start: at(from),
|
|
104
|
+
end: at(to),
|
|
105
|
+
progress,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const milestone = (id: string, label: string, client: ClientId, day: number): Assignment => ({
|
|
109
|
+
id,
|
|
110
|
+
label,
|
|
111
|
+
kind: "milestone",
|
|
112
|
+
client,
|
|
113
|
+
tone: "accent",
|
|
114
|
+
start: at(day),
|
|
115
|
+
end: at(day),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** Absences are agreed elsewhere: the chart shows them and refuses work on top of them. */
|
|
119
|
+
const absence = (id: string, label: string, from: number, to: number): Assignment => ({
|
|
120
|
+
id,
|
|
121
|
+
label,
|
|
122
|
+
kind: "absence",
|
|
123
|
+
client: null,
|
|
124
|
+
color: "purple",
|
|
125
|
+
readOnly: true,
|
|
126
|
+
start: at(from),
|
|
127
|
+
end: at(to),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const duty = (id: string, from: number, to: number): Assignment => ({
|
|
131
|
+
id,
|
|
132
|
+
label: "Дежурство",
|
|
133
|
+
kind: "duty",
|
|
134
|
+
client: null,
|
|
135
|
+
tone: "neutral",
|
|
136
|
+
start: at(from),
|
|
137
|
+
end: at(to),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const PEOPLE: Person[] = [
|
|
141
|
+
{
|
|
142
|
+
id: "sokolov",
|
|
143
|
+
name: "Пётр Соколов",
|
|
144
|
+
initials: "ПС",
|
|
145
|
+
role: "Руководитель проекта",
|
|
146
|
+
team: "rollout",
|
|
147
|
+
color: "blue",
|
|
148
|
+
bars: [
|
|
149
|
+
project("s-1", "Внедрение CRM", "granit", -3, 12, 0.5),
|
|
150
|
+
project("s-2", "Планирование работ", "mostostroy", 8, 16),
|
|
151
|
+
milestone("s-3", "Запуск у «Техностиля»", "tekhnostil", 9),
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "kotova",
|
|
156
|
+
name: "Анна Котова",
|
|
157
|
+
initials: "АК",
|
|
158
|
+
role: "Аналитик",
|
|
159
|
+
team: "analytics",
|
|
160
|
+
color: "green",
|
|
161
|
+
bars: [
|
|
162
|
+
project("k-1", "Обследование процессов", "granit", -4, 5, 0.8),
|
|
163
|
+
project("k-2", "ТЗ на интеграцию", "mostostroy", 6, 15),
|
|
164
|
+
absence("k-3", "Отпуск", 17, 27),
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "gordeev",
|
|
169
|
+
name: "Илья Гордеев",
|
|
170
|
+
initials: "ИГ",
|
|
171
|
+
role: "Разработчик интеграций",
|
|
172
|
+
team: "rollout",
|
|
173
|
+
color: "orange",
|
|
174
|
+
bars: [
|
|
175
|
+
project("g-1", "Интеграция с 1С", "tekhnostil", -6, 9, 0.6),
|
|
176
|
+
project("g-2", "Обмен с банком", "kvartal", 4, 14),
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "belova",
|
|
181
|
+
name: "Ольга Белова",
|
|
182
|
+
initials: "ОБ",
|
|
183
|
+
role: "Инженер внедрения",
|
|
184
|
+
team: "rollout",
|
|
185
|
+
color: "purple",
|
|
186
|
+
bars: [
|
|
187
|
+
absence("b-1", "Отпуск", -2, 5),
|
|
188
|
+
project("b-2", "Настройка ролей", "tekhnostil", 5, 12),
|
|
189
|
+
project("b-3", "Обучение пользователей", "granit", 14, 19),
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "khasanov",
|
|
194
|
+
name: "Марат Хасанов",
|
|
195
|
+
initials: "МХ",
|
|
196
|
+
role: "Инженер внедрения",
|
|
197
|
+
team: "rollout",
|
|
198
|
+
color: "teal",
|
|
199
|
+
bars: [project("h-1", "Миграция данных", "mostostroy", 0, 18, 0.2)],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "zueva",
|
|
203
|
+
name: "Елена Зуева",
|
|
204
|
+
initials: "ЕЗ",
|
|
205
|
+
role: "Аналитик",
|
|
206
|
+
team: "analytics",
|
|
207
|
+
color: "sky",
|
|
208
|
+
bars: [
|
|
209
|
+
project("z-1", "Отчёты руководителя", "kvartal", 2, 10, 0.4),
|
|
210
|
+
milestone("z-2", "Приёмка отчётов", "kvartal", 10),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "orlov",
|
|
215
|
+
name: "Денис Орлов",
|
|
216
|
+
initials: "ДО",
|
|
217
|
+
role: "Инженер поддержки",
|
|
218
|
+
team: "support",
|
|
219
|
+
color: "yellow",
|
|
220
|
+
bars: [duty("o-1", 0, 7), project("o-2", "Сопровождение запуска", "tekhnostil", 9, 23)],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "lebedeva",
|
|
224
|
+
name: "Мария Лебедева",
|
|
225
|
+
initials: "МЛ",
|
|
226
|
+
role: "Инженер поддержки",
|
|
227
|
+
team: "support",
|
|
228
|
+
color: "pink",
|
|
229
|
+
bars: [absence("l-1", "Больничный", -1, 2), duty("l-2", 7, 14)],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "morozov",
|
|
233
|
+
name: "Антон Морозов",
|
|
234
|
+
initials: "АМ",
|
|
235
|
+
role: "Разработчик интеграций",
|
|
236
|
+
team: "rollout",
|
|
237
|
+
color: "red",
|
|
238
|
+
bars: [],
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const ROLES = [...new Set(PEOPLE.map((person) => person.role))];
|
|
243
|
+
|
|
244
|
+
const FIELDS: SmartFilterField[] = [
|
|
245
|
+
{ key: "role", label: "Роль", options: ROLES.map((role) => ({ value: role, label: role })) },
|
|
246
|
+
{
|
|
247
|
+
key: "client",
|
|
248
|
+
label: "Клиент",
|
|
249
|
+
options: (Object.keys(CLIENTS) as ClientId[]).map((value) => ({
|
|
250
|
+
value,
|
|
251
|
+
label: CLIENTS[value].name,
|
|
252
|
+
})),
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const DAY = new Intl.DateTimeFormat("ru-RU", { day: "numeric", month: "long" });
|
|
257
|
+
|
|
258
|
+
const formatPeriod = ({ start, end }: GanttDates) =>
|
|
259
|
+
start.getTime() === end.getTime()
|
|
260
|
+
? DAY.format(start)
|
|
261
|
+
: `${DAY.format(start)} – ${DAY.format(addDays(end, -1))}`;
|
|
262
|
+
|
|
263
|
+
const lengthInDays = ({ start, end }: GanttDates) =>
|
|
264
|
+
Math.round((end.getTime() - start.getTime()) / DAY_MS);
|
|
265
|
+
|
|
266
|
+
const overlapsAbsence = (person: Person, dates: GanttDates, barId?: string) =>
|
|
267
|
+
person.bars.some(
|
|
268
|
+
(bar) =>
|
|
269
|
+
bar.kind === "absence" && bar.id !== barId && dates.start < bar.end && bar.start < dates.end,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
/** The stand-in for the schedule service's response time. */
|
|
273
|
+
const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
274
|
+
|
|
275
|
+
/** `yyyy-MM-dd` from the Datepicker's hidden input → a local calendar day. */
|
|
276
|
+
const parseDay = (value: FormDataEntryValue) => new Date(`${value}T00:00`);
|
|
277
|
+
|
|
278
|
+
export default function SchedulePattern() {
|
|
279
|
+
const { notify } = useNotifications();
|
|
280
|
+
const formId = React.useId();
|
|
281
|
+
const [people, setPeople] = React.useState(PEOPLE);
|
|
282
|
+
const [loading, setLoading] = React.useState(true);
|
|
283
|
+
const [team, setTeam] = React.useState("all");
|
|
284
|
+
const [filter, setFilter] = React.useState<SmartFilterValue>({});
|
|
285
|
+
const [search, setSearch] = React.useState("");
|
|
286
|
+
const [selected, setSelected] = React.useState<{ bar: Assignment; person: Person } | null>(null);
|
|
287
|
+
const [detailOpen, setDetailOpen] = React.useState(false);
|
|
288
|
+
const [draft, setDraft] = React.useState<Draft | null>(null);
|
|
289
|
+
const [errors, setErrors] = React.useState<Errors>({});
|
|
290
|
+
const [saving, setSaving] = React.useState(false);
|
|
291
|
+
// The chart keeps a busy ghost while the drawn range is being named; the dialog settles it.
|
|
292
|
+
const pendingCreate = React.useRef<{ resolve: () => void; reject: (reason: Error) => void }>(
|
|
293
|
+
null,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
React.useEffect(() => {
|
|
297
|
+
const timer = window.setTimeout(() => setLoading(false), 900);
|
|
298
|
+
return () => window.clearTimeout(timer);
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
const query = search.trim().toLowerCase();
|
|
302
|
+
// Absences stay visible under a client filter: whether a person is free matters for any client.
|
|
303
|
+
// `matchesSmartFilter(entry, null)` is true only while that filter is empty.
|
|
304
|
+
const rows = people
|
|
305
|
+
.filter(
|
|
306
|
+
(person) =>
|
|
307
|
+
(team === "all" || person.team === team) &&
|
|
308
|
+
matchesSmartFilter(filter.role, person.role) &&
|
|
309
|
+
person.name.toLowerCase().includes(query),
|
|
310
|
+
)
|
|
311
|
+
.map((person) => ({
|
|
312
|
+
...person,
|
|
313
|
+
bars: person.bars.filter(
|
|
314
|
+
(bar) => bar.kind === "absence" || matchesSmartFilter(filter.client, bar.client),
|
|
315
|
+
),
|
|
316
|
+
}))
|
|
317
|
+
.filter(
|
|
318
|
+
(person) =>
|
|
319
|
+
matchesSmartFilter(filter.client, null) ||
|
|
320
|
+
person.bars.some((bar) => bar.kind !== "absence"),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const findPerson = (id: string) => people.find((person) => person.id === id) as Person;
|
|
324
|
+
|
|
325
|
+
const updateBars = (personId: string, change: (bars: Assignment[]) => Assignment[]) =>
|
|
326
|
+
setPeople((current) =>
|
|
327
|
+
current.map((person) =>
|
|
328
|
+
person.id === personId ? { ...person, bars: change(person.bars) } : person,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const resetFilters = () => {
|
|
333
|
+
setTeam("all");
|
|
334
|
+
setFilter({});
|
|
335
|
+
setSearch("");
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// The rows change only after the service agrees, so a refused move glides back by itself.
|
|
339
|
+
const moveBar = async (dates: GanttDates, bar: Assignment, row: Person) => {
|
|
340
|
+
const person = findPerson(row.id);
|
|
341
|
+
await wait(700);
|
|
342
|
+
if (overlapsAbsence(person, dates, bar.id)) {
|
|
343
|
+
notify({
|
|
344
|
+
tone: "danger",
|
|
345
|
+
title: "Сроки не изменены",
|
|
346
|
+
description: `${person.name} в эти дни отсутствует`,
|
|
347
|
+
});
|
|
348
|
+
throw new Error("Пересекается с отсутствием");
|
|
349
|
+
}
|
|
350
|
+
updateBars(person.id, (bars) =>
|
|
351
|
+
bars.map((item) => (item.id === bar.id ? { ...item, ...dates } : item)),
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const startCreate = (dates: GanttDates, row: Person) => {
|
|
356
|
+
const person = findPerson(row.id);
|
|
357
|
+
if (overlapsAbsence(person, dates)) {
|
|
358
|
+
notify({
|
|
359
|
+
tone: "warning",
|
|
360
|
+
title: "Задача не назначена",
|
|
361
|
+
description: `${person.name} в эти дни отсутствует`,
|
|
362
|
+
});
|
|
363
|
+
return Promise.reject(new Error("Пересекается с отсутствием"));
|
|
364
|
+
}
|
|
365
|
+
return new Promise<void>((resolve, reject) => {
|
|
366
|
+
pendingCreate.current = { resolve, reject };
|
|
367
|
+
setErrors({});
|
|
368
|
+
setDraft({ personId: person.id, dates });
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const closeDraft = (open: boolean) => {
|
|
373
|
+
if (open) return;
|
|
374
|
+
pendingCreate.current?.reject(new Error("Отменено"));
|
|
375
|
+
pendingCreate.current = null;
|
|
376
|
+
setDraft(null);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
if (!draft) return;
|
|
382
|
+
const data = new FormData(event.currentTarget);
|
|
383
|
+
const title = String(data.get("title") ?? "").trim();
|
|
384
|
+
const client = String(data.get("client") ?? "") as ClientId | "";
|
|
385
|
+
const personId = draft.personId ?? String(data.get("person") ?? "");
|
|
386
|
+
const [from, to] = data.getAll("period");
|
|
387
|
+
const dates =
|
|
388
|
+
draft.dates ?? (from && to ? { start: parseDay(from), end: addDays(parseDay(to), 1) } : null);
|
|
389
|
+
|
|
390
|
+
const found: Errors = {};
|
|
391
|
+
if (!title) found.title = "Назовите задачу";
|
|
392
|
+
if (!client) found.client = "Выберите клиента";
|
|
393
|
+
if (!personId) found.person = "Выберите сотрудника";
|
|
394
|
+
if (!dates) found.period = "Укажите начало и конец работ";
|
|
395
|
+
else if (personId && overlapsAbsence(findPerson(personId), dates))
|
|
396
|
+
found.period = "Сотрудник в эти дни отсутствует";
|
|
397
|
+
setErrors(found);
|
|
398
|
+
const firstInvalid = (["title", "client", "person", "period"] as const).find(
|
|
399
|
+
(key) => found[key],
|
|
400
|
+
);
|
|
401
|
+
if (firstInvalid || !client || !dates) {
|
|
402
|
+
if (firstInvalid) document.getElementById(`${formId}-${firstInvalid}`)?.focus();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setSaving(true);
|
|
407
|
+
await wait(700);
|
|
408
|
+
const person = findPerson(personId);
|
|
409
|
+
updateBars(person.id, (bars) => [
|
|
410
|
+
...bars,
|
|
411
|
+
{
|
|
412
|
+
id: `${person.id}-${Date.now()}`,
|
|
413
|
+
label: title,
|
|
414
|
+
kind: "project",
|
|
415
|
+
client,
|
|
416
|
+
color: CLIENTS[client].color,
|
|
417
|
+
...dates,
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
pendingCreate.current?.resolve();
|
|
421
|
+
pendingCreate.current = null;
|
|
422
|
+
setSaving(false);
|
|
423
|
+
setDraft(null);
|
|
424
|
+
notify({
|
|
425
|
+
tone: "success",
|
|
426
|
+
title: "Задача назначена",
|
|
427
|
+
description: `${title} · ${person.name}`,
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const removeSelected = async () => {
|
|
432
|
+
if (!selected) return;
|
|
433
|
+
await wait(700);
|
|
434
|
+
updateBars(selected.person.id, (bars) => bars.filter((bar) => bar.id !== selected.bar.id));
|
|
435
|
+
setDetailOpen(false);
|
|
436
|
+
notify({ tone: "success", title: "Задача снята", description: selected.bar.label });
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const draftPerson = draft?.personId ? findPerson(draft.personId) : null;
|
|
440
|
+
const detail = selected?.bar;
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<PageContent.Root fill>
|
|
444
|
+
<PageContent.Header>
|
|
445
|
+
<PageContent.Title>Загрузка команды</PageContent.Title>
|
|
446
|
+
<PageContent.Description>
|
|
447
|
+
Кто из команды внедрения чем занят у клиентов: проекты, отпуска и вехи запусков.
|
|
448
|
+
</PageContent.Description>
|
|
449
|
+
</PageContent.Header>
|
|
450
|
+
<PageContent.Body>
|
|
451
|
+
<div className={styles.board}>
|
|
452
|
+
<SmartFilter.Root
|
|
453
|
+
fields={FIELDS}
|
|
454
|
+
value={filter}
|
|
455
|
+
onValueChange={setFilter}
|
|
456
|
+
search={search}
|
|
457
|
+
onSearchChange={setSearch}
|
|
458
|
+
>
|
|
459
|
+
<PageToolbar.Root aria-label="График команды">
|
|
460
|
+
<PageToolbar.Tools>
|
|
461
|
+
<SmartFilter.Toolbar />
|
|
462
|
+
</PageToolbar.Tools>
|
|
463
|
+
<PageToolbar.Sections>
|
|
464
|
+
<SegmentedControl.Root
|
|
465
|
+
fullWidth
|
|
466
|
+
value={team}
|
|
467
|
+
onValueChange={setTeam}
|
|
468
|
+
aria-label="Команда"
|
|
469
|
+
>
|
|
470
|
+
<SegmentedControl.Item value="all">
|
|
471
|
+
Все<SegmentedControl.Count>{people.length}</SegmentedControl.Count>
|
|
472
|
+
</SegmentedControl.Item>
|
|
473
|
+
{TEAMS.map((item) => (
|
|
474
|
+
<SegmentedControl.Item key={item.value} value={item.value}>
|
|
475
|
+
{item.label}
|
|
476
|
+
<SegmentedControl.Count>
|
|
477
|
+
{people.filter((person) => person.team === item.value).length}
|
|
478
|
+
</SegmentedControl.Count>
|
|
479
|
+
</SegmentedControl.Item>
|
|
480
|
+
))}
|
|
481
|
+
</SegmentedControl.Root>
|
|
482
|
+
</PageToolbar.Sections>
|
|
483
|
+
<PageToolbar.Actions>
|
|
484
|
+
<Button.Root
|
|
485
|
+
onClick={() => {
|
|
486
|
+
setErrors({});
|
|
487
|
+
setDraft({ personId: null, dates: null });
|
|
488
|
+
}}
|
|
489
|
+
>
|
|
490
|
+
<Button.Icon>
|
|
491
|
+
<Icon name="action.add" />
|
|
492
|
+
</Button.Icon>
|
|
493
|
+
Назначить
|
|
494
|
+
</Button.Root>
|
|
495
|
+
</PageToolbar.Actions>
|
|
496
|
+
<PageToolbar.Chips>
|
|
497
|
+
<SmartFilter.Chips />
|
|
498
|
+
</PageToolbar.Chips>
|
|
499
|
+
</PageToolbar.Root>
|
|
500
|
+
</SmartFilter.Root>
|
|
501
|
+
|
|
502
|
+
<Gantt<Person, Assignment>
|
|
503
|
+
aria-label="Загрузка команды внедрения"
|
|
504
|
+
className={styles.chart}
|
|
505
|
+
defaultScale="day"
|
|
506
|
+
scrollHeight="fill"
|
|
507
|
+
loading={loading}
|
|
508
|
+
rows={loading ? [] : rows}
|
|
509
|
+
getRowKey={(person) => person.id}
|
|
510
|
+
getRowLabel={(person) => person.name}
|
|
511
|
+
getRowBars={(person) => person.bars}
|
|
512
|
+
renderRowAvatar={(person) => (
|
|
513
|
+
<Avatar.Root color={person.color}>
|
|
514
|
+
<Avatar.Fallback>{person.initials}</Avatar.Fallback>
|
|
515
|
+
</Avatar.Root>
|
|
516
|
+
)}
|
|
517
|
+
renderRow={(person) => (
|
|
518
|
+
<span className={styles.person}>
|
|
519
|
+
<Typography as="span" variant="body-m">
|
|
520
|
+
{person.name}
|
|
521
|
+
</Typography>
|
|
522
|
+
<Typography as="span" variant="caption" tone="muted">
|
|
523
|
+
{person.role}
|
|
524
|
+
</Typography>
|
|
525
|
+
</span>
|
|
526
|
+
)}
|
|
527
|
+
onBarChange={moveBar}
|
|
528
|
+
onBarCreate={startCreate}
|
|
529
|
+
onSelect={(bar, row) => {
|
|
530
|
+
setSelected({ bar, person: findPerson(row.id) });
|
|
531
|
+
setDetailOpen(true);
|
|
532
|
+
}}
|
|
533
|
+
renderBarTooltip={(bar) => (bar.client ? CLIENTS[bar.client].name : KIND[bar.kind])}
|
|
534
|
+
toolbar={
|
|
535
|
+
<>
|
|
536
|
+
<Typography as="span" variant="body-s" tone="muted">
|
|
537
|
+
{loading
|
|
538
|
+
? "Загружаем график…"
|
|
539
|
+
: `Сотрудников: ${rows.length} из ${people.length}`}
|
|
540
|
+
</Typography>
|
|
541
|
+
{/* The legend of client hues: the bar's hue repeats its client, the tooltip names it. */}
|
|
542
|
+
{(Object.keys(CLIENTS) as ClientId[]).map((client) => (
|
|
543
|
+
<Badge.Root key={client} color={CLIENTS[client].color}>
|
|
544
|
+
<Badge.Dot />
|
|
545
|
+
{CLIENTS[client].name}
|
|
546
|
+
</Badge.Root>
|
|
547
|
+
))}
|
|
548
|
+
</>
|
|
549
|
+
}
|
|
550
|
+
empty={
|
|
551
|
+
<EmptyPage.Root size="s" role="status" aria-labelledby="schedule-empty">
|
|
552
|
+
<EmptyPage.Icon>
|
|
553
|
+
<Icon name="action.search" />
|
|
554
|
+
</EmptyPage.Icon>
|
|
555
|
+
<EmptyPage.Title as="p" id="schedule-empty">
|
|
556
|
+
Никого не нашли
|
|
557
|
+
</EmptyPage.Title>
|
|
558
|
+
<EmptyPage.Description>Измените запрос или сбросьте фильтры.</EmptyPage.Description>
|
|
559
|
+
<EmptyPage.Actions>
|
|
560
|
+
<Button.Root variant="soft" tone="neutral" onClick={resetFilters}>
|
|
561
|
+
Сбросить фильтры
|
|
562
|
+
</Button.Root>
|
|
563
|
+
</EmptyPage.Actions>
|
|
564
|
+
</EmptyPage.Root>
|
|
565
|
+
}
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<Modal.Root open={draft !== null} onOpenChange={closeDraft}>
|
|
570
|
+
<Modal.Content size="s">
|
|
571
|
+
<Modal.Header>
|
|
572
|
+
<Modal.Title>Новая задача</Modal.Title>
|
|
573
|
+
<Modal.Description>
|
|
574
|
+
{draftPerson && draft?.dates
|
|
575
|
+
? `${draftPerson.name} · ${formatPeriod(draft.dates)}`
|
|
576
|
+
: "Сотрудник увидит задачу в своём графике."}
|
|
577
|
+
</Modal.Description>
|
|
578
|
+
</Modal.Header>
|
|
579
|
+
<Modal.Body>
|
|
580
|
+
<Modal.Guard busy={saving} />
|
|
581
|
+
<form id={formId} noValidate onSubmit={submit}>
|
|
582
|
+
<Fieldset aria-label="Задача" disabled={saving}>
|
|
583
|
+
<Input.Root id={`${formId}-title`} label="Задача" required error={errors.title}>
|
|
584
|
+
<Input.Wrapper>
|
|
585
|
+
<Input.Field name="title" placeholder="Обучение пользователей" autoFocus />
|
|
586
|
+
</Input.Wrapper>
|
|
587
|
+
</Input.Root>
|
|
588
|
+
<Select.Root
|
|
589
|
+
name="client"
|
|
590
|
+
label="Клиент"
|
|
591
|
+
required
|
|
592
|
+
placeholder="Выберите клиента"
|
|
593
|
+
error={errors.client}
|
|
594
|
+
id={`${formId}-client`}
|
|
595
|
+
>
|
|
596
|
+
<Select.Trigger>
|
|
597
|
+
<Select.Value />
|
|
598
|
+
</Select.Trigger>
|
|
599
|
+
<Select.Content>
|
|
600
|
+
{(Object.keys(CLIENTS) as ClientId[]).map((value) => (
|
|
601
|
+
<Select.Item key={value} value={value}>
|
|
602
|
+
{CLIENTS[value].name}
|
|
603
|
+
</Select.Item>
|
|
604
|
+
))}
|
|
605
|
+
</Select.Content>
|
|
606
|
+
</Select.Root>
|
|
607
|
+
{draft?.personId ? null : (
|
|
608
|
+
<>
|
|
609
|
+
<Select.Root
|
|
610
|
+
name="person"
|
|
611
|
+
label="Сотрудник"
|
|
612
|
+
required
|
|
613
|
+
placeholder="Выберите сотрудника"
|
|
614
|
+
error={errors.person}
|
|
615
|
+
id={`${formId}-person`}
|
|
616
|
+
>
|
|
617
|
+
<Select.Trigger>
|
|
618
|
+
<Select.Value />
|
|
619
|
+
</Select.Trigger>
|
|
620
|
+
<Select.Content searchable>
|
|
621
|
+
{people.map((person) => (
|
|
622
|
+
<Select.Item key={person.id} value={person.id} keywords={person.role}>
|
|
623
|
+
{person.name}
|
|
624
|
+
</Select.Item>
|
|
625
|
+
))}
|
|
626
|
+
</Select.Content>
|
|
627
|
+
</Select.Root>
|
|
628
|
+
<Datepicker.Root
|
|
629
|
+
mode="range"
|
|
630
|
+
name="period"
|
|
631
|
+
label="Период"
|
|
632
|
+
required
|
|
633
|
+
fullWidth
|
|
634
|
+
placeholder="Начало и конец работ"
|
|
635
|
+
error={errors.period}
|
|
636
|
+
id={`${formId}-period`}
|
|
637
|
+
/>
|
|
638
|
+
</>
|
|
639
|
+
)}
|
|
640
|
+
</Fieldset>
|
|
641
|
+
</form>
|
|
642
|
+
</Modal.Body>
|
|
643
|
+
<Modal.Footer>
|
|
644
|
+
<Modal.Close>
|
|
645
|
+
<Button.Root variant="outline" tone="neutral">
|
|
646
|
+
Отмена
|
|
647
|
+
</Button.Root>
|
|
648
|
+
</Modal.Close>
|
|
649
|
+
<Button.Root type="submit" form={formId} loading={saving}>
|
|
650
|
+
Назначить
|
|
651
|
+
</Button.Root>
|
|
652
|
+
</Modal.Footer>
|
|
653
|
+
</Modal.Content>
|
|
654
|
+
</Modal.Root>
|
|
655
|
+
|
|
656
|
+
<Drawer.Root open={detailOpen} onOpenChange={setDetailOpen}>
|
|
657
|
+
<Drawer.Content size="s">
|
|
658
|
+
<Drawer.Header>
|
|
659
|
+
<Drawer.Title>{detail?.label}</Drawer.Title>
|
|
660
|
+
<Drawer.Description>{detail ? KIND[detail.kind] : null}</Drawer.Description>
|
|
661
|
+
</Drawer.Header>
|
|
662
|
+
<Drawer.Body>
|
|
663
|
+
{selected && detail ? (
|
|
664
|
+
<DescriptionList.Root orientation="vertical">
|
|
665
|
+
<DescriptionList.Item label="Сотрудник">
|
|
666
|
+
{selected.person.name} · {selected.person.role}
|
|
667
|
+
</DescriptionList.Item>
|
|
668
|
+
<DescriptionList.Item label="Клиент">
|
|
669
|
+
{detail.client ? (
|
|
670
|
+
<Badge.Root color={CLIENTS[detail.client].color}>
|
|
671
|
+
{CLIENTS[detail.client].name}
|
|
672
|
+
</Badge.Root>
|
|
673
|
+
) : (
|
|
674
|
+
"Без клиента"
|
|
675
|
+
)}
|
|
676
|
+
</DescriptionList.Item>
|
|
677
|
+
<DescriptionList.Item label={detail.kind === "milestone" ? "Дата" : "Период"}>
|
|
678
|
+
{formatPeriod(detail)}
|
|
679
|
+
{detail.kind === "milestone" ? null : ` · ${lengthInDays(detail)} дн.`}
|
|
680
|
+
</DescriptionList.Item>
|
|
681
|
+
{detail.progress === undefined ? null : (
|
|
682
|
+
<DescriptionList.Item label="Выполнено">
|
|
683
|
+
<ProgressBar
|
|
684
|
+
value={Math.round(detail.progress * 100)}
|
|
685
|
+
showValue
|
|
686
|
+
aria-label="Выполнено"
|
|
687
|
+
/>
|
|
688
|
+
</DescriptionList.Item>
|
|
689
|
+
)}
|
|
690
|
+
</DescriptionList.Root>
|
|
691
|
+
) : null}
|
|
692
|
+
</Drawer.Body>
|
|
693
|
+
<Drawer.Footer>
|
|
694
|
+
<Drawer.Close>
|
|
695
|
+
<Button.Root variant="outline" tone="neutral">
|
|
696
|
+
Закрыть
|
|
697
|
+
</Button.Root>
|
|
698
|
+
</Drawer.Close>
|
|
699
|
+
{detail && !detail.readOnly ? (
|
|
700
|
+
<ConfirmDialog.Root
|
|
701
|
+
title={`Снять задачу «${detail.label}»?`}
|
|
702
|
+
description={`${selected?.person.name} освободится на эти дни. Задачу можно будет назначить заново.`}
|
|
703
|
+
confirmLabel="Снять задачу"
|
|
704
|
+
onConfirm={removeSelected}
|
|
705
|
+
>
|
|
706
|
+
<ConfirmDialog.Trigger>
|
|
707
|
+
<Button.Root variant="soft" tone="danger">
|
|
708
|
+
<Button.Icon>
|
|
709
|
+
<Icon name="action.delete" />
|
|
710
|
+
</Button.Icon>
|
|
711
|
+
Снять задачу
|
|
712
|
+
</Button.Root>
|
|
713
|
+
</ConfirmDialog.Trigger>
|
|
714
|
+
</ConfirmDialog.Root>
|
|
715
|
+
) : null}
|
|
716
|
+
</Drawer.Footer>
|
|
717
|
+
</Drawer.Content>
|
|
718
|
+
</Drawer.Root>
|
|
719
|
+
</PageContent.Body>
|
|
720
|
+
</PageContent.Root>
|
|
721
|
+
);
|
|
722
|
+
}
|