fix(web): pack the activity desk header so the columns stay visible
The toolbar keeps every control, just on two shorter rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,11 +16,9 @@ import {
|
||||
UiDropdownMenu,
|
||||
UiInput,
|
||||
UiModal,
|
||||
UiPageHeader,
|
||||
UiPagination,
|
||||
} from '@/components/ui';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import UiSegmented from '@/components/ui/UiSegmented.vue';
|
||||
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
||||
import ActivitiesKanban from '@/components/activities/ActivitiesKanban.vue';
|
||||
import ActivityStagesManager from '@/components/activities/ActivityStagesManager.vue';
|
||||
@@ -105,11 +103,6 @@ const TIME_TABS: Array<{ key: TimeFilter; label: string }> = [
|
||||
{ key: 'all', label: 'همه زمانها' },
|
||||
];
|
||||
const TIME_KEYS: TimeFilter[] = TIME_TABS.map((t) => t.key);
|
||||
|
||||
const VIEW_OPTIONS = [
|
||||
{ value: 'tabs' as const, label: 'نمای تب', icon: 'list' as const },
|
||||
{ value: 'board' as const, label: 'نمای ستونی', icon: 'grid' as const },
|
||||
];
|
||||
const VIEW_STORAGE_KEY = 'activities-view-mode';
|
||||
|
||||
const DESK_TABS: Array<{ key: DeskScope; label: string }> = [
|
||||
@@ -666,11 +659,6 @@ function setViewMode(next: ViewMode) {
|
||||
});
|
||||
}
|
||||
|
||||
const viewModel = computed<ViewMode>({
|
||||
get: () => viewMode.value,
|
||||
set: (v) => setViewMode(v),
|
||||
});
|
||||
|
||||
function onStagesChanged() {
|
||||
void loadStages().then(() => loadCounts());
|
||||
}
|
||||
@@ -1013,18 +1001,42 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<section class="page" @click="closePopovers">
|
||||
<UiBusyOverlay :open="pageBusy" :message="busyMessage" />
|
||||
<div class="desk-chrome">
|
||||
<div class="desk-top">
|
||||
<div class="desk-title">
|
||||
<h1>میز کار و فعالیتها</h1>
|
||||
<p class="lead">
|
||||
<nav class="crumbs" aria-label="مسیر">
|
||||
<RouterLink :to="{ name: 'home' }">خانه</RouterLink>
|
||||
<span>‹</span>
|
||||
<span>میز کار</span>
|
||||
</nav>
|
||||
|
||||
<UiPageHeader
|
||||
title="میز کار و فعالیتها"
|
||||
lead="کارهای امروز، ارجاع به همکار و پیگیری وضعیت."
|
||||
<span class="lead-sep" aria-hidden="true">·</span>
|
||||
<span>کارهای امروز، ارجاع به همکار و پیگیری وضعیت.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="desk-actions">
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="typesManagerOpen = true"
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="ghost" type="button" :disabled="busy" :loading="pageBusy" @click.stop="exportExcel">
|
||||
<UiIcon name="sliders" :size="14" />
|
||||
انواع فعالیت و فیلدها
|
||||
</UiButton>
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="stagesManagerOpen = true"
|
||||
>
|
||||
<UiIcon name="settings" :size="14" />
|
||||
مدیریت ستونها
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" type="button" :disabled="busy" :loading="pageBusy" @click.stop="exportExcel">
|
||||
<span class="btn-full">خروجی اکسل</span>
|
||||
<span class="btn-short">خروجی</span>
|
||||
</UiButton>
|
||||
@@ -1042,26 +1054,61 @@ onUnmounted(() => {
|
||||
@settled="onImportSettled"
|
||||
>
|
||||
<template #default="{ loading }">
|
||||
<UiButton variant="ghost" type="button" :disabled="busy" :loading="loading">
|
||||
<UiButton variant="ghost" size="sm" type="button" :disabled="busy" :loading="loading">
|
||||
<span class="btn-full">ورود از اکسل</span>
|
||||
<span class="btn-short">ورود</span>
|
||||
</UiButton>
|
||||
</template>
|
||||
</ExcelImportFlow>
|
||||
<UiButton variant="primary" @click="activityModal = {}">
|
||||
<UiButton
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="icon-only"
|
||||
title="فعالیت جدید"
|
||||
aria-label="فعالیت جدید"
|
||||
@click="activityModal = {}"
|
||||
>
|
||||
<UiIcon name="plus" :size="16" />
|
||||
فعالیت جدید
|
||||
</UiButton>
|
||||
<RouterLink :to="{ name: 'activities-calendar' }">
|
||||
<UiButton variant="ghost">
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="icon-only"
|
||||
title="تقویم"
|
||||
aria-label="تقویم"
|
||||
>
|
||||
<UiIcon name="calendar" :size="16" />
|
||||
تقویم
|
||||
</UiButton>
|
||||
</RouterLink>
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
<div class="view-toggle" role="group" aria-label="نوع نما">
|
||||
<button
|
||||
type="button"
|
||||
class="seg"
|
||||
:class="{ on: viewMode === 'tabs' }"
|
||||
title="نمای تب"
|
||||
aria-label="نمای تب"
|
||||
:aria-pressed="viewMode === 'tabs'"
|
||||
@click="setViewMode('tabs')"
|
||||
>
|
||||
<UiIcon name="list" :size="16" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="seg"
|
||||
:class="{ on: viewMode === 'board' }"
|
||||
title="نمای ستونی"
|
||||
aria-label="نمای ستونی"
|
||||
:aria-pressed="viewMode === 'board'"
|
||||
@click="setViewMode('board')"
|
||||
>
|
||||
<UiIcon name="grid" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view-row">
|
||||
<div class="desk-bar">
|
||||
<div v-if="showDeskTabs" class="desk-tabs" role="tablist" aria-label="محدوده میز کار">
|
||||
<button
|
||||
v-for="tab in DESK_TABS"
|
||||
@@ -1075,119 +1122,9 @@ onUnmounted(() => {
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-row-end">
|
||||
<UiDropdownMenu
|
||||
v-if="boards.length"
|
||||
:open="boardMenuOpen"
|
||||
align="start"
|
||||
@update:open="(v) => (boardMenuOpen = v)"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<button
|
||||
type="button"
|
||||
class="board-trigger"
|
||||
aria-label="گروه فعالیت"
|
||||
:title="currentBoard?.name"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<UiIcon name="layers" :size="14" />
|
||||
<span>{{ currentBoard?.name ?? 'گروه' }}</span>
|
||||
<UiIcon name="chevron" :size="13" class="board-chev" />
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-for="b in boards"
|
||||
:key="b.id"
|
||||
type="button"
|
||||
:class="{ on: b.id === board }"
|
||||
@click="setBoard(b.id)"
|
||||
>
|
||||
{{ b.name }}
|
||||
</button>
|
||||
<template v-if="canManageStages">
|
||||
<hr class="board-menu-sep" />
|
||||
<button type="button" class="board-menu-action" @click="openCreateBoard">
|
||||
<UiIcon name="plus" :size="14" />
|
||||
گروه جدید
|
||||
</button>
|
||||
<button type="button" class="board-menu-action" @click="openEditBoard">
|
||||
<UiIcon name="pencil" :size="14" />
|
||||
ویرایش این گروه
|
||||
</button>
|
||||
</template>
|
||||
</UiDropdownMenu>
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="stagesManagerOpen = true"
|
||||
>
|
||||
<UiIcon name="settings" :size="15" />
|
||||
مدیریت ستونها
|
||||
</UiButton>
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="typesManagerOpen = true"
|
||||
>
|
||||
<UiIcon name="sliders" :size="15" />
|
||||
انواع فعالیت و فیلدها
|
||||
</UiButton>
|
||||
<UiSegmented v-model="viewModel" :options="VIEW_OPTIONS">
|
||||
<template #icon="{ option }">
|
||||
<UiIcon :name="option.icon ?? 'list'" :size="15" />
|
||||
</template>
|
||||
</UiSegmented>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'tabs'" class="tabs" role="tablist" aria-label="ستونهای فعالیت">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: stage === 'all' }"
|
||||
:aria-selected="stage === 'all'"
|
||||
@click="setStage('all')"
|
||||
>
|
||||
<span>همه</span>
|
||||
<em>{{ toFaDigits(stageCounts.total.toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
<button
|
||||
v-for="s in stages"
|
||||
:key="s.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: stage === s.id }"
|
||||
:aria-selected="stage === s.id"
|
||||
@click="setStage(s.id)"
|
||||
>
|
||||
<i class="stage-dot" :style="stageDotStyle(s)" aria-hidden="true" />
|
||||
<span>{{ s.name }}</span>
|
||||
<em>{{ toFaDigits((stageCounts.counts[s.id] ?? 0).toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="time-chips" role="tablist" aria-label="فیلتر زمانی">
|
||||
<button
|
||||
v-for="tab in TIME_TABS"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: filter === tab.key, late: tab.key === 'overdue' }"
|
||||
:aria-selected="filter === tab.key"
|
||||
@click="setFilter(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<em>{{ toFaDigits(counts[tab.key].toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<form class="search" @submit.prevent="onSearch">
|
||||
<UiIcon name="search" :size="18" />
|
||||
<UiIcon name="search" :size="16" />
|
||||
<input
|
||||
v-model="q"
|
||||
type="search"
|
||||
@@ -1201,9 +1138,9 @@ onUnmounted(() => {
|
||||
:class="{ on: activeFacetCount > 0 }"
|
||||
@click="filterOpen = !filterOpen"
|
||||
>
|
||||
<UiIcon name="filter" :size="16" />
|
||||
<UiIcon name="filter" :size="15" />
|
||||
{{ filterBtnLabel }}
|
||||
<UiIcon name="chevron" :size="14" class="chev" />
|
||||
<UiIcon name="chevron" :size="13" class="chev" />
|
||||
</button>
|
||||
<div v-if="filterOpen" class="popover filter-popover">
|
||||
<p class="facet-title">نوع کار</p>
|
||||
@@ -1259,6 +1196,88 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-chips" role="tablist" aria-label="فیلتر زمانی">
|
||||
<button
|
||||
v-for="tab in TIME_TABS"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: filter === tab.key, late: tab.key === 'overdue' }"
|
||||
:aria-selected="filter === tab.key"
|
||||
@click="setFilter(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<em>{{ toFaDigits(counts[tab.key].toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
</div>
|
||||
<UiDropdownMenu
|
||||
v-if="boards.length"
|
||||
:open="boardMenuOpen"
|
||||
align="start"
|
||||
@update:open="(v) => (boardMenuOpen = v)"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<button
|
||||
type="button"
|
||||
class="board-trigger"
|
||||
aria-label="گروه فعالیت"
|
||||
:title="currentBoard?.name"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<UiIcon name="layers" :size="14" />
|
||||
<span>{{ currentBoard?.name ?? 'گروه' }}</span>
|
||||
<UiIcon name="chevron" :size="13" class="board-chev" />
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-for="b in boards"
|
||||
:key="b.id"
|
||||
type="button"
|
||||
:class="{ on: b.id === board }"
|
||||
@click="setBoard(b.id)"
|
||||
>
|
||||
{{ b.name }}
|
||||
</button>
|
||||
<template v-if="canManageStages">
|
||||
<hr class="board-menu-sep" />
|
||||
<button type="button" class="board-menu-action" @click="openCreateBoard">
|
||||
<UiIcon name="plus" :size="14" />
|
||||
گروه جدید
|
||||
</button>
|
||||
<button type="button" class="board-menu-action" @click="openEditBoard">
|
||||
<UiIcon name="pencil" :size="14" />
|
||||
ویرایش این گروه
|
||||
</button>
|
||||
</template>
|
||||
</UiDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'tabs'" class="tabs" role="tablist" aria-label="ستونهای فعالیت">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: stage === 'all' }"
|
||||
:aria-selected="stage === 'all'"
|
||||
@click="setStage('all')"
|
||||
>
|
||||
<span>همه</span>
|
||||
<em>{{ toFaDigits(stageCounts.total.toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
<button
|
||||
v-for="s in stages"
|
||||
:key="s.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: stage === s.id }"
|
||||
:aria-selected="stage === s.id"
|
||||
@click="setStage(s.id)"
|
||||
>
|
||||
<i class="stage-dot" :style="stageDotStyle(s)" aria-hidden="true" />
|
||||
<span>{{ s.name }}</span>
|
||||
<em>{{ toFaDigits((stageCounts.counts[s.id] ?? 0).toLocaleString('en-US')) }}</em>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
|
||||
@@ -1784,18 +1803,148 @@ onUnmounted(() => {
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
.desk-chrome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desk-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px 10px;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desk-title {
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.desk-title h1 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.desk-title .lead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desk-title .lead > span:last-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lead-sep {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.desk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
margin-inline-start: auto;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desk-actions :deep(.icon-only) {
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
background: #e8eef1;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.view-toggle .seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
background: transparent;
|
||||
color: #7b8794;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-toggle .seg.on {
|
||||
background: #fff;
|
||||
color: var(--accent-deep);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.desk-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desk-bar .tools {
|
||||
flex: 0 1 340px;
|
||||
min-width: 200px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.desk-bar .search {
|
||||
flex: 1 1 160px;
|
||||
max-width: 280px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.desk-bar .filter-btn {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
display: inline-flex;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
color: #94a3b8;
|
||||
font-size: 0.78rem;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.crumbs a {
|
||||
@@ -1896,9 +2045,10 @@ h1 {
|
||||
|
||||
.desk-tabs {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 12px;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.15rem;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
width: fit-content;
|
||||
}
|
||||
@@ -1908,11 +2058,12 @@ h1 {
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font: inherit;
|
||||
font-size: 0.86rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 10px;
|
||||
padding: 0.22rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desk-tabs button.on {
|
||||
@@ -1940,16 +2091,16 @@ h1 {
|
||||
.board-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: 0.3rem;
|
||||
height: 30px;
|
||||
max-width: 14rem;
|
||||
padding: 0 0.6rem;
|
||||
max-width: 12rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface, #fff);
|
||||
color: var(--ink, #0f172a);
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -2041,21 +2192,22 @@ h1 {
|
||||
|
||||
.time-chips {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.time-chips button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.3rem;
|
||||
height: 30px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.32rem 0.75rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -2810,8 +2962,16 @@ time.note {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.desk-top,
|
||||
.page-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.desk-actions {
|
||||
width: 100%;
|
||||
margin-inline-start: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
|
||||
Reference in New Issue
Block a user