diff --git a/webApp/__init__.py b/webApp/__init__.py index 360e2b8..5c85d10 100644 --- a/webApp/__init__.py +++ b/webApp/__init__.py @@ -12,15 +12,15 @@ webApp = Flask(__name__, instance_relative_config=True, template_folder=config[' __version__ = '1.0.1.0' -webApp.config['SECRET_KEY'] = '2qwtq2' -webApp.config['INTERNAL_API_KEY'] = 'lGR9g09RW5cE07rKivRNIH9f' -webApp.config['REMOTE_LOGIN_KEY'] = 'oRTiLTHeNTraZEsI' +webApp.config['SECRET_KEY'] = '2qwtsdfsdfsdfsdfq2' +webApp.config['INTERNAL_API_KEY'] = 'lGR9g09RW5cEsdfsfsdfsdf07rKivRNIH9f' +webApp.config['REMOTE_LOGIN_KEY'] = 'sdfsdfsdfsdf' webApp.config['SESSION_COOKIE_SAMESITE'] = "None" webApp.config['SESSION_COOKIE_SECURE'] = True webApp.jinja_env.add_extension('pypugjs.ext.jinja.PyPugJSExtension') -logging.basicConfig(level = logging.INFO) +# logging.basicConfig(level = logging.INFO) from webApp.helper import jinjaHelper from webApp.interfaces import * diff --git a/webApp/helper/jinjaHelper.py b/webApp/helper/jinjaHelper.py index 6fe9053..264dedd 100644 --- a/webApp/helper/jinjaHelper.py +++ b/webApp/helper/jinjaHelper.py @@ -14,4 +14,5 @@ def example(): else: return config.config['WEB_APP']['FRONTEND_VERSION'] + return dict(randomString = randomString) \ No newline at end of file diff --git a/webApp/interfaces/dependencies.py b/webApp/interfaces/dependencies.py index 28a75e4..38a935e 100644 --- a/webApp/interfaces/dependencies.py +++ b/webApp/interfaces/dependencies.py @@ -13,4 +13,4 @@ def send_css(path): def send_js(path): resp = make_response(send_from_directory(os.path.join(config['WEB_APP']['template_folder'], 'static/js'), path), 200) resp.headers['Cache-Control'] = 'public, max-age=31536000' - return resp + return resp \ No newline at end of file diff --git a/webApp/interfaces/pagesController.py b/webApp/interfaces/pagesController.py index 48e682f..a7d11e3 100644 --- a/webApp/interfaces/pagesController.py +++ b/webApp/interfaces/pagesController.py @@ -18,6 +18,15 @@ def authorize_google(): client = gspread.authorize(creds) return client +# Функция проверки существования пользователя в таблице +def user_exists(sheet, fio, tel): #, user_id): + records = sheet.get_all_values() # Получаем все данные таблицы без заголовков + + for row in records[1:]: # Пропускаем первую строку, если там заголовки, или уберите [1:], если заголовков нет + if len(row) > 1 and row[0] == fio and row[1] == tel:# and row[13] == user_id: + return True # Если ФИО, телефон и user_id совпадают, значит, запись уже существует + return False + # Обработка формы @webApp.route('/form_submit', methods=['POST']) @@ -25,20 +34,25 @@ def form_submit(): data = json.loads(request.data) # {'fio': '', 'tel': '', 'email': '', 'passport': '', 'passport_date': '', 'postal_code': '', 'address': '', 'snils': '', 'inn': '', 'dob': ''} data['current_time'] = datetime.datetime.now().strftime("%d.%m.%Y, %H:%M") - print(data) # Авторизация и добавление данных в Google Sheets sheet = authorize_google().open("Информация о сотрудниках").sheet1 + + # Проверка, существует ли уже пользователь + if user_exists(sheet, data['fio'], data['tel']): #, data['user_id']): + return jsonify({'success': False, 'error': 'Ваши данные уже присутствуют в таблице'}), 400 + sheet.append_row([ data['fio'], data['tel'], data['email'], - data['passport'][:4], - data['passport'][4:], + data['passport'][:5], + data['passport'][5:], data['passport_issued_by'], data['passport_date'], data['postal_code'], data['address'], + data['residential_address'], data['snils'], data['inn'], data['dob'], diff --git a/webApp/templates/2.0/main_page.pug b/webApp/templates/2.0/main_page.pug index f20044e..4256d3f 100644 --- a/webApp/templates/2.0/main_page.pug +++ b/webApp/templates/2.0/main_page.pug @@ -1,8 +1,8 @@ include 2.0/application - -link(rel="stylesheet" href="/css/1_model_app.css") -link(rel="stylesheet" href="/css/1_model_icons.css") +link(rel="stylesheet" href="/css/1_model_app.css?q="~randomString()) +link(rel="stylesheet" href="/css/1_model_icons.css?q="~randomString()) +script(type="text/javascript", src="/js/UFMS.js?q="~randomString()) script(type="text/javascript", src="/js/2.0.dashboard.js?q="~randomString()) @@ -13,39 +13,61 @@ script(type="text/javascript", src="/js/2.0.dashboard.js?q="~randomString()) .card-body.px-0.pb-0 h5.mx-4.fw-bold="Заполните форму" input.tg_input#user_id(hidden, value=user_id) + + + + //--.my-3.mx-4 + .alert-placeholder.alert-lg.text-warning(data-content='У данного пользователя отсутсвуют права для направления пациентов') + input.form-control.form-control.custom-input#form_name(type="text") + + + .my-3.mx-4 label.form-label.m-0(for="fio")='ФИО' input.form-control.tg_input#fio - .my-3.mx-4 - label.form-label.m-0(for="tel")='Телефон' - input.form-control.tg_input#tel - .my-3.mx-4 + .my-3.mx-4.success-field + label.form-label.m-0(for="tel")="Телефон" + input.form-control.tg_input#tel(type="text") + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field + label.form-label.m-0(for="dob")='Дата рождения' + input.form-control.tg_input#dob(type="date") + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field label.form-label.m-0(for="email")='Электронная почта' input.form-control.tg_input#email - .my-3.mx-4 + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field label.form-label.m-0(for="passport")='Серия и номер паспорта' input.form-control.tg_input#passport - .my-3.mx-4 + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field label.form-label.m-0(for="passport_date")='Дата выдачи паспорта' input.form-control.tg_input#passport_date(type="date") - .my-3.mx-4 - label.form-label.m-0(for="postal_code")='Кем выдан' - input.form-control.tg_input#passport_issued_by - .my-3.mx-4 + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field label.form-label.m-0(for="postal_code")='Код подразделения' input.form-control.tg_input#postal_code + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4 + label.form-label.m-0(for="passport_issued_by")='Кем выдан паспорт' + select.form-select.tg_input#passport_issued_by(style="") + option(disabled, selected)="Кем выдан паспорт" .my-3.mx-4 - label.form-label.m-0(for="address")='Место жительства' + label.form-label.m-0(for="address")='Адрес регистрации' input.form-control.tg_input#address .my-3.mx-4 + label.form-label.m-0(for="residential_address")='Адрес проживания' + input.form-control.tg_input#residential_address + .my-3.mx-4.success-field label.form-label.m-0(for="snils")='СНИЛС' input.form-control.tg_input#snils - .my-3.mx-4 + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4.success-field label.form-label.m-0(for="inn")='ИНН' input.form-control.tg_input#inn - .my-3.mx-4 - label.form-label.m-0(for="dob")='Дата рождения' - input.form-control.tg_input#dob(type="date") + small="(Пожалуйста, проверьте формат ввода)" + .my-3.mx-4 button.btn.btn-success.float-end.mb-3#form_submit span.spinner-border.spinner-border-sm.me-1.d-none(role="status" aria-hidden="true") diff --git a/webApp/templates/static/css/1_model_app.css b/webApp/templates/static/css/1_model_app.css index 1210daa..dcbda30 100644 --- a/webApp/templates/static/css/1_model_app.css +++ b/webApp/templates/static/css/1_model_app.css @@ -666,4 +666,79 @@ PLUS DROPDOWN .hotkey-card:focus{ border: 1px solid blue !important; +} + +.success-field input{ + /*border: 1px solid #000;*/ +} + +.errors-field input{ + border: 1px solid #F00; +} + +.success-field small{ + display: none; +} + +.errors-field small{ + color: #F00; + margin-left: 10px; + font-size: 0.7em; + font-style: italic; + display: block; +} + + +#passport-error { + display: none; /* Скрыть по умолчанию */ + color: #F00; /* Красный цвет для ошибки */ +} + +select { + border: 1px solid #838383; + border-style: outset; + font-weight: bold; + color: #3F3F3F; + max-width: 100% !important; + overflow: hidden; +} + +option { + max-width: 120px !important; + overflow: hidden; +} + + +.alert-placeholder +{ + position: relative; + color: attr(data-color) !important; + font-size: 20px; + white-space: nowrap; +} +.alert-lg:before{ + top: 4px; +} +.alert-md:before{ + top: 4px; +} +.alert-sm:before{ + top: 1px; +} +.alert-placeholder:before +{ + position: absolute; + right: 15px; + font-family: 'Font Awesome\ 5 Free'; + content: "\f071"; + font-weight: 900; + pointer-events: none; + opacity: 1; + overflow: hidden; + text-overflow: clip; +} +.alert-popover{ + top: 0px !important; + left: calc(50% - 124px) !important; + width: 250px !important; } \ No newline at end of file diff --git a/webApp/templates/static/css/1_model_icons.css b/webApp/templates/static/css/1_model_icons.css index d3ad136..50f1871 100644 --- a/webApp/templates/static/css/1_model_icons.css +++ b/webApp/templates/static/css/1_model_icons.css @@ -7,6 +7,14 @@ margin-right: 3px; } +.warning-icn::before { + font-family: 'Font Awesome\ 5 Free'; + content: "\f071"; + font-weight: 900; + margin-right: 3px; + color: orange; +} + .m-off::before{ margin: 0px !important; } \ No newline at end of file diff --git a/webApp/templates/static/js/2.0.dashboard.js b/webApp/templates/static/js/2.0.dashboard.js index 8e617e3..2b9bafe 100644 --- a/webApp/templates/static/js/2.0.dashboard.js +++ b/webApp/templates/static/js/2.0.dashboard.js @@ -20,10 +20,223 @@ function validate_length(val, dest_len){ } $(document).ready(function(){ + + + const validateEmail = (email) => { + return email.match( + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); + }; + + const validate = () => { + const email = $('#email').val(); + if(validateEmail(email)){ + $('#email').parent().removeClass('errors-field').addClass('success-field'); + } else{ + $('#email').parent().removeClass('success-field').addClass('errors-field'); + } + return false; + } + + $('#email').on('input', validate); + + $('body').append('
'); $('body').append(' '); - $('#fio').on("input", function(){ - console.log($('#fio').val()); + $('#fio').on('input', function() { + let input = $(this).val(); + input = input.replace(/[^a-zA-Zа-яА-ЯёЁ ]/g, ''); + // Разделение текста по пробелам и преобразование каждого слова + let words = input.split(/\s+/); // Разбиваем по пробелам + // Преобразуем каждое слово + let formattedInput = words.map(word => { + if (word.length > 0) { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + return ''; // Если слово пустое (например, несколько пробелов), не изменяем + }).join(' '); // Объединяем обратно с пробелами + // Обновляем поле ввода с отформатированным текстом + $(this).val(formattedInput); + }); + + $('#email').on('input', function() { + let input = $(this).val(); + // Разрешаем только буквы, цифры, точки, дефисы и подчеркивания + input = input.replace(/[^a-zA-Z0-9._@-]/g, ''); + // Обновляем поле ввода + $(this).val(input); + }); + + $('#tel').on('change', function() { + if($(this).val().length != 16){ + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else{ + $(this).parent().removeClass('errors-field').addClass('success-field'); + } + }); + + // Проверка на дату рождения (не старше 80 лет) + $('#dob').on('change', function() { + const dob = new Date($(this).val()); + const currentDate = new Date(); + const age = currentDate.getFullYear() - dob.getFullYear(); + + // Проверка на возраст (не старше 80 лет) + if (age > 80 || (age === 80 && (currentDate.getMonth() < dob.getMonth() || (currentDate.getMonth() === dob.getMonth() && currentDate.getDate() < dob.getDate())))) { + // Если возраст больше 80 лет, подсвечиваем ячейку как ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else { + // Если возраст нормален, подсвечиваем ячейку как успешную + $(this).parent().removeClass('errors-field').addClass('success-field'); + } + }); + + // Проверка на дату выдачи паспорта (выдан после 14, 20 или 45 лет) + $('#passport_date').on('change', function() { + const passportDate = new Date($(this).val()); + const dob = new Date($('#dob').val()); + + // Проверка, что дата рождения введена и корректна + if (!dob || isNaN(dob)) { + // Если дата рождения не введена или неверна, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + return; + } + + const ageAtPassportIssue = passportDate.getFullYear() - dob.getFullYear(); + + // Проверка на возраст, в котором может быть выдан паспорт + if (ageAtPassportIssue === 14 || ageAtPassportIssue === 20 || ageAtPassportIssue === 45) { + // Если возраст при выдаче паспорта соответствует требованиям, подсвечиваем ячейку как успешную + $(this).parent().removeClass('errors-field').addClass('success-field'); + } else { + // Если возраст не соответствует, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } + }); + + $("#passport").attr('maxlength','12'); + + $('#passport').on('input', function() { + let input = $(this).val(); + // Удаляем все символы, кроме цифр и пробелов + input = input.replace(/\D/g, ''); + + // Форматируем ввод в "00 00 000000" с пробелами + if (input.length > 2 && input.length <= 4) { + input = input.replace(/(\d{2})(\d{0,2})/, '$1 $2'); + } else if (input.length > 4 && input.length <= 10) { + input = input.replace(/(\d{2})(\d{2})(\d{0,6})/, '$1 $2 $3'); + } + + // Ограничиваем длину до "00 00 000000" + if (input.length > 12) { + input = input.slice(0, 12); + } + + // Обновляем поле ввода + $(this).val(input); + + // Проверка на минимальную длину (не менее 11 символов) + if (input.length < 12) { + // Если введено меньше цифр, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else { + // Если введено достаточно цифр, показываем успех + $(this).parent().removeClass('errors-field').addClass('success-field'); + } + }); + + + $('#postal_code').on('input', function() { + let input = $(this).val(); + // Удаляем все символы, кроме цифр + input = input.replace(/\D/g, ''); + // Форматируем ввод в "000-000" только при наличии 4 и более цифр + if (input.length > 3) { + input = input.replace(/(\d{3})(\d{0,3})/, '$1-$2'); + } + // Ограничиваем длину до "000-000" + if (input.length > 7) { + input = input.slice(0, 7); + } + // Обновляем поле ввода + $(this).val(input); + + // Проверка на минимальную длину (не менее 11 символов) + if (input.length < 7) { + // Если введено меньше цифр, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else { + // Если введено достаточно цифр, показываем успех + $(this).parent().removeClass('errors-field').addClass('success-field'); + } + }); + + $('#passport_issued_by').select2({ + theme: 'bootstrap-5' + }); + + $("#postal_code").change(function(){ + $('#passport_issued_by').empty(); + $('#passport_issued_by').append('') + for(i in UFMS_list[$(this).val()]) + { + item = UFMS_list[$(this).val()][i]; + $('#passport_issued_by').append(''); + } +// $('#passport_issued_by').select2("change"); + }); + + + $('#snils').on('input', function() { + let input = $(this).val(); + // Удаляем все символы, кроме цифр + input = input.replace(/\D/g, ''); + + // Форматируем ввод в "000-000-000-00" только после 3, 6, 9 цифр + if (input.length > 3 && input.length <= 6) { + input = input.replace(/(\d{3})(\d{0,3})/, '$1-$2'); + } else if (input.length > 6 && input.length <= 9) { + input = input.replace(/(\d{3})(\d{3})(\d{0,3})/, '$1-$2-$3'); + } else if (input.length > 9) { + input = input.replace(/(\d{3})(\d{3})(\d{3})(\d{0,2})/, '$1-$2-$3-$4'); + } + + // Ограничиваем длину до "000-000-000-00" + if (input.length > 14) { + input = input.slice(0, 14); + } + + // Обновляем поле ввода + $(this).val(input); + if (input.length < 14) { + // Если введено меньше цифр, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else { + // Если введено достаточно цифр, показываем успех + $(this).parent().removeClass('errors-field').addClass('success-field'); + } + }); + + $("#inn").attr('maxlength','12'); + $('#inn').on('input', function() { + let input = $(this).val(); + // Убираем все символы, кроме цифр + input = input.replace(/\D/g, ''); + // Ограничиваем длину до 12 символов + if (input.length > 12) { + input = input.slice(0, 12); + } + // Обновляем значение в поле ввода + $(this).val(input); + if (input.length < 12) { + // Если введено меньше цифр, показываем ошибку + $(this).parent().removeClass('success-field').addClass('errors-field'); + } else { + // Если введено достаточно цифр, показываем успех + $(this).parent().removeClass('errors-field').addClass('success-field'); + } }); $("#form_submit").click(function(){ @@ -31,7 +244,6 @@ $(document).ready(function(){ $(this).attr("disabled", true); message = {} - if(!(validate_length($('#fio').val(), 3))) { show_error('Введите ФИО', false); @@ -40,10 +252,96 @@ $(document).ready(function(){ return; } + if(!(validate_length($('#tel').val(), 16))) + { + show_error('Введите Телефон', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if ($('#dob').val().length !== 10 || new Date().getFullYear() - new Date($('#dob').val()).getFullYear() > 80) { + show_error('Дата рождения должна быть корректной и возраст не более 80 лет', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validateEmail($('#email').val()))){ + show_error('Введите корректный Email', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#passport').val(), 12))) + { + show_error('Введите серию и номер паспорта', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if ($('#passport_date').val().length !== 10 || ![14, 20, 45].includes(new Date($('#passport_date').val()).getFullYear() - new Date($('#dob').val()).getFullYear())) { + show_error('Дата выдачи паспорта должна быть в 14, 20 или 45 лет', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#postal_code').val(), 7))) + { + show_error('Введите код подразделения', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if($('#passport_issued_by').val() == null){ + show_error("Выберите кем выдан паспорт"); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#address').val(), 3))) + { + show_error('Введите адрес регистрации', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#residential_address').val(), 3))) + { + show_error('Введите адрес проживания', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#snils').val(), 14))) + { + show_error('Введите СНИЛС', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + if(!(validate_length($('#inn').val(), 12))) + { + show_error('Введите ИНН', false); + $(this).children(".spinner-border").addClass('d-none'); + $(this).removeAttr("disabled"); + return; + } + + + + $.each($('.tg_input'), function(e) { message[$(this).prop("id")] = $(this).val(); }); - console.log(message); $.ajax({ type: "post", url: "/form_submit", @@ -64,4 +362,83 @@ $(document).ready(function(){ } }); }); -}); \ No newline at end of file + + + + + + + + + + + + +// $('.alert-placeholder').mouseenter(function(){ +// var popOverSettings = { +// placement: 'bottom', +// content: $(this).data('content'), +// trigger: 'hover', +// container: this, +// offset: '0' +// } +// $(this).popover(popOverSettings); +// $(this).popover('show'); +// $('.popover').addClass('alert-popover'); +// }); +// $('.alert-placeholder').mouseleave(function(){ +// $('.alert-popover').remove() +// }); + + + + let phoneStr = ''; + let formattedStr = ''; + let deleteMode = false; + const phoneInput = document.querySelector('#tel'); + const defaultFormat = '+7({0}{1}{2}){3}{4}{5}-{6}{7}-{8}{9}'; + phoneInput.value = formatPhoneString(); + + phoneInput.addEventListener('keydown', (e) => { + if (e.key === 'Backspace') + deleteMode = true; + else + deleteMode = false; + }); +7 + phoneInput.addEventListener('input', (e) => { + if (deleteMode) { + phoneInput.value = phoneInput.value; + phoneStr = parsePhoneString(phoneInput.value.replace("+7", "").replace("-", "")); + } else { + if (e.inputType == 'insertText' && !isNaN(parseInt(e.data))) { + if (phoneStr.length <= 9) + phoneStr += e.data; + } + phoneInput.value = formatPhoneString(); + } + }); + + function formatPhoneString() { + let strArr = phoneStr.split(''); + formattedStr = defaultFormat; + for (let i = 0; i < strArr.length; i++) { + formattedStr = formattedStr.replace(`{${i}}`, strArr[i]); + } + + if (formattedStr.indexOf('{') === -1) + { + return formattedStr; + } + else + { + return formattedStr.substring(0, formattedStr.indexOf('{')); + } + } + + function parsePhoneString(str) { + return str.replace(' ', '').replace('(', '').replace(')', '').replace('-', ''); + } +}); + + diff --git a/web_run.py b/web_run.py index c428a14..e9e7f64 100644 --- a/web_run.py +++ b/web_run.py @@ -4,5 +4,5 @@ from waitress import serve from config import config if __name__ == '__main__': - # webApp.run(debug=True, port=config['WEB_APP']['PORT']) - serve(webApp, host='0.0.0.0', port=config['WEB_APP']['PORT']) \ No newline at end of file + webApp.run(debug=True, port=config['WEB_APP']['PORT']) + #serve(webApp, host='0.0.0.0', port=config['WEB_APP']['PORT']) \ No newline at end of file