This commit is contained in:
che
2026-07-25 13:18:07 +05:00
parent 0544d41b54
commit 249a4b455e
10 changed files with 824 additions and 212 deletions
+13 -1
View File
@@ -1,6 +1,6 @@
use che_tauri::{Filter, FilterSet};
use super::models::{Employee, Message, Task};
use super::models::{Employee, Message, Task, TaskTransfer};
static EMPLOYEE_FILTERS: &[Filter] = &[
Filter::exact("id"),
@@ -24,6 +24,14 @@ static MESSAGE_FILTERS: &[Filter] = &[
Filter::exact("employee_id"),
];
static TASK_TRANSFER_FILTERS: &[Filter] = &[
Filter::exact("id"),
Filter::exact("employee_from_id"),
Filter::exact("employee_to_id"),
Filter::exact("status"),
Filter::exact("typ"),
];
pub fn employee_filterset() -> FilterSet<Employee> {
FilterSet::new(EMPLOYEE_FILTERS)
}
@@ -35,3 +43,7 @@ pub fn task_filterset() -> FilterSet<Task> {
pub fn message_filterset() -> FilterSet<Message> {
FilterSet::new(MESSAGE_FILTERS)
}
pub fn task_transfer_filterset() -> FilterSet<TaskTransfer> {
FilterSet::new(TASK_TRANSFER_FILTERS)
}
+8 -1
View File
@@ -21,11 +21,18 @@ impl AppModule for TaskModule {
serializers::employee_serializer(),
filters::employee_filterset(),
);
ctx.resource::<models::Task>(
ctx.remote_resource::<models::Task>(
"task",
"/api/persone/task",
serializers::task_serializer(),
filters::task_filterset(),
);
ctx.remote_resource::<models::TaskTransfer>(
"task_transfer",
"/api/persone/task_transfer",
serializers::task_transfer_serializer(),
filters::task_transfer_filterset(),
);
ctx.resource::<models::Message>(
"message",
serializers::message_serializer(),
@@ -42,3 +42,22 @@ pub struct Message {
pub text: String,
}
#[derive(Debug, Clone, Model)]
#[model(table = "task_transfer")]
pub struct TaskTransfer {
#[field(primary_key)]
pub id: i64,
#[field(foreign_key = Employee)]
pub employee_from_id: Option<i64>,
#[field(foreign_key = Employee)]
pub employee_to_id: Option<i64>,
pub date_create: String,
pub status: String,
pub typ: String,
}
@@ -1,6 +1,6 @@
use che_tauri::{Field, ModelSerializer};
use super::models::{Employee, Message, Task};
use super::models::{Employee, Message, Task, TaskTransfer};
static EMPLOYEE_FIELDS: &[Field] = &[
Field::new("id").read_only(),
@@ -23,6 +23,15 @@ static MESSAGE_FIELDS: &[Field] = &[
Field::new("text"),
];
static TASK_TRANSFER_FIELDS: &[Field] = &[
Field::new("id").read_only(),
Field::new("employee_from_id").required(false).nullable(),
Field::new("employee_to_id").required(false).nullable(),
Field::new("date_create").read_only(),
Field::new("status"),
Field::new("typ"),
];
pub fn employee_serializer() -> ModelSerializer<Employee> {
ModelSerializer::new(EMPLOYEE_FIELDS)
}
@@ -34,3 +43,7 @@ pub fn task_serializer() -> ModelSerializer<Task> {
pub fn message_serializer() -> ModelSerializer<Message> {
ModelSerializer::new(MESSAGE_FIELDS)
}
pub fn task_transfer_serializer() -> ModelSerializer<TaskTransfer> {
ModelSerializer::new(TASK_TRANSFER_FIELDS)
}
+48
View File
@@ -6,6 +6,14 @@ use tauri::Manager;
use crate::sync::SyncContractsResult;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct CurrentEmployee {
id: i64,
name: String,
short_name: String,
avatar: Option<String>,
}
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
@@ -43,6 +51,45 @@ fn auth_status(api: tauri::State<'_, TauriApi>) -> bool {
api.is_authenticated()
}
#[tauri::command]
async fn current_employee(api: tauri::State<'_, TauriApi>) -> Result<CurrentEmployee, ApiError> {
let remote = api
.state()
.config
.remote
.as_ref()
.ok_or_else(|| ApiError::bad_request("current_employee requires [remote].base_url config"))?;
let token = api
.state()
.auth_token()
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
let response = reqwest::Client::new()
.get(format!(
"{}/api/persone/employee/who_im/",
remote.base_url.trim_end_matches('/')
))
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
.send()
.await
.map_err(|error| ApiError::new("remote_error", error.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let detail = response.text().await.unwrap_or_default();
return Err(ApiError::new(
"remote_error",
format!("who_im request failed with {status}: {detail}"),
));
}
response
.json::<CurrentEmployee>()
.await
.map_err(|error| ApiError::new("remote_error", error.to_string()))
}
#[tauri::command]
async fn sync_contracts(
app: tauri::AppHandle,
@@ -116,6 +163,7 @@ pub fn run() {
auth_set_token,
auth_logout,
auth_status,
current_employee,
sync_contracts,
load_application_file
])
+92 -31
View File
@@ -1,45 +1,91 @@
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { showToast } from "vant";
import { taskApi } from "../../../generated/api";
import { employeeApi, taskApi } from "../../../generated/api";
import type { Employee, TaskCreate, TaskPersonInput } from "../../../generated/models";
import { useModelApi } from "../../../shared/composables/useModelApi";
import EmployeeSelect from "../../personnel/components/EmployeeSelect.vue";
const router = useRouter();
const newTaskName = ref("");
const selectedAuthorId = ref(0);
const deadline = ref("");
const taskText = ref("");
const selectedDoerId = ref(0);
const selectedResponsibleId = ref(0);
const {
creating,
error,
create: createTaskApi,
} = useModelApi(taskApi, {
const { creating, error, create: createTaskApi } = useModelApi(taskApi, {
createErrorMessage: "Не удалось создать задачу",
autoLoad: false,
autoLoadOnFilterChange: false,
});
const { retrieve: loadEmployee } = useModelApi(employeeApi, {
autoLoad: false,
autoLoadOnFilterChange: false,
});
const canSubmit = computed(() => Boolean(deadline.value.trim()) && Boolean(selectedDoerId.value));
function makeShortName(name: string) {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) {
return "";
}
if (parts.length === 1) {
return parts[0];
}
return `${parts[0]} ${parts.slice(1).map((part) => `${part[0]?.toUpperCase()}.`).join(" ")}`.trim();
}
async function buildPerson(id: number): Promise<TaskPersonInput | null> {
if (!id) {
return null;
}
const employee = await loadEmployee(id);
return employeeToTaskPerson(employee);
}
function employeeToTaskPerson(employee: Employee): TaskPersonInput {
return {
id: employee.id,
name: employee.name,
short_name: makeShortName(employee.name),
avatar_small: employee.avatar_small,
};
}
async function createTask() {
const name = newTaskName.value.trim();
if (!name) {
showToast("Введите название задачи");
if (!canSubmit.value) {
showToast("Выберите исполнителя и срок");
return;
}
const task = await createTaskApi({
name,
author_id: selectedAuthorId.value || null,
responsible_id: selectedResponsibleId.value || null,
});
const doer = await buildPerson(selectedDoerId.value);
if (!doer) {
showToast("Не удалось загрузить исполнителя");
return;
}
const responsible = await buildPerson(selectedResponsibleId.value);
const payload: TaskCreate = {
doer,
deadline: deadline.value,
text: taskText.value.trim() || undefined,
responsible,
};
const task = await createTaskApi(payload);
if (task) {
showToast("Задача создана");
router.push("/tasks");
router.push(`/tasks/${task.id}`);
}
}
</script>
<template>
@@ -55,24 +101,40 @@ async function createTask() {
/>
<van-form class="card" @submit="createTask">
<van-field
v-model="newTaskName"
name="name"
label="Название"
placeholder="Например: Купить молоко"
clearable
:disabled="creating"
/>
<van-field label="Автор">
<van-field label="Исполнитель" required>
<template #input>
<EmployeeSelect v-model="selectedAuthorId" />
<EmployeeSelect v-model="selectedDoerId" />
</template>
</van-field>
<van-field label="Ответственный">
<template #input>
<EmployeeSelect v-model="selectedResponsibleId" />
</template>
</van-field>
<van-field
v-model="deadline"
name="deadline"
label="Срок"
type="date"
required
clearable
:disabled="creating"
/>
<van-field
v-model="taskText"
name="text"
label="Текст"
type="textarea"
rows="3"
autosize
clearable
placeholder="Описание задачи"
:disabled="creating"
/>
<div class="form-actions stacked-actions">
<van-button
block
@@ -80,12 +142,11 @@ async function createTask() {
type="primary"
native-type="submit"
:loading="creating"
:disabled="!newTaskName.trim()"
:disabled="!canSubmit"
>
Создать задачу
</van-button>
<van-button block round plain type="default" @click="router.back()">Отмена</van-button>
</div>
</van-form>
</template>
+41 -118
View File
@@ -1,16 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { showToast } from "vant";
import { employeeApi, messageApi, taskApi } from "../../../generated/api";
import { taskApi } from "../../../generated/api";
import { useModelApi } from "../../../shared/composables/useModelApi";
import EmployeeSelect from "../../personnel/components/EmployeeSelect.vue";
const route = useRoute();
const router = useRouter();
const taskId = computed(() => Number(route.params.id));
const messageText = ref("");
const selectedEmployeeId = ref(0);
const {
item: task,
@@ -22,80 +18,43 @@ const {
autoLoad: false,
autoLoadOnFilterChange: false,
});
const {
items: messages,
loading: loadingMessages,
creating: sendingMessage,
error: messageError,
load: loadMessages,
create: createMessageApi,
} = useModelApi(messageApi, {
loadErrorMessage: "Не удалось загрузить сообщения",
createErrorMessage: "Не удалось отправить сообщение",
autoLoad: false,
autoLoadOnFilterChange: false,
});
const { items: employees } = useModelApi(employeeApi, {
defaultListParams: { ordering: "id" },
loadErrorMessage: "Не удалось загрузить сотрудников",
});
async function sendMessage() {
if (!Number.isFinite(taskId.value)) {
showToast("Некорректный ID задачи");
return;
function personName(person: { short_name?: string | null; name?: string | null } | null) {
if (!person) {
return "не указан";
}
const text = messageText.value.trim();
if (!text) {
showToast("Введите сообщение");
return;
}
if (!selectedEmployeeId.value) {
showToast("Выберите сотрудника");
return;
}
const message = await createMessageApi({
task_id: taskId.value,
employee_id: selectedEmployeeId.value,
text,
});
if (message) {
messageText.value = "";
await loadMessages({ task_id: taskId.value });
showToast("Сообщение отправлено");
}
return person.short_name ?? person.name ?? "не указан";
}
function employeeName(id: number) {
return employees.value.find((employee) => employee.id === id)?.name ?? `#${id}`;
function taskTitle() {
return task.value?.text?.trim() || task.value?.result?.trim() || `Задача #${taskId.value}`;
}
function employeeAvatar(id: number) {
return employees.value.find((employee) => employee.id === id)?.avatar_small ?? "";
function formatDate(value: string | null | undefined) {
return value || "не указано";
}
function formatBoolean(value: boolean) {
return value ? "Да" : "Нет";
}
onMounted(() => {
if (Number.isFinite(taskId.value)) {
loadTask(taskId.value);
loadMessages({ task_id: taskId.value });
}
});
</script>
<template>
<van-notice-bar
v-if="error || messageError"
v-if="error"
class="notice"
color="#991b1b"
background="#fee2e2"
left-icon="warning-o"
wrapable
:scrollable="false"
:text="error || messageError"
:text="error"
/>
<section class="card detail-card">
@@ -106,9 +65,21 @@ onMounted(() => {
<template v-else>
<van-cell-group inset>
<van-cell title="ID" :value="task.id" />
<van-cell title="Название" :value="task.name" />
<van-cell title="Автор" :value="task.author_id ?? 'не указан'" />
<van-cell title="Ответственный" :value="task.responsible_id ?? 'не указан'" />
<van-cell title="Заголовок" :value="taskTitle()" />
<van-cell title="Исполнитель" :value="personName(task.doer)" />
<van-cell title="Автор" :value="personName(task.author)" />
<van-cell title="Ответственный" :value="personName(task.responsible)" />
<van-cell title="Статус" :value="task.get_status" />
<van-cell title="Срок" :value="formatDate(task.deadline)" />
<van-cell title="Плановая дата" :value="formatDate(task.plan_date)" />
<van-cell title="Запрос новой даты" :value="formatDate(task.request_new_deadline)" />
<van-cell title="Приоритет" :value="task.priority ?? 'не указан'" />
<van-cell title="Тип" :value="task.typ || 'не указан'" />
<van-cell title="Результат" :value="task.result || 'не указан'" />
<van-cell title="Комментарий" :value="task.comment || 'не указан'" />
<van-cell title="Примечание" :value="task.note || 'не указано'" />
<van-cell title="Согласование" :value="formatBoolean(task.approve)" />
<van-cell title="Архив" :value="formatBoolean(task.archive)" />
</van-cell-group>
</template>
</section>
@@ -116,73 +87,25 @@ onMounted(() => {
<section class="card messages-card">
<div class="list-title">
<h2>Диалог</h2>
<van-button
size="small"
plain
type="primary"
:loading="loadingMessages"
@click="loadMessages({ task_id: taskId })"
>
Обновить
</van-button>
</div>
<van-loading v-if="loadingMessages && messages.length === 0" class="state" type="spinner">
Загрузка сообщений...
</van-loading>
<van-empty v-else-if="messages.length === 0" description="Сообщений пока нет" />
<van-empty v-if="!task?.message_set?.length" description="Сообщений пока нет" />
<div v-else class="message-list">
<article v-for="message in messages" :key="message.id" class="message-item">
<van-image
class="message-avatar"
round
width="40"
height="40"
:src="employeeAvatar(message.employee_id)"
/>
<article v-for="message in task.message_set" :key="message.id" class="message-item">
<van-image class="message-avatar" round width="40" height="40" :src="message.author.avatar_small ?? ''" />
<div class="message-bubble">
<div class="message-author">{{ employeeName(message.employee_id) }}</div>
<div class="message-author">{{ personName(message.author) }}</div>
<div class="message-text">{{ message.text }}</div>
<div class="message-meta">
{{ message.date }} · {{ message.status }} · {{ personName(message.recipient) }}
</div>
</div>
</article>
</div>
</section>
<van-form class="card" @submit="sendMessage">
<van-field
name="employee_id"
label="Отправитель"
placeholder="Выберите сотрудника"
>
<template #input>
<EmployeeSelect v-model="selectedEmployeeId" />
</template>
</van-field>
<van-field
v-model="messageText"
name="text"
label="Сообщение"
type="textarea"
rows="2"
autosize
clearable
placeholder="Напишите сообщение"
:disabled="sendingMessage"
/>
<div class="form-actions stacked-actions">
<van-button
block
round
type="primary"
native-type="submit"
:loading="sendingMessage"
:disabled="!Number.isFinite(taskId) || !messageText.trim() || !selectedEmployeeId"
>
Отправить сообщение
</van-button>
<van-button block round type="primary" plain @click="router.back()">Назад</van-button>
</div>
</van-form>
<div class="form-actions stacked-actions">
<van-button block round type="primary" plain @click="router.back()">Назад</van-button>
</div>
</template>
+345 -46
View File
@@ -1,78 +1,377 @@
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { employeeApi, taskApi } from "../../../generated/api";
import { taskApi, taskTransferApi } from "../../../generated/api";
import type {
Task,
TaskCreate,
TaskListParams,
TaskTransfer,
TaskTransferListParams,
TaskUpdate,
} from "../../../generated/models";
import { useModelApi } from "../../../shared/composables/useModelApi";
type CurrentEmployee = {
id: number;
name: string;
short_name: string;
avatar?: string | null;
};
type MainTabKey = "incoming" | "transfer" | "review" | "outgoing" | "archive";
type DocTabKey =
| "all"
| "simple"
| "memo"
| "bill"
| "contract"
| "contract_application"
| "entry_letter"
| "outgoing_letter";
const router = useRouter();
const mainTab = ref<MainTabKey>("incoming");
const docTab = ref<DocTabKey>("all");
const employee = ref<CurrentEmployee | null>(null);
const employeeError = ref("");
const employeeLoading = ref(false);
const refreshing = ref(false);
const switchingList = ref(false);
let suppressDocTabReload = false;
const {
items: tasks,
loading,
error,
loading: taskLoading,
error: taskError,
load: loadTasks,
} = useModelApi(taskApi, {
defaultListParams: { ordering: "id" },
} = useModelApi<Task, TaskCreate, TaskUpdate, TaskListParams>(taskApi, {
defaultListParams: { ordering: "-id" },
loadErrorMessage: "Не удалось загрузить задачи",
});
const { items: employees } = useModelApi(employeeApi, {
defaultListParams: { ordering: "id" },
loadErrorMessage: "Не удалось загрузить сотрудников",
const {
items: taskTransfers,
loading: taskTransferLoading,
error: taskTransferError,
load: loadTaskTransfers,
} = useModelApi<
TaskTransfer,
Partial<TaskTransfer>,
Partial<TaskTransfer>,
TaskTransferListParams
>(taskTransferApi, {
defaultListParams: { ordering: "-id" },
loadErrorMessage: "Не удалось загрузить переносы срока",
});
function employeeName(id: number | null) {
if (id === null || id === 0) {
const mainTabs = [
{ key: "incoming", label: "Входящие", icon: "todo-list-o" },
{ key: "transfer", label: "Перенос срока", icon: "underway-o" },
{ key: "review", label: "Для проверки", icon: "eye-o" },
{ key: "outgoing", label: "Исходящие", icon: "send-gift-o" },
{ key: "archive", label: "Архив", icon: "folder-o" },
] as const;
const docTabs = [
{ key: "all", label: "Все" },
{ key: "simple", label: "Простые" },
{ key: "memo", label: "Служебные" },
{ key: "bill", label: "Счета" },
{ key: "contract", label: "Договоры" },
{ key: "contract_application", label: "Приложения" },
{ key: "entry_letter", label: "Входящие письма" },
{ key: "outgoing_letter", label: "Исходящие письма" },
] as const;
const visibleItems = computed(() =>
mainTab.value === "transfer" ? taskTransfers.value : tasks.value,
);
const activeLoading = computed(() =>
mainTab.value === "transfer" ? taskTransferLoading.value : taskLoading.value,
);
const showEmployeeLoading = computed(
() =>
employeeLoading.value &&
visibleItems.value.length === 0 &&
!switchingList.value,
);
const activeError = computed(() =>
mainTab.value === "transfer" ? taskTransferError.value : taskError.value,
);
const showDocTabs = computed(() => mainTab.value !== "transfer");
function personName(
person: { short_name?: string | null; name?: string | null } | null,
) {
if (!person) {
return "не указан";
}
return employees.value.find((employee) => employee.id === id)?.name ?? `#${id}`;
return person.short_name ?? person.name ?? "не указан";
}
function taskTitle(task: {
id: number;
text?: string | null;
result?: string | null;
}) {
return task.text?.trim() || task.result?.trim() || `Задача #${task.id}`;
}
function isTaskTransfer(item: Task | TaskTransfer): item is TaskTransfer {
return "employee_from" in item;
}
function itemTitle(item: Task | TaskTransfer) {
return isTaskTransfer(item) ? taskTitle(item.task) : taskTitle(item);
}
function itemLabel(item: Task | TaskTransfer) {
if (isTaskTransfer(item)) {
return `Из ${personName(item.employee_from)} в ${personName(item.employee_to)} · Срок: ${item.task.deadline} · Создан: ${item.date_create}`;
}
return `ID: ${item.id} · Исполнитель: ${personName(item.doer)} · Ответственный: ${personName(item.responsible)} · Срок: ${item.deadline}`;
}
function itemStatus(item: Task | TaskTransfer) {
return isTaskTransfer(item) ? item.status : item.get_status || item.status;
}
function itemPath(item: Task | TaskTransfer) {
return isTaskTransfer(item) ? `/tasks/${item.task.id}` : `/tasks/${item.id}`;
}
function taskParams(): TaskListParams | TaskTransferListParams | null {
if (!employee.value) {
return null;
}
const employeeId = employee.value.id;
switch (mainTab.value) {
case "incoming":
return {
doer: employeeId,
archive: false,
typ: docTab.value === "all" ? undefined : docTab.value,
q: "entry",
incomplete: true,
};
case "review":
return {
q: "to_approve",
incomplete: true,
typ: docTab.value === "all" ? undefined : docTab.value,
};
case "outgoing":
return {
q: "outgoing",
archive: false,
incomplete: true,
typ: docTab.value === "all" ? undefined : docTab.value,
};
case "archive":
return {
typ: docTab.value === "all" ? undefined : docTab.value,
};
default:
return null;
}
}
async function loadCurrentEmployee() {
employeeLoading.value = true;
employeeError.value = "";
try {
employee.value = await invoke<CurrentEmployee>("current_employee");
} catch (error) {
employee.value = null;
employeeError.value =
error instanceof Error
? error.message
: "Не удалось получить текущего сотрудника";
} finally {
employeeLoading.value = false;
}
}
async function reloadList() {
if (!employee.value) {
return;
}
const params = taskParams();
if (!params) {
return;
}
if (mainTab.value === "transfer") {
await loadTaskTransfers({ employee_to: employee.value.id, status: "A" });
return;
}
await loadTasks(params as TaskListParams);
}
async function reloadForTabChange() {
switchingList.value = true;
try {
await reloadList();
} finally {
switchingList.value = false;
}
}
async function onRefresh() {
refreshing.value = true;
try {
await reloadList();
} finally {
refreshing.value = false;
}
}
watch(mainTab, () => {
if (mainTab.value === "transfer") {
suppressDocTabReload = true;
docTab.value = "all";
void nextTick(() => {
suppressDocTabReload = false;
});
}
void reloadForTabChange();
});
watch(docTab, () => {
if (showDocTabs.value && !suppressDocTabReload) {
void reloadForTabChange();
}
});
onMounted(async () => {
await loadCurrentEmployee();
await reloadList();
});
</script>
<template>
<van-notice-bar
v-if="error"
v-if="employeeError || activeError"
class="notice"
color="#991b1b"
background="#fee2e2"
left-icon="warning-o"
wrapable
:scrollable="false"
:text="error"
:text="employeeError || activeError"
/>
<section class="card list-card">
<div class="list-title">
<h2>Список</h2>
<div class="list-actions">
<van-button size="small" plain type="primary" :loading="loading" @click="loadTasks()">
Обновить
</van-button>
<van-button size="small" round type="primary" icon="plus" @click="router.push('/tasks/new')" />
</div>
</div>
<van-loading v-if="loading && tasks.length === 0" class="state" type="spinner">
Загрузка...
</van-loading>
<van-empty v-else-if="tasks.length === 0" description="Задач пока нет" />
<van-cell-group v-else inset>
<van-cell
v-for="task in tasks"
:key="task.id"
:title="task.name"
center
is-link
@click="router.push(`/tasks/${task.id}`)"
<van-tabbar
v-model="mainTab"
:fixed="false"
:border="false"
class="task-tabbar"
>
<van-tabbar-item
v-for="tab in mainTabs"
:key="tab.key"
:name="tab.key"
:icon="tab.icon"
>
<template #label>
ID: {{ task.id }} · Автор: {{ employeeName(task.author_id) }} · Ответственный:
{{ employeeName(task.responsible_id) }}
</template>
<template #right-icon>
<van-tag plain type="primary">Task</van-tag>
</template>
</van-cell>
</van-cell-group>
{{ tab.label }}
</van-tabbar-item>
</van-tabbar>
<van-tabs
v-if="showDocTabs"
v-model:active="docTab"
shrink
class="tabs tabs-secondary"
>
<van-tab
v-for="tab in docTabs"
:key="tab.key"
:name="tab.key"
:title="tab.label"
/>
</van-tabs>
<van-pull-refresh
v-if="!switchingList"
v-model="refreshing"
class="pull-refresh"
@refresh="onRefresh"
>
<van-list
:loading="activeLoading || refreshing"
:finished="true"
loading-text="Загрузка..."
:immediate-check="false"
>
<van-loading v-if="showEmployeeLoading" class="state" type="spinner">
Загрузка...
</van-loading>
<van-empty
v-else-if="visibleItems.length === 0"
description="Записей пока нет"
/>
<van-cell-group v-else inset>
<van-cell
v-for="item in visibleItems"
:key="item.id"
:title="itemTitle(item)"
center
is-link
@click="router.push(itemPath(item))"
>
<template #label>
{{ itemLabel(item) }}
</template>
<template #right-icon>
<van-tag plain type="primary">{{ itemStatus(item) }}</van-tag>
</template>
</van-cell>
</van-cell-group>
</van-list>
</van-pull-refresh>
<van-loading v-else class="state" type="spinner">Загрузка...</van-loading>
</section>
<van-floating-bubble icon="plus" @click="router.push('/tasks/new')" />
</template>
<style scoped>
.list-card {
display: flex;
flex-direction: column;
}
.task-tabbar {
position: sticky;
top: 0;
z-index: 2;
margin: -12px -12px 8px;
border-radius: 0;
}
.pull-refresh {
min-height: calc(100vh - 220px);
}
@media (max-width: 768px) {
.pull-refresh {
min-height: calc(100vh - 200px);
}
}
</style>
+3
View File
@@ -36,6 +36,8 @@ import type {
TaskCreate,
TaskUpdate,
TaskListParams,
TaskTransfer,
TaskTransferListParams,
User,
UserCreate,
UserUpdate,
@@ -51,4 +53,5 @@ export const frcApi = createModelApi<Frc, FrcCreate, FrcUpdate, FrcListParams>("
export const messageApi = createModelApi<Message, MessageCreate, MessageUpdate, MessageListParams>("message");
export const projectApi = createModelApi<Project, ProjectCreate, ProjectUpdate, ProjectListParams>("project");
export const taskApi = createModelApi<Task, TaskCreate, TaskUpdate, TaskListParams>("task");
export const taskTransferApi = createModelApi<TaskTransfer, Partial<TaskTransfer>, Partial<TaskTransfer>, TaskTransferListParams>("task_transfer");
export const userApi = createModelApi<User, UserCreate, UserUpdate, UserListParams>("users");
+241 -14
View File
@@ -319,6 +319,107 @@ export interface MessageListParams extends ListParams {
employee_id?: number;
}
export interface TaskTransferPerson {
id: number;
name: string;
short_name: string;
first_position: string;
get_main_position: string;
departament_name: string;
company: Record<string, unknown> | null;
}
export interface TaskTransferProject {
id: number;
name: string;
short_name: string;
tender: string;
get_absolute_url: string;
}
export interface TaskTransferTask {
id: number;
text: string;
author: TaskTransferPerson;
doer: TaskTransferPerson;
deadline: string;
get_last_day: string;
status: string;
project: TaskTransferProject | null;
}
export interface TaskTransfer {
id: number;
employee_from: TaskTransferPerson;
employee_to: TaskTransferPerson;
date_create: string;
task: TaskTransferTask;
status: string;
typ: string;
}
export interface TaskTransferListParams extends ListParams {
id?: number;
employee_from?: number;
employee_to?: number;
status?: string;
typ?: string;
date_create?: string;
}
export interface TaskPosition {
id: number;
frc_name: string;
name: string;
frc: number;
}
export interface TaskPerson {
id: number;
name: string;
short_name: string;
avatar: string | null;
avatar_small: string | null;
first_position: string;
company: Record<string, unknown> | null;
position: TaskPosition[];
}
export interface TaskPersonInput {
id?: number;
name: string;
short_name: string;
avatar?: string | null;
avatar_small?: string | null;
first_position?: string;
company?: Record<string, unknown> | null;
position?: TaskPosition[];
}
export interface TaskProject {
id: number;
name: string;
short_name: string;
tender: string;
get_absolute_url: string;
}
export interface TaskMessage {
id: number;
author: TaskPerson;
text: string;
request_new_deadline: string | null;
recipient: TaskPerson;
date: string;
status: string;
task: number;
}
export interface TaskRelatedObject {
id: number;
[key: string]: unknown;
}
export interface Project {
id: number;
name: string;
@@ -354,29 +455,156 @@ export interface ProjectListParams extends ListParams {
export interface Task {
id: number;
name: string;
author_id: number | null;
responsible_id: number | null;
project: TaskProject | null;
doer: TaskPerson;
doer_name: string;
author: TaskPerson | null;
memo_full: TaskRelatedObject | null;
bill_full: TaskRelatedObject | null;
contract_full: TaskRelatedObject | null;
contract_application_full: TaskRelatedObject | null;
outgoing_letter_full: TaskRelatedObject | null;
entry_letter_full: TaskRelatedObject | null;
protocolitem_full: TaskRelatedObject | null;
decree_full: TaskRelatedObject | null;
delivery_full: TaskRelatedObject | null;
get_status: string;
get_status_class: string;
get_scan_url: string | null;
get_last_day: string;
frc_icon: string;
uploadfile_set: unknown[];
deadline: string;
request_new_deadline: string | null;
plan_date: string | null;
date: string;
message_set: TaskMessage[];
responsible: TaskPerson | null;
counterparty: TaskRelatedObject | null;
get_deadline_history: unknown[];
duration: number | null;
bid_full: TaskRelatedObject | null;
price_agreement_full: TaskRelatedObject | null;
transfer: unknown;
text: string;
status: string;
result: string;
complit_date: string | null;
comment: string;
note: string;
approve: boolean;
archive: boolean;
approve_date: string | null;
approve_required: boolean;
progress_status: string;
start_date: string | null;
end_date: string | null;
order_number: number | null;
priority: number | null;
typ: string;
stage: unknown;
contract: unknown;
questionnair: unknown;
contract_application: unknown;
entry_letter: unknown;
outgoing_letter: unknown;
protocol: unknown;
bill: unknown;
decree: unknown;
court_case: unknown;
bill_register: unknown;
price_agreement: unknown;
bid: unknown;
delivery: unknown;
scheduled_task: unknown;
report: unknown;
memo: unknown;
protocolitem: unknown;
related_note: unknown;
task: unknown;
}
export interface TaskCreate {
name: string;
author_id?: number | null;
responsible_id?: number | null;
project?: TaskProject | null;
doer: TaskPersonInput;
get_status?: string;
get_status_class?: string;
get_scan_url?: string | null;
get_last_day?: string;
frc_icon?: string;
deadline: string;
request_new_deadline?: string | null;
plan_date?: string | null;
responsible?: TaskPersonInput | null;
counterparty?: TaskRelatedObject | null;
bid_full?: TaskRelatedObject | null;
price_agreement_full?: TaskRelatedObject | null;
text?: string;
status?: string;
result?: string;
complit_date?: string | null;
comment?: string;
note?: string;
approve?: boolean;
archive?: boolean;
approve_date?: string | null;
approve_required?: boolean;
progress_status?: string;
start_date?: string | null;
end_date?: string | null;
order_number?: number | null;
priority?: number | null;
typ?: string;
}
export interface TaskUpdate {
name?: string;
author_id?: number | null;
responsible_id?: number | null;
project?: TaskProject | null;
doer?: TaskPersonInput;
get_status?: string;
get_status_class?: string;
get_scan_url?: string | null;
get_last_day?: string;
frc_icon?: string;
deadline?: string;
request_new_deadline?: string | null;
plan_date?: string | null;
responsible?: TaskPersonInput | null;
counterparty?: TaskRelatedObject | null;
bid_full?: TaskRelatedObject | null;
price_agreement_full?: TaskRelatedObject | null;
text?: string;
status?: string;
result?: string;
complit_date?: string | null;
comment?: string;
note?: string;
approve?: boolean;
archive?: boolean;
approve_date?: string | null;
approve_required?: boolean;
progress_status?: string;
start_date?: string | null;
end_date?: string | null;
order_number?: number | null;
priority?: number | null;
typ?: string;
}
export interface TaskListParams extends ListParams {
id?: number;
name?: string;
name__contains?: string;
author_id?: number | null;
responsible_id?: number | null;
text?: string;
text__contains?: string;
doer?: number | null;
author?: number | null;
archive?: boolean;
status?: string;
result?: string;
doer_name?: string;
doer_name__contains?: string;
deadline?: string;
progress_status?: string;
priority?: number | null;
typ?: string;
}
export interface User {
@@ -397,4 +625,3 @@ export interface UserListParams extends ListParams {
name?: string;
name__contains?: string;
}