add note , add lesson , add category , add subexam | 350

main
alireza khaji 2 years ago
parent 093924033b
commit d92199f6b0
  1. 51
      src/components/selectedCourse/index.js
  2. 16
      src/redux/actions/exam.js
  3. 1
      src/redux/actions/index.js
  4. 23
      src/redux/actions/note.js
  5. 2
      src/redux/reducers/exam.js
  6. 2
      src/redux/reducers/exam2.js
  7. 1
      src/redux/reducers/file.js
  8. 1
      src/redux/reducers/index.js
  9. 37
      src/redux/reducers/note.js
  10. 5
      src/view/addExam/components/addQuestionnaireCourse.js
  11. 306
      src/view/addExam/views/addQuestionnaireExam.js

@ -1,42 +1,65 @@
import React from "react"; import React from "react";
const SelectedCourse = ({ onClick, title, questionQuantity, coefficient, optionsQuantity }) => { const SelectedCourse = ({
onClick,
title,
questionQuantity,
coefficient,
optionsQuantity,
optionsNumber,
}) => {
return ( return (
<div className="w-full border border-gray-400 rounded-xl bg-white flex items-center justify-between px-6 py-8 mt-8"> <div className="w-full border border-gray-400 rounded-xl bg-white flex items-center justify-between px-6 py-8 mt-8">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<h1 className="w-full text-lg text-right text-blue3A font-bold"> <h1 className="w-full text-lg text-right text-blue3A font-bold">
{title} {title}
</h1> </h1>
<div className="w-full flex mt-4 justify-between"> <div className="w-full flex mt-4 justify-between">
<p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52"> <p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52">
<p>تعداد سوالات</p> <p>تعداد سوالات</p>
<input type='number' className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs" value={questionQuantity} /> <input
type="number"
className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs"
value={questionQuantity}
/>
</p> </p>
<p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52"> <p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52">
<p className="w-full text-center"> ضریب</p> <p className="w-full text-center"> ضریب</p>
<input type='number' className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs" value={coefficient} /> <input
type="number"
className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs"
value={coefficient}
/>
</p> </p>
<p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52"> <p className="flex items-center w-1/4 text-sm justify-between border-l-2 px-2 border-red52">
<p>تعداد گزینه</p> <p>تعداد گزینه</p>
<input type='number' className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs" value={4} /> <input
type="number"
className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center text-xs"
value={optionsNumber || 4}
/>
</p> </p>
<p className="flex items-center w-1/4 text-sm justify-between px-2 "> <p className="flex items-center w-1/4 text-sm justify-between px-2 ">
<p>نمره منفی</p> <p>نمره منفی</p>
<input type='number' className="w-5 h-5 ml-1 border border-red52 outline-none rounded text-center" /> <input
type="checkbox"
className="note-course-negetive-point-input w-5 h-5 ml-1 border border-red52 outline-none rounded text-center"
/>
</p> </p>
</div> </div>
<textarea className="border border-gray-300 mt-8 rounded-xl h-32 p-4 outline-none" placeholder="توضیحات"> <textarea
className="border border-gray-300 mt-8 rounded-xl h-32 p-4 outline-none"
</textarea> placeholder="توضیحات"
></textarea>
</div> </div>
<h1 className="w-40 text-center text-lg text-blue3A font-bold cursor-pointer" onClick={onClick}> <h1
className="w-40 text-center text-lg text-blue3A font-bold cursor-pointer"
onClick={onClick}
>
پاسخ برگ پاسخ برگ
</h1> </h1>
</div> </div>
) );
} };
export default SelectedCourse; export default SelectedCourse;

@ -117,6 +117,22 @@ const exam = {
}); });
}; };
}, },
addExamQuestionaires: (data = {}) => {
return (dispatch) => {
dispatch({
type: "ADD_EXAM_QUESTIONAIRES",
payload: data,
});
};
},
addQuestionnaireCourse: (data = {}) => {
return (dispatch) => {
dispatch({
type: "ADD_QUESTIONNAIRE_COURSES",
payload: data,
});
};
},
}; };
export default exam; export default exam;

