Browse Source

feat: 流水账详情页分页、打卡

text-draggable
xuesinan 3 years ago
parent
commit
c4720b7fa0
  1. 19
      src/App.vue
  2. 17
      src/apis/index.js
  3. 2
      src/components/tall/plugin/Plugin.vue
  4. 1
      src/components/tall/top/TopNavbar.vue
  5. 524
      src/plugins/p-daily-account/p-daily-account-detail.vue
  6. 28
      src/views/home/Index.vue

19
src/App.vue

@ -6,14 +6,32 @@
<script setup>
import { useStore } from 'vuex';
import { computed } from 'vue';
import { v4 as uuidv4 } from 'uuid';
const store = useStore();
const projectInfo = computed(() => store.state.projects.project); //
const sessionProject = sessionStorage.getItem('project'); //
// queryu token
const userString = sessionStorage.getItem('user');
const sessionToken = sessionStorage.getItem('token');
if (sessionProject && !projectInfo.value.id) {
const info = JSON.parse(sessionProject);
store.commit('projects/setProject', info);
store.commit('task/setBusinessCode', info && info.businessCode ? info.businessCode : '');
}
if (projectInfo.value.id || sessionProject) {
const info = JSON.parse(sessionProject);
const projectId = projectInfo.value.id || info.id;
const url = projectInfo.value.url || info.url;
const param = { projectId };
store.dispatch('role/getAllMembers', { param, url });
}
//
store.commit('layout/setDeviceId', uuidv4().split('-')[0]);
@ -121,6 +139,7 @@ body,
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
color: rgba(0, 0, 0, 0.85);
z-index: 9999;
}
.ant-message .ant-message-notice {

17
src/apis/index.js

@ -81,6 +81,23 @@ export const getDeliverByTaskId = (params, url) => http.post(`${url}/deliver/get
// 检查交付物
export const checkDeliver = (params, url) => http.post(`${url}/deliver/checkDeliver`, params);
// 流水账
// 获取基本信息 (成员列表、项目列表)
export const getBasicInfo = url => http.post(`${url}/dailyAccount/info`);
// 获取任务列表
export const queryTasks = (params, url) => http.post(`${url}/dailyAccount/queryTasks`, params);
// 提交任务
export const submitTask = (params, url) => http.post(`${url}/dailyAccount/submitTask`, params);
// 当前打卡状态
export const clockQuery = (params, url) => http.post(`${url}/clock/query`, params);
// 打卡
export const clockPunch = (params, url) => http.post(`${url}/clock/punch`, params);
/**
* 导入wbs
* @param {object} e

2
src/components/tall/plugin/Plugin.vue

@ -3,7 +3,7 @@
<!-- 交付物插件 -->
<p-deliver v-else-if="pluginId === '15'" />
<!-- <p-deliver-second v-else-if="pluginId === '15'" /> -->
<!-- <p-deliver-second v-else-if="pluginId === '25'" /> -->
<!-- 资源管理 -->
<!-- <p-source-manage v-else-if="pluginId === '16'" class="p-2" /> -->
<!-- 财务审批统计 -->

1
src/components/tall/top/TopNavbar.vue

@ -73,6 +73,7 @@ h1 span {
text-align: center;
box-shadow: 0px 0 6px 0px rgba(0, 0, 0, 0.3);
background-color: #fff;
z-index: 9;
}
.user-info:hover .user-action {

524
src/plugins/p-daily-account/p-daily-account-detail.vue

@ -4,115 +4,182 @@
<div class="mb-8 flex justify-between items-center">
<div class="text-2xl font-semibold">流水账</div>
<div class="flex items-center">
<a-button type="primary">早打卡</a-button>
<a-button class="mx-5" type="primary">晚打卡</a-button>
<a-button type="primary" :disabled="morningStatus">{{ morningStatus ? '已打卡' : '早打卡' }}</a-button>
<a-button class="mx-5" type="primary" :disabled="nightStatus">{{ nightStatus ? '已打卡' : '晚打卡' }}</a-button>
<FullscreenExitOutlined v-if="isFullScreen" class="text-lg" style="color: #777" @click="isFullScreen = !isFullScreen" />
<FullscreenOutlined v-else class="text-lg" style="color: #777" @click="isFullScreen = !isFullScreen" />
</div>
</div>
<!-- 筛选 -->
<a-form class="flex flex-wrap" :model="formState" name="basic" autocomplete="off">
<a-form-item name="timeRange" label="时间" style="width: 250px; margin-right: 20px; margin-bottom: 12px">
<a-form class="flex flex-wrap" :model="formState">
<a-form-item name="timeRange" label="时间" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-range-picker v-model:value="formState.timeRange" />
</a-form-item>
<a-form-item name="staffRange" label="员工" style="width: 250px; margin-right: 20px; margin-bottom: 12px">
<a-select v-model:value="formState.staffRange" :options="options" mode="multiple" placeholder="请选择员工"></a-select>
<a-form-item name="staffRange" label="员工" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-select
v-model:value="formState.staffRange"
:options="memberList"
:field-names="{ label: 'empName', value: 'empName' }"
mode="multiple"
placeholder="请选择员工"
></a-select>
</a-form-item>
<a-form-item name="programName" label="项目" style="width: 250px; margin-right: 20px; margin-bottom: 12px">
<a-select v-model:value="formState.programName" :options="options" mode="multiple" placeholder="请输入项目名称"></a-select>
<a-form-item name="programName" label="项目" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-select
v-model:value="formState.programName"
:options="proList"
:field-names="{ label: 'projectName', value: 'projectName' }"
mode="multiple"
placeholder="请输入项目名称"
></a-select>
</a-form-item>
<div class="block w-full">
<a-button type="primary">筛选</a-button>
<a-button type="primary" html-type="submit" @click="handleSubmit">筛选</a-button>
<a-button class="mx-3" type="primary">导出</a-button>
<a-button>重置</a-button>
<a-button @click="resetData">重置</a-button>
</div>
</a-form>
<!-- 表格 -->
<a-table class="mt-6" :columns="columns" :data-source="data" bordered :scroll="{ x: 1400 }">
<template #bodyCell="{ column, text }">
<div style="height: 100px" :class="{ 'text-left': column.dataIndex === 'program' }" @dblclick="showModal">{{ text }}</div>
<a-table class="mt-6" :columns="columns" :data-source="columnDatas" bordered :pagination="false" :scroll="{ x: 1400 }">
<template #bodyCell="{ column, text, record }">
<div
style="height: 100px"
:class="{ 'text-left': column.dataIndex === 'program' }"
class="overflow-y-auto task-today"
@dblclick="showModal(record, column.proId)"
>
<template v-if="column.dataIndex === 'program'">
<template v-for="item in text">
<template v-if="column.proId === item.proId">
<div class="mb-3" v-for="(task, key) in item.tasks" :key="key">
<PushpinOutlined v-if="task.cooperation === 1" title="协作任务" />
<span class="ml-2">
我今日计划结果{{ key + 1 }}{{ task.taskName }}交付物是{{ task.deliverName }}截止时间{{
dayjs(+task.deadline).format('MM-DD')
}}时长{{ task.duration / 3600000 }}检查人{{ task.checker }}
</span>
</div>
</template>
</template>
</template>
<template v-else>
{{ text }}
</template>
</div>
</template>
</a-table>
<a-pagination
class="text-right"
v-model:current="current"
v-model:pageSize="pageSize"
:total="dataTotal"
@change="handlePage"
show-less-items
/>
</div>
<a-modal v-model:visible="visible" title="今日计划" centered :footer="null" width="800px">
<a-modal
v-model:visible="visible"
title="今日计划"
centered
:footer="null"
width="800px"
style="padding: 0; max-height: 800px; overflow-y: auto"
>
<div style="padding: 0 96px">
<a-form :model="modalFormState" name="basic" autocomplete="off">
<a-form :model="modalFormState">
<div v-for="(item, index) in modalFormState" :key="index">
<div class="flex justify-between items-center text-gray-400">
<div class="mb-5 text-right" style="width: 120px">计划结果{{ index + 1 }}</div>
<DeleteOutlined v-if="modalFormState.length > 1" @click="delFormItem(index)" />
</div>
<a-form-item name="planValue" style="margin-bottom: 20px">
<a-form-item name="taskName" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>今日计划结果1
<span style="color: #ff5353; margin-right: 5px">*</span>今日计划结果{{ index + 1 }}
</label>
<a-textarea v-model:value="item.planValue" placeholder="请输入今日计划" />
<a-textarea v-model:value="item.taskName" :disabled="isDisabled" placeholder="请输入今日计划" />
</div>
</a-form-item>
<a-form-item name="deliverable" style="margin-bottom: 20px">
<a-form-item name="deliverName" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>交付物
</label>
<a-input v-model:value="item.deliverable" placeholder="请输入交付物" />
<a-input v-model:value="item.deliverName" :disabled="isDisabled" placeholder="请输入交付物" />
</div>
</a-form-item>
<a-form-item name="deadline" style="margin-bottom: 20px">
<a-form-item name="showDeadLine" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>截止时间
</label>
<a-date-picker class="w-full" v-model:value="item.deadline" />
<a-date-picker class="w-full" v-model:value="item.showDeadLine" />
</div>
</a-form-item>
<a-form-item name="duration" style="margin-bottom: 20px">
<a-form-item name="showDuration" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>时长
</label>
<a-select
v-model:value="item.duration"
:options="workDurations"
placeholder="请输入工作时长"
@change="handleDuration($event, index)"
></a-select>
<a-input-group compact class="items-center" style="width: 100%; display: flex">
<a-auto-complete
style="width: calc(100% - 35px)"
v-model:value="item.showDuration"
:options="workDurations"
placeholder="请输入工作时长"
:disabled="isDisabled"
@change="handleDuration($event, index)"
/>
<div style="width: 35px; border: none" class="text-right">小时</div>
</a-input-group>
</div>
</a-form-item>
<a-form-item name="inspector" style="margin-bottom: 20px">
<a-form-item name="checker" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>检查人
</label>
<a-select
v-model:value="item.inspector"
:options="members"
v-model:value="item.checker"
:options="memberList"
:field-names="{ label: 'empName', value: 'empName' }"
show-search
placeholder="请选择检查人"
:field-names="{ label: 'name', value: 'memberId' }"
:disabled="isDisabled"
:filter-option="filterOption"
@change="handleInspector($event, index)"
></a-select>
</div>
</a-form-item>
<a-form-item name="address" style="margin-bottom: 20px">
<a-form-item name="cooperation" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>交付物地址
</label>
<a-textarea v-model:value="item.address" placeholder="请输入交付物地址" />
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px"> 是否协作 </label>
<a-radio-group v-model:value="item.cooperation" class="w-full items-center" style="display: flex">
<a-radio class="items-center" style="display: flex" :value="1"></a-radio>
<a-radio class="items-center" style="display: flex" :value="0"></a-radio>
</a-radio-group>
</div>
</a-form-item>
<a-form-item name="deliverLink" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px"> 交付物链接 </label>
<a-textarea v-model:value="item.deliverLink" :disabled="isDisabled" placeholder="请输入交付物链接" />
</div>
</a-form-item>
</div>
@ -138,105 +205,215 @@
<script setup>
import { useStore } from 'vuex';
import { reactive, ref, computed } from 'vue';
import { reactive, ref, onMounted, computed, watch } from 'vue';
import dayjs from 'dayjs';
import { FullscreenExitOutlined, FullscreenOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { FullscreenExitOutlined, FullscreenOutlined, DeleteOutlined, PushpinOutlined } from '@ant-design/icons-vue';
import { getBasicInfo, queryTasks, submitTask, clockQuery, clockPunch } from 'apis';
import { message } from 'ant-design-vue';
const store = useStore();
const isFullScreen = ref(false);
const projectId = computed(() => store.getters['project/projectId']);
const sessionProjectId = sessionStorage.getItem('projectId');
const roleId = computed(() => store.state.role.roleId);
const members = computed(() => store.state.role.members);
const isFullScreen = ref(false); //
const visible = ref(false); //
const members = computed(() => store.state.role.members); //
const isDisabled = ref(false); //
const morningStatus = ref(false); //
const nightStatus = ref(false); //
const checkerId = ref(null); // id
//
const formState = reactive({
timeRange: [], //
staffRange: ['a1', 'b2'], //
programName: ['a1', 'b2'], //
timeRange: [dayjs(+new Date().getTime()), dayjs(+new Date().getTime()).add(1, 'day')], //
staffRange: [], //
programName: [], //
});
//
const options = [...Array(25)].map((_, i) => ({ value: (i + 10).toString(36) + (i + 1) }));
const memberList = ref([]); //
const proList = ref([]); //
const emps = ref([]); //
const proDatas = ref([]); //
//
const columns = [
{
title: '时间',
width: 100,
dataIndex: 'time',
key: 'time',
fixed: 'left',
align: 'center',
},
{
title: '员工',
width: 100,
dataIndex: 'staff',
key: 'staff',
fixed: 'left',
align: 'center',
},
{
title: '项目1',
dataIndex: 'program',
key: '1',
width: 300,
align: 'center',
},
{
title: '项目2',
dataIndex: 'program',
key: '2',
width: 300,
align: 'center',
},
{
title: '项目3',
dataIndex: 'program',
key: '3',
width: 300,
align: 'center',
},
{
title: '项目4',
dataIndex: 'program',
key: '4',
width: 300,
align: 'center',
},
];
const columns = ref();
const current = ref(1); //
const pageSize = ref(10); //
const dataTotal = ref(0); //
//
const data = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i,
time: dayjs(+new Date().getTime()).format('MM-DD'),
staff: `成员 ${i}`,
program: `我今日计划结果1是XXXXXX,交付物是XXXXXXX方法论,截止时间是:4/25,检查人是:卫老师`,
});
}
const columnDatas = ref([]);
//
const modalFormState = ref([
{
planValue: '',
deliverable: '',
deadline: dayjs(+new Date().getTime()),
taskName: '',
deliverName: '',
showDeadLine: dayjs(+new Date().getTime()),
deadline: '',
showDuration: '2',
duration: '',
inspector: '',
address: '',
checker: '',
cooperation: 0,
deliverLink: '',
sequence: 0,
},
]);
//
const workDurations = [...Array(8)].map((_, i) => ({ value: `${i + 1}小时` }));
const workDurations = [...Array(8)].map((_, i) => ({ value: `${i + 1}` }));
const isSubmitDeliver = ref(false); //
onMounted(async () => {
getClockQuery();
await getInfo();
await getQueryTasks({});
});
//
async function getInfo() {
try {
const { url } = store.state.projects.project;
const data = await getBasicInfo(url);
memberList.value = data.emps;
pageSize.value = data.emps.length;
proList.value = data.pros;
} catch (error) {
throw new Error(error);
}
}
//
async function getQueryTasks(query) {
try {
const { url } = store.state.projects.project;
const params = {
param: {
startTime: query.startTime || formState.timeRange[0].startOf('day').valueOf(),
endTime: query.endTime || formState.timeRange[1].startOf('day').valueOf(),
emps: emps.value || [],
pros: proDatas.value || [],
},
};
//
const days = dayjs(+formState.timeRange[1].startOf('day')).diff(+formState.timeRange[0].startOf('day'), 'day');
dataTotal.value = pageSize.value * days; //
const data = await queryTasks(params, url);
columns.value = [
{
title: '时间',
width: 100,
dataIndex: 'time',
key: 'time',
fixed: 'left',
align: 'center',
},
{
title: '员工',
width: 100,
dataIndex: 'staff',
key: 'staff',
fixed: 'left',
align: 'center',
},
];
columnDatas.value = [];
data.pros.forEach((proId, index) => {
const proInfo = proList.value.find(item => item.id === proId);
columns.value.push({
title: proInfo.projectName,
dataIndex: 'program',
key: `${index}`,
proId,
width: 300,
align: 'center',
});
});
memberList.value.forEach((member, key) => {
const obj = {
empId: member.id,
key,
time: dayjs(+new Date().getTime()).format('MM-DD'),
staff: `${member.empName}`,
program: [],
};
const currMember = data.recs.filter(item => member.id === item.empId);
if (currMember.length > 0) {
const { pros } = currMember[0];
obj.program = pros;
}
columnDatas.value.push(obj);
});
} catch (error) {
throw new Error(error);
}
}
//
function handleSubmit() {
emps.value = [];
proDatas.value = [];
memberList.value.forEach(item => {
const index = formState.staffRange.findIndex(emp => emp === item.empName);
if (index > -1) emps.value.push(item.id);
});
proList.value.forEach(item => {
const index = formState.programName.findIndex(pro => pro === item.projectName);
if (index > -1) proDatas.value.push(item.id);
});
const endTime = dayjs(+formState.timeRange[0].startOf('day')).add(1, 'day');
getQueryTasks({ endTime: dayjs(+endTime).valueOf() });
}
//
function resetData() {
formState.timeRange = [dayjs(+new Date().getTime()), dayjs(+new Date().getTime()).add(1, 'day')];
formState.staffRange = [];
formState.programName = [];
emps.value = [];
proDatas.value = [];
}
//
function handlePage(e) {
const startTime = dayjs(+formState.timeRange[0].startOf('day')).add(e - 1, 'day');
const endTime = dayjs(+startTime).add(1, 'day');
getQueryTasks({ startTime: dayjs(+startTime).valueOf(), endTime: dayjs(+endTime).valueOf() });
}
//
function showModal() {
function showModal(data, proId) {
console.log('data', data, proId);
isDisabled.value = dayjs(+new Date().getTime()).format('MM-DD') !== data.time;
visible.value = true;
data.program.forEach(item => {
if (proId === item.proId) {
modalFormState.value = [...item.tasks];
}
});
modalFormState.value.forEach((item, index) => {
item.showDeadLine = dayjs(+item.deadline);
item.showDuration = `${item.duration / 3600000}`;
item.sequence = index;
});
}
//
function filterOption(input, option) {
return option.name.indexOf(input) >= 0;
return option.emp_name.indexOf(input) >= 0;
}
//
@ -245,28 +422,145 @@ function handleDuration(e, index) {
modalFormState.value[index].duration = e;
}
//
function handleInspector(e, index) {
modalFormState.value[index].inspector = e;
}
//
function addFormItem() {
const len = modalFormState.value.length;
modalFormState.value.push({
planValue: '',
deliverable: '',
deadline: dayjs(+new Date().getTime()),
taskName: '',
deliverName: '',
showDeadLine: dayjs(+new Date().getTime()),
deadline: '',
showDuration: '2',
duration: '',
inspector: '',
address: '',
checker: '',
cooperation: 0,
deliverLink: '',
sequence: len,
});
}
//
const delFormItem = index => {
modalFormState.value.splice(index, 1);
};
//
function cancleModal() {
visible.value = false;
}
function submitForm() {}
//
async function submitForm() {
let flag = false;
for (let i = 0; i < modalFormState.value.length; i++) {
const data = modalFormState.value[i];
data.deadline = dayjs(+data.showDeadLine).valueOf();
data.duration = data.showDuration * 3600000;
if (!data.taskName) {
message.info(`请输入今日计划结果${i + 1}`);
return false;
}
if (!data.deliverName) {
message.info(`请输入交付物`);
return false;
}
if (!data.duration) {
message.info(`请输入时长`);
return false;
}
if (!data.checker) {
message.info(`请选择检查人`);
return false;
}
const reg = /[a-zA-z]+:\/\/[^\s]*/;
if (data.deliverLink && !reg.test(data.deliverLink)) {
message.info(`请输入正确的链接`);
return false;
}
flag = true;
}
if (flag) {
isSubmitDeliver.value = true;
}
const params = {
param: {
time: new Date().getTime(),
empId: 1,
proId: 1,
tasks: modalFormState.value,
},
};
try {
const { url } = store.state.projects.project;
const data = await submitTask(params, url);
visible.value = false;
punch();
getQueryTasks();
} catch (error) {
throw new Error(error);
}
}
//
async function getClockQuery() {
const startTime = dayjs(+new Date().getTime()).valueOf();
const endTime = dayjs(+new Date().getTime())
.add(1, 'day')
.valueOf();
const params = { param: { projectId: projectId.value || sessionProjectId, roleId: roleId.value, memberIdList: [], startTime, endTime } };
try {
const { url } = store.state.projects.project;
const data = await clockQuery(params, url);
//
if (data[0].recordList[0].lastCheckerId) {
checkerId.value = data[0].recordList[0].lastCheckerId;
} else if (data[0].recordList[0].checkerId) {
checkerId.value = data[0].recordList[0].checkerId;
} else {
checkerId.value = members.value[0].memberId;
}
morningStatus.value = data[0].recordList[0].morningStatus > 0; //
nightStatus.value = data[0].recordList[0].nightStatus > 0; //
} catch (error) {
throw new Error(error);
}
}
async function punch() {
const clockType = isSubmitDeliver.value ? 1 : 0; // 1 0
const dateTime = dayjs(+new Date().getTime()).valueOf(); //
const params = { param: { clockType, dateTime } };
try {
const { url } = store.state.projects.project;
const data = await clockPunch(params, url);
morningStatus.value = data[0].recordList[0].morningStatus > 0; //
nightStatus.value = data[0].recordList[0].nightStatus > 0; //
} catch (error) {
throw new Error(error);
}
}
</script>
<style scoped>
.task-today::-webkit-scrollbar {
width: 0 !important;
}
</style>

28
src/views/home/Index.vue

@ -44,7 +44,7 @@ const store = useStore();
const collapsed = computed(() => store.state.layout.display.left); //
const projectInfo = computed(() => store.state.projects.project); //
const sessionProject = sessionStorage.getItem('project'); //
// const sessionProject = sessionStorage.getItem('project'); //
// const sessionTask = sessionStorage.getItem('taskDetail');
// const taskDetail = computed(() => store.state.task.taskDetail); //
@ -53,20 +53,20 @@ const targetUrl = sessionStorage.getItem('targetUrl'); // 缓存的详情页链
const taskDetailShow = computed(() => store.state.task.taskDetailShow); //
const sessionTaskDetailShow = sessionStorage.getItem('taskDetailShow'); //
if (sessionProject && !projectInfo.value.id) {
const info = JSON.parse(sessionProject);
console.log(`info`, info);
store.commit('projects/setProject', info);
store.commit('task/setBusinessCode', info && info.businessCode ? info.businessCode : '');
}
// if (sessionProject && !projectInfo.value.id) {
// const info = JSON.parse(sessionProject);
// console.log(`info`, info);
// store.commit('projects/setProject', info);
// store.commit('task/setBusinessCode', info && info.businessCode ? info.businessCode : '');
// }
if (projectInfo.value.id || sessionProject) {
const info = JSON.parse(sessionProject);
const projectId = projectInfo.value.id || info.id;
const url = projectInfo.value.url || info.url;
const param = { projectId };
store.dispatch('role/getAllMembers', { param, url });
}
// if (projectInfo.value.id || sessionProject) {
// const info = JSON.parse(sessionProject);
// const projectId = projectInfo.value.id || info.id;
// const url = projectInfo.value.url || info.url;
// const param = { projectId };
// store.dispatch('role/getAllMembers', { param, url });
// }
if (targetUrl && !taskDetailUrl.value) {
//

Loading…
Cancel
Save