@ -19,6 +19,7 @@ export { default as lesson } from "./lesson";
export { default as question } from "./question"; export { default as question } from "./question";
export { default as category } from "./category"; export { default as category } from "./category";
export { default as examQuestion } from "./examQuestion"; export { default as examQuestion } from "./examQuestion";
export { default as note } from "./note";

@ -0,0 +1,23 @@
import proxy from "../proxy";
const note = {
info:
(data = {}) =>
async (dispatch) =>
await proxy.get("note/info", data, { dispatch }),
list:
(data = {}) =>
async (dispatch) =>
await proxy.get("note/list", data, { dispatch }),
add:
(data = {}) =>
async (dispatch) =>
await proxy.post("note/add", data, { dispatch }),
delete:
(data = {}) =>
async (dispatch) =>
await proxy.delete("note/delete", data, { dispatch }),
};
export default note;

@ -225,7 +225,7 @@ export default function addExamReduser(
); );
thisQ = { thisQ = {
...thisQ, ...thisQ,
courses: [...thisQ.courses, payload], courses: thisQ?.courses?.length ? [...thisQ?.courses, payload] :[payload],
}; };
console.log(thisQ); console.log(thisQ);
return { return {

@ -9,6 +9,7 @@ const initialState = {
lastExamAdd: null, lastExamAdd: null,
preview: null, preview: null,
subExamList: [], subExamList: [],
lastSubExamAdd:null,
confirm:null confirm:null
}; };
export default function exam2(state = initialState, action) { export default function exam2(state = initialState, action) {
@ -30,6 +31,7 @@ export default function exam2(state = initialState, action) {
...state, ...state,
loading: false, loading: false,
error: null, error: null,
lastSubExamAdd:data.id,
subExamList: [...state.subExamList, data.id], subExamList: [...state.subExamList, data.id],
}; };
case "empty_subexam_list": case "empty_subexam_list":

@ -11,6 +11,7 @@ export default function file(state = initialState, action) {
let { type, data, params } = action; let { type, data, params } = action;
switch (type) { switch (type) {
case "file/upload": case "file/upload":
toast.success("آپلود با موفقیت انجام شد")
return { return {
...state, ...state,
loading: false, loading: false,

@ -19,5 +19,6 @@ export { default as lesson } from "./lesson";
export { default as question } from "./question"; export { default as question } from "./question";
export { default as category } from "./category"; export { default as category } from "./category";
export { default as examQuestion } from "./examQuestion"; export { default as examQuestion } from "./examQuestion";
export { default as note } from "./note";

@ -0,0 +1,37 @@
import { toast } from "react-toastify";
const initialState = {
loading: false,
error: null,
info: null,
list: null,
lastAdd: null,
myNotes:[]
};
export default function note(state = initialState, action) {
let { type, data } = action;
switch (type) {
case "note/info":
return { ...state, loading: false, info: data, error: null };
case "note/add":
toast.success("درخواست با موفقیت انجام شد");
return {
...state,
loading: false,
error: null,
lastAdd: data.id,
myNotes:[...state.myNotes,data.id]
};
case "note/list":
return { ...state, loading: false, error: null, list: data };
case "note/loading":
return { ...state, loading: true };
case "note/error":
toast.error(data.message);
return { ...state, loading: false, error: data.message };
default:
return state;
}
}

@ -7,6 +7,7 @@ const AddQuestionnaireCourse = ({
questionEnd, questionEnd,
coefficientOnChange, coefficientOnChange,
closeOnClick, closeOnClick,
optionsNumber,
}) => { }) => {
return ( return (
<div className="w-full z-10 fixed h-screen backdrop-blur flex items-center -mt-10 justify-center"> <div className="w-full z-10 fixed h-screen backdrop-blur flex items-center -mt-10 justify-center">
@ -77,13 +78,13 @@ const AddQuestionnaireCourse = ({
type="number" type="number"
placeholder="ضریب ..." placeholder="ضریب ..."
className="w-4/5 p-4 ml-2 rounded border border-red52 h-12 text-red26 outline-none" className="w-4/5 p-4 ml-2 rounded border border-red52 h-12 text-red26 outline-none"
onChange={coefficientOnChange} onChange={(e) => coefficientOnChange(e)}
/> />
<input <input
type="number" type="number"
placeholder="تعداد گزینه" placeholder="تعداد گزینه"
className="w-4/5 p-4 ml-2 rounded border border-red52 h-12 text-red26 outline-none" className="w-4/5 p-4 ml-2 rounded border border-red52 h-12 text-red26 outline-none"
// onChange={} onChange={(e) => optionsNumber(e)}
/> />
</div> </div>
<div className="flex w-full justify-end pt-4"> <div className="flex w-full justify-end pt-4">

@ -11,13 +11,34 @@ import { DragDropContext } from "react-beautiful-dnd";
import { Droppable } from "react-beautiful-dnd"; import { Droppable } from "react-beautiful-dnd";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
import { toMap } from "draft-js/lib/DefaultDraftBlockRenderMap"; import { toMap } from "draft-js/lib/DefaultDraftBlockRenderMap";
import { category, exam, file, lesson, note } from "../../../redux/actions";
import loadingGiff from "../../../assets/img/Rolling-2s-223px.gif";
let lastCourseIndex;
const AddQuestionnaireExam = ({ const AddQuestionnaireExam = ({
grades, grades,
addExamState, addExamState,
dispatch, dispatch,
stepClick, stepClick,
backStep, backStep,
uploadNote,
lastUpload,
uploadLoading,
addExamQuestionaires,
addNoteLoading,
myNoteList,
addNote,
examId,
addLesson,
lastlessonAdd,
addCategory,
lastCategoryAdd,
addQuestionnaireCourse,
subexamList,
subexamAdd,
lastSubExamAdd,
}) => { }) => {
const [questionarePopUp, setQuestionairePopUp] = useState(false); const [questionarePopUp, setQuestionairePopUp] = useState(false);
const [addCoursePopup, setAddCoursePopup] = useState(false); const [addCoursePopup, setAddCoursePopup] = useState(false);
@ -33,28 +54,31 @@ const AddQuestionnaireExam = ({
courses: [], courses: [],
}); });
const [answers, setAnswers] = useState(null); const [answers, setAnswers] = useState(null);
const [newCourse, setNewCourse] = useState({ const [newCourse, setNewCourse] = useState(null);
id: selectedQuestionare.courses.length + selectedQuestionare.id + 1, // id: selectedQuestionare?.courses?.length + selectedQuestionare?.id + 1,
questionnaireId: selectedQuestionare.id, // questionnaireId: selectedQuestionare?.id,
questionsQuantity: 20, // questionsQuantity: 20,
name: "", // name: "",
coefficient: 4, // coefficient: 4,
}); const [selectedCourse, setSelectedCourse] = useState(null);
const [selectedCourse, setSelectedCourse] = useState({ const [subexamArray,setSubexamArray]=useState([]);
sortId: 1, // {
title: "ادبیات فارسی", // sortId: 1,
coefficient: 3, // title: "ادبیات فارسی",
questionStart: 1, // coefficient: 3,
questionEnd: 0, // questionStart: 1,
}); // questionEnd: 0,
// }
// console.log(choosedQuestionaries) // console.log(choosedQuestionaries)
// console.log(newQuestionaire) // console.log(newQuestionaire)
// console.log(addExamState.questionnaires) // console.log(addExamState.questionnaires)
console.log(newCourse); // console.log(newCourse);
console.log(selectedQuestionare); // console.log(selectedQuestionare);
console.log(selectedCourse.id); // console.log(selectedCourse?.id);
console.log(answers); // console.log(answers);
const [noteSelectedId, setNoteSelectedId] = useState(null);
const [NoteList, setNoteList] = useState([]);
// const [noteAndCategory]
const [fakeState, setFakeState] = useState(2); const [fakeState, setFakeState] = useState(2);
useEffect(() => { useEffect(() => {
setSelectedCourse(selectedCourse); setSelectedCourse(selectedCourse);
@ -68,13 +92,148 @@ const AddQuestionnaireExam = ({
} }
}, [answers]); }, [answers]);
const [noteFile,setNoteFile]=useState(null); //add note process =================================================================
const noteChooseFile=()=>{ console.log("=======myNoteList===================");
console.log(myNoteList);
const [noteFile, setNoteFile] = useState(null);
const noteChooseFile = () => {
let input = document.querySelector("#add-note-input-file"); let input = document.querySelector("#add-note-input-file");
input.click(); input.click();
setNoteFile(input); // setNoteFile(input);
setFakeState(fakeState + 15); // setFakeState(Math.random()*1000);
};
const noteChangeFile = (e) => {
console.log("{{{}}}");
console.log(e.target.value);
setNoteFile(e.target.value);
};
const uploadNoteHandler = () => {
let input = document.querySelector("#add-note-input-file");
uploadNote({
file: input.files[0],
});
};
const submitNoteHandler = () => {
// dispatch({
// type: "",
// payload: newQuestionaire,
// });
addExamQuestionaires(newQuestionaire);
setQuestionairePopUp(false);
addNote({
name: newQuestionaire.name,
fileId: lastUpload,
examId: examId,
});
};
// useEffect(() => {
// setNewQuestionaire({
// ...newQuestionaire,
// id: lastNoteadd,
// });
// }, [lastNoteadd]);
//add lesson and category process =================================================================
// console.log("========selectedQuestionare=========");
// console.log(addExamState.questionnaires);
// console.log("========selectedQuestionare=========");
// console.log(addExamState.questionnaires);
console.log("========NoteList=========");
console.log(NoteList);
console.log("========subexamArray=========");
console.log(subexamArray);
const submitLessonhandler = () => {
addQuestionnaireCourse(newCourse);
setAddCoursePopup(false);
setFakeState(fakeState + 356);
addLesson({
name: newCourse.name,
});
};
useEffect(() => {
if (lastlessonAdd) {
addCategory({
title: newCourse.name,
lessonId: lastlessonAdd,
gradeId: addExamState.grade,
});
}
}, [lastlessonAdd]);
useEffect(() => {
if (lastCategoryAdd) {
setNoteList([
...NoteList,
{
noteId: noteSelectedId,
categoryId: lastCategoryAdd,
name: newCourse.name,
},
]);
}
}, [lastCategoryAdd]);
//===========================================================
const submitSubexam = (item, index) => {
lastCourseIndex = index;
setSelectedCourse(item);
console.log("a...............................a");
console.log(selectedCourse);
let negetiveInput = document.querySelectorAll(
".note-course-negetive-point-input"
);
let categoryId;
for (let i = 0; i < NoteList.length; i++) {
if (
noteSelectedId === NoteList[i].noteId &&
item?.name === NoteList[i].name
) {
categoryId = NoteList[i].categoryId;
break;
}
}
console.log({
noteId: noteSelectedId,
name: item?.name,
description: " ",
categoryId,
coefficient: item?.coefficient,
negativePoint: negetiveInput[index].checked,
questionType: item.optionsNumber || 2,
questionCount: Number(item.questionEnd) - Number(item.questionStart) + 1,
});
subexamAdd({
pid: examId,
ord:index+1,
noteId: noteSelectedId,
name: item?.name,
description: " ",
categoryId,
coefficient: item?.coefficient,
negativePoint: negetiveInput[index].checked,
questionType: item.optionsNumber || 2,
questionCount: Number(item.questionEnd) - Number(item.questionStart) + 1,
});
};
useEffect(() => {
if (lastSubExamAdd){
setSubexamArray([
...subexamArray,
{
index: lastCourseIndex,
subexamId: lastSubExamAdd,
noteId: noteSelectedId,
},
]);
} }
}, [lastSubExamAdd]);
return ( return (
<> <>
@ -143,31 +302,43 @@ const AddQuestionnaireExam = ({
انتخاب فایل انتخاب فایل
</div> </div>
<div className="w-[20%] h-12 rounded border border-red53 flex items-center ltr text-xs px-2 overflow-hidden text-left text-ellipsis whitespace-normal"> <div className="w-[20%] h-12 rounded border border-red53 flex items-center ltr text-xs px-2 overflow-hidden text-left text-ellipsis whitespace-normal">
{noteFile!==null ? noteFile?.value : "فایلی اضافه نشده"} {noteFile !== null ? noteFile : "فایلی اضافه نشده"}
</div> </div>
<div className="w-[10%] h-12 flex items-center justify-center text-sm rounded bg-[#111] text-white cursor-pointer"> {!uploadLoading && (
<div
className="w-[10%] h-12 flex items-center justify-center text-sm rounded bg-[#111] text-white cursor-pointer"
onClick={uploadNoteHandler}
>
آپلود آپلود
</div> </div>
)}
{uploadLoading && (
<div className="w-[10%] h-12 flex items-center justify-center text-sm rounded bg-[#111] text-white cursor-pointer">
<img src={loadingGiff} alt="loadingGiff" className="h-9" />
</div>
)}
<input <input
type="file" type="file"
placeholder="آپلود فایل دفترچه" placeholder="آپلود فایل دفترچه"
className="bg-white items-center flex h-12 text-red26 outline-none hidden" className="bg-white items-center flex h-12 text-red26 outline-none hidden"
id="add-note-input-file" id="add-note-input-file"
onChange={(e) => noteChangeFile(e)}
/> />
</div> </div>
<div className="flex w-full justify-end pt-2"> <div className="flex w-full justify-end pt-2">
{!addNoteLoading && (
<button <button
className="px-6 py-2 rounded-md bg-red52 text-white font-bold" className="px-6 py-2 rounded-md bg-red52 text-white font-bold"
onClick={() => { onClick={submitNoteHandler}
dispatch({
type: "ADD_EXAM_QUESTIONAIRES",
payload: newQuestionaire,
});
setQuestionairePopUp(false);
}}
> >
تایید تایید
</button> </button>
)}
{addNoteLoading && (
<div className="w-16 h-10 flex items-center justify-center rounded-md bg-red52 text-white font-bold">
<img src={loadingGiff} alt="loadingGiff" className="h-8" />
</div>
)}
</div> </div>
{/* { {/* {
@ -235,9 +406,12 @@ const AddQuestionnaireExam = ({
nameOnChange={(e) => nameOnChange={(e) =>
setNewCourse({ ...newCourse, name: e.target.value }) setNewCourse({ ...newCourse, name: e.target.value })
} }
coefficientOnChange={(e) => coefficientOnChange={(e) => {
setNewCourse({ ...newCourse, coefficient: e.target.vlaue }) setNewCourse({ ...newCourse, coefficient: e.target.value });
} }}
optionsNumber={(e) => {
setNewCourse({ ...newCourse, optionsNumber: e.target.value });
}}
questionEnd={(e) => questionEnd={(e) =>
setNewCourse({ setNewCourse({
...newCourse, ...newCourse,
@ -250,14 +424,10 @@ const AddQuestionnaireExam = ({
...newCourse, ...newCourse,
questionStart: e.target.value, questionStart: e.target.value,
questionnaireId: selectedQuestionare.id, questionnaireId: selectedQuestionare.id,
id: selectedQuestionare.id + selectedCourse.id + 1, id: selectedQuestionare?.id + (selectedCourse?.id || 0) + 1,
}) })
} }
submitOnClick={() => { submitOnClick={submitLessonhandler}
dispatch({ type: "ADD_QUESTIONNAIRE_COURSES", payload: newCourse });
setAddCoursePopup(false);
setFakeState(fakeState + 356);
}}
/> />
)} )}
{/* ==============================top-part============================= */} {/* ==============================top-part============================= */}
@ -314,13 +484,16 @@ const AddQuestionnaireExam = ({
? "bg-pinkF5 border border-red52 text-red52" ? "bg-pinkF5 border border-red52 text-red52"
: "bg-white border border-gray-300 text-blue3A" : "bg-white border border-gray-300 text-blue3A"
} items-center justify-center w-36 h-32 mx-2 font-bold rounded-xl cursor-pointer`} } items-center justify-center w-36 h-32 mx-2 font-bold rounded-xl cursor-pointer`}
onClick={() => onClick={() => {
console.log("note click");
console.log(item);
setNoteSelectedId(myNoteList[index]);
setSelectedQuestionare({ setSelectedQuestionare({
id: item.id, id: item.id,
name: item.name, name: item.name,
courses: item.courses, courses: item.courses,
}) });
} }}
> >
<p className="text-red52">دفترچه {item.name}</p> <p className="text-red52">دفترچه {item.name}</p>
</div> </div>
@ -353,8 +526,9 @@ const AddQuestionnaireExam = ({
</div> </div>
</div> </div>
</div> </div>
{/* ====================================================================== */} {/* ================================bottom-part====================================== */}
<div className="w-full flex px-10"> <div className="w-full flex px-10">
{/* ==============================b-right-part============================= */}
<div className="w-1/2 border-l flex flex-col border-gray-300 py-10"> <div className="w-1/2 border-l flex flex-col border-gray-300 py-10">
{selectedQuestionare.id !== null && ( {selectedQuestionare.id !== null && (
<> <>
@ -368,17 +542,17 @@ const AddQuestionnaireExam = ({
<div className="w-4/5 "> <div className="w-4/5 ">
{addExamState.questionnaires {addExamState.questionnaires
.find((itm) => itm.id == selectedQuestionare.id) .find((itm) => itm.id == selectedQuestionare.id)
?.courses.map((item, index) => ( ?.courses?.map((item, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<SelectedCourse <SelectedCourse
title={item.name} title={item.name}
coefficient={item.coefficient} coefficient={item.coefficient}
optionsNumber={item.optionsNumber}
questionQuantity={ questionQuantity={
item.questionEnd - item.questionStart item.questionEnd - item.questionStart + 1
} }
onClick={() => { onClick={() => {
setSelectedCourse(item); submitSubexam(item, index);
console.log(selectedCourse);
}} }}
/> />
</React.Fragment> </React.Fragment>
@ -398,13 +572,14 @@ const AddQuestionnaireExam = ({
</div> </div>
)} )}
</div> </div>
{/* ==============================b-left-part============================= */}
<div className="w-1/2 flex justify-center"> <div className="w-1/2 flex justify-center">
<div className="w-4/5 py-8"> <div className="w-4/5 py-8">
<AnswerSheet <AnswerSheet
selectedCourseId={selectedCourse.id} selectedCourseId={selectedCourse?.id}
selectedQId={selectedQuestionare.id} selectedQId={selectedQuestionare?.id}
questionEnd={selectedCourse.questionEnd} questionEnd={selectedCourse?.questionEnd}
questionStart={selectedCourse.questionStart} questionStart={selectedCourse?.questionStart}
passAnswers={(data) => passAnswers={(data) =>
dispatch({ type: "ADD_ANSWERS", payload: data }) dispatch({ type: "ADD_ANSWERS", payload: data })
} }
@ -436,6 +611,29 @@ const AddQuestionnaireExam = ({
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
grades: state.publicApi.grades, grades: state.publicApi.grades,
addExamState: state.addExamReduser, addExamState: state.addExamReduser,
lastUpload: state.file.lastUpload,
uploadLoading: state.file.loading,
addNoteLoading: state.note.loading,
myNoteList: state.note.myNotes,
lastNoteadd: state.note.lastAdd,
examId: state.exam2.lastExamAdd,
lastlessonAdd: state.lesson.lastAdd,
lastCategoryAdd: state.category.lastAdd,
subexamList: state.exam2.subExamList,
lastSubExamAdd: state.exam2.lastSubExamAdd,
}); });
export default connect(mapStateToProps)(AddQuestionnaireExam); const mapDispatchToProps = {
uploadNote: file.upload,
addNote: note.add,
addExamQuestionaires: exam.addExamQuestionaires,
addLesson: lesson.add,
addCategory: category.add,
addQuestionnaireCourse: exam.addQuestionnaireCourse,
subexamAdd: exam.subexamAdd,
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddQuestionnaireExam);

Loading…
Cancel
Save