Spaces:
Running
Running
Commit ·
cf2c247
1
Parent(s): 14961a3
Updated logic for file processing
Browse files
server.py
CHANGED
|
@@ -32,15 +32,12 @@ session_data = {}
|
|
| 32 |
|
| 33 |
class GmailApiService:
|
| 34 |
def __init__(self):
|
| 35 |
-
self.sender_email = os.getenv('EMAIL_SENDER')
|
| 36 |
-
self.service = None
|
| 37 |
try:
|
| 38 |
-
from google.oauth2 import service_account
|
| 39 |
-
from googleapiclient.discovery import build
|
| 40 |
base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
|
| 41 |
if not base64_creds: print("[Gmail API] WARNING: GDRIVE_SA_KEY_BASE64 not found."); return
|
| 42 |
-
creds_json = base64.b64decode(base64_creds).decode('utf-8')
|
| 43 |
-
creds_dict = json.loads(creds_json)
|
| 44 |
credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
|
| 45 |
if self.sender_email: credentials = credentials.with_subject(self.sender_email)
|
| 46 |
self.service = build('gmail', 'v1', credentials=credentials)
|
|
@@ -51,20 +48,16 @@ class GmailApiService:
|
|
| 51 |
if not self.service or not recipients: return False
|
| 52 |
try:
|
| 53 |
from googleapiclient.errors import HttpError
|
| 54 |
-
message = MIMEMultipart()
|
| 55 |
-
message['From'] = self.sender_email; message['To'] = ', '.join(recipients); message['Subject'] = subject
|
| 56 |
message.attach(MIMEText(body, 'html'))
|
| 57 |
if attachments:
|
| 58 |
for filename, content in attachments.items():
|
| 59 |
-
part = MIMEBase('application', 'octet-stream')
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
|
| 63 |
-
message.attach(part)
|
| 64 |
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
|
| 65 |
sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
|
| 66 |
-
print(f"[Gmail API] Email sent successfully! Message ID: {sent_message['id']}")
|
| 67 |
-
return True
|
| 68 |
except Exception as e: print(f"[Gmail API] Error: {e}"); return False
|
| 69 |
|
| 70 |
class GoogleDriveService:
|
|
@@ -121,19 +114,20 @@ def generate_and_send_reports(session_id, results, is_crash_report=False, is_ter
|
|
| 121 |
socketio.emit('process_complete', {'message': 'No patients were processed.'}); return
|
| 122 |
data = session_data.get(session_id, {})
|
| 123 |
|
| 124 |
-
|
| 125 |
-
#
|
| 126 |
-
|
| 127 |
-
|
| 128 |
result_df = pd.DataFrame(results)
|
| 129 |
|
| 130 |
if not result_df.empty:
|
| 131 |
-
# Use a
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
| 137 |
bad_df = full_df[full_df['Status'] == 'Bad'][['Name', 'PRN', 'Status']]
|
| 138 |
timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
|
| 139 |
full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"
|
|
@@ -178,9 +172,10 @@ def handle_file_processing():
|
|
| 178 |
filtered_df = final_df[final_df['PRN'] != ''].copy()
|
| 179 |
|
| 180 |
patient_data_for_automation = filtered_df.to_dict('records')
|
| 181 |
-
|
| 182 |
-
# Base DataFrame for the final report
|
| 183 |
report_base_df = df_quantum.copy()
|
|
|
|
| 184 |
report_base_df['Status'] = ''
|
| 185 |
report_base_df['Status'] = report_base_df['Status'].astype('object')
|
| 186 |
|
|
|
|
| 32 |
|
| 33 |
class GmailApiService:
|
| 34 |
def __init__(self):
|
| 35 |
+
self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
|
|
|
|
| 36 |
try:
|
| 37 |
+
from google.oauth2 import service_account; from googleapiclient.discovery import build
|
|
|
|
| 38 |
base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
|
| 39 |
if not base64_creds: print("[Gmail API] WARNING: GDRIVE_SA_KEY_BASE64 not found."); return
|
| 40 |
+
creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
|
|
|
|
| 41 |
credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
|
| 42 |
if self.sender_email: credentials = credentials.with_subject(self.sender_email)
|
| 43 |
self.service = build('gmail', 'v1', credentials=credentials)
|
|
|
|
| 48 |
if not self.service or not recipients: return False
|
| 49 |
try:
|
| 50 |
from googleapiclient.errors import HttpError
|
| 51 |
+
message = MIMEMultipart(); message['From'] = self.sender_email; message['To'] = ', '.join(recipients); message['Subject'] = subject
|
|
|
|
| 52 |
message.attach(MIMEText(body, 'html'))
|
| 53 |
if attachments:
|
| 54 |
for filename, content in attachments.items():
|
| 55 |
+
part = MIMEBase('application', 'octet-stream'); part.set_payload(content.encode('utf-8'))
|
| 56 |
+
encoders.encode_base64(part); part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
|
| 57 |
+
msg.attach(part)
|
|
|
|
|
|
|
| 58 |
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
|
| 59 |
sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
|
| 60 |
+
print(f"[Gmail API] Email sent successfully! Message ID: {sent_message['id']}"); return True
|
|
|
|
| 61 |
except Exception as e: print(f"[Gmail API] Error: {e}"); return False
|
| 62 |
|
| 63 |
class GoogleDriveService:
|
|
|
|
| 114 |
socketio.emit('process_complete', {'message': 'No patients were processed.'}); return
|
| 115 |
data = session_data.get(session_id, {})
|
| 116 |
|
| 117 |
+
# --- Definitive Reporting Fix ---
|
| 118 |
+
# Create the base of the report from the processed data which includes Name and PRN
|
| 119 |
+
report_base_df = pd.DataFrame(data.get('patient_data_for_report'))
|
|
|
|
| 120 |
result_df = pd.DataFrame(results)
|
| 121 |
|
| 122 |
if not result_df.empty:
|
| 123 |
+
# Use a reliable merge to update the status
|
| 124 |
+
report_base_df = pd.merge(report_base_df, result_df[['Name', 'Status']], on='Name', how='left')
|
| 125 |
+
# Fill any rows that weren't processed with their original status (or blank)
|
| 126 |
+
report_base_df['Status_y'].fillna(report_base_df['Status_x'], inplace=True)
|
| 127 |
+
report_base_df.rename(columns={'Status_y': 'Status'}, inplace=True)
|
| 128 |
+
del report_base_df['Status_x']
|
| 129 |
+
|
| 130 |
+
full_df = report_base_df
|
| 131 |
bad_df = full_df[full_df['Status'] == 'Bad'][['Name', 'PRN', 'Status']]
|
| 132 |
timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
|
| 133 |
full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"
|
|
|
|
| 172 |
filtered_df = final_df[final_df['PRN'] != ''].copy()
|
| 173 |
|
| 174 |
patient_data_for_automation = filtered_df.to_dict('records')
|
| 175 |
+
|
| 176 |
+
# Base DataFrame for the final report now correctly includes PRN
|
| 177 |
report_base_df = df_quantum.copy()
|
| 178 |
+
report_base_df = pd.merge(report_base_df, filtered_df[['Name', 'PRN']], on='Name', how='left')
|
| 179 |
report_base_df['Status'] = ''
|
| 180 |
report_base_df['Status'] = report_base_df['Status'].astype('object')
|
| 181 |
|
worker.py
CHANGED
|
@@ -6,17 +6,15 @@ from selenium import webdriver
|
|
| 6 |
from selenium.webdriver.chrome.service import Service as ChromeService
|
| 7 |
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
| 8 |
from selenium.webdriver.common.by import By
|
|
|
|
| 9 |
from selenium.webdriver.support.ui import WebDriverWait
|
| 10 |
from selenium.webdriver.support import expected_conditions as EC
|
| 11 |
from selenium.common.exceptions import TimeoutException
|
| 12 |
|
| 13 |
class QuantumBot:
|
| 14 |
def __init__(self, socketio, app):
|
| 15 |
-
self.socketio = socketio
|
| 16 |
-
self.
|
| 17 |
-
self.driver = None
|
| 18 |
-
self.DEFAULT_TIMEOUT = 30
|
| 19 |
-
self.termination_event = threading.Event()
|
| 20 |
|
| 21 |
def _kill_chrome_processes(self):
|
| 22 |
try:
|
|
@@ -29,12 +27,9 @@ class QuantumBot:
|
|
| 29 |
try:
|
| 30 |
self.micro_status("Initializing headless browser...")
|
| 31 |
self._kill_chrome_processes()
|
| 32 |
-
options = ChromeOptions()
|
| 33 |
-
options.
|
| 34 |
-
options.add_argument("--
|
| 35 |
-
options.add_argument("--no-sandbox")
|
| 36 |
-
options.add_argument("--disable-dev-shm-usage")
|
| 37 |
-
options.add_argument("--disable-gpu")
|
| 38 |
options.add_argument("--window-size=1920,1080")
|
| 39 |
user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 40 |
options.add_argument(f"--user-agent={user_agent}")
|
|
@@ -45,8 +40,7 @@ class QuantumBot:
|
|
| 45 |
})
|
| 46 |
return True, None
|
| 47 |
except Exception as e:
|
| 48 |
-
error_message = f"Message: {str(e)}"
|
| 49 |
-
print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
|
| 50 |
return False, error_message
|
| 51 |
|
| 52 |
def micro_status(self, message):
|
|
@@ -55,15 +49,12 @@ class QuantumBot:
|
|
| 55 |
self.socketio.emit('micro_status_update', {'message': message})
|
| 56 |
|
| 57 |
def stop(self):
|
| 58 |
-
self.micro_status("Termination signal received...")
|
| 59 |
-
self.termination_event.set()
|
| 60 |
|
| 61 |
def login(self, username, password):
|
| 62 |
try:
|
| 63 |
-
self.micro_status("Navigating to login page...")
|
| 64 |
-
self.
|
| 65 |
-
time.sleep(2)
|
| 66 |
-
self.micro_status("Entering credentials...")
|
| 67 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
|
| 68 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
|
| 69 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
|
|
@@ -71,39 +62,34 @@ class QuantumBot:
|
|
| 71 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
|
| 72 |
return True, None
|
| 73 |
except Exception as e:
|
| 74 |
-
error_message = f"Error during login: {str(e)}"
|
| 75 |
-
print(f"[Bot] ERROR during login: {error_message}")
|
| 76 |
return False, error_message
|
| 77 |
|
| 78 |
def submit_otp(self, otp):
|
| 79 |
try:
|
| 80 |
-
self.micro_status(f"Submitting OTP...")
|
| 81 |
-
|
| 82 |
-
for i in range(6):
|
| 83 |
-
self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
|
| 84 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
|
| 85 |
self.micro_status("Verifying login success...")
|
| 86 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
|
| 87 |
return True, None
|
| 88 |
except Exception as e:
|
| 89 |
-
error_message = f"Error during OTP submission: {str(e)}"
|
| 90 |
-
print(f"[Bot] ERROR during OTP submission: {error_message}")
|
| 91 |
return False, error_message
|
| 92 |
|
| 93 |
def process_patient_list(self, patient_data):
|
| 94 |
results = []
|
| 95 |
for index, record in enumerate(patient_data):
|
| 96 |
-
if self.termination_event.is_set():
|
| 97 |
-
|
| 98 |
-
break
|
| 99 |
-
patient_name = record['Name']
|
| 100 |
-
patient_prn = record.get('PRN', '')
|
| 101 |
self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
|
| 102 |
status = self._process_single_patient(patient_name, patient_prn)
|
| 103 |
results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
|
| 104 |
with self.app.app_context():
|
| 105 |
self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
|
| 106 |
-
self.socketio.emit('stats_update', {
|
|
|
|
|
|
|
| 107 |
return results
|
| 108 |
|
| 109 |
def _process_single_patient(self, patient_name, patient_prn):
|
|
@@ -116,15 +102,10 @@ class QuantumBot:
|
|
| 116 |
self.micro_status(f"Searching... (Attempt {attempt + 1})")
|
| 117 |
WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
|
| 118 |
search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
|
| 119 |
-
search_box.click(); time.sleep(0.5)
|
| 120 |
-
search_box.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
break
|
| 124 |
-
except Exception:
|
| 125 |
-
time.sleep(1)
|
| 126 |
-
if not search_successful:
|
| 127 |
-
raise Exception("Failed to search for patient.")
|
| 128 |
time.sleep(3)
|
| 129 |
self.micro_status("Opening transaction details...")
|
| 130 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
|
|
@@ -135,39 +116,36 @@ class QuantumBot:
|
|
| 135 |
try:
|
| 136 |
self.micro_status("Verifying success and saving...")
|
| 137 |
company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
|
| 138 |
-
company_input.clear()
|
| 139 |
-
company_input.send_keys(patient_name)
|
| 140 |
|
| 141 |
# --- Definitive PRN Handling Logic ---
|
| 142 |
try:
|
| 143 |
-
contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "
|
| 144 |
-
self.micro_status("Clearing
|
| 145 |
-
contact_input.
|
|
|
|
|
|
|
| 146 |
if patient_prn and pd.notna(patient_prn) and str(patient_prn).strip():
|
| 147 |
self.micro_status(f"Entering PRN: {patient_prn}...")
|
| 148 |
contact_input.send_keys(str(patient_prn))
|
| 149 |
else:
|
| 150 |
-
self.micro_status("No PRN provided, leaving
|
| 151 |
except TimeoutException:
|
| 152 |
-
self.micro_status("
|
| 153 |
|
| 154 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
|
| 155 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
|
| 156 |
-
time.sleep(5)
|
| 157 |
-
return 'Done'
|
| 158 |
except TimeoutException:
|
| 159 |
self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
|
| 160 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
|
| 161 |
return 'Bad'
|
| 162 |
except Exception as e:
|
| 163 |
-
print(f"An error occurred while processing {patient_name}: {e}")
|
| 164 |
-
return 'Error'
|
| 165 |
|
| 166 |
def shutdown(self):
|
| 167 |
try:
|
| 168 |
-
if self.driver:
|
| 169 |
-
self.driver.quit()
|
| 170 |
self._kill_chrome_processes()
|
| 171 |
print("[Bot] Chrome session closed and cleaned up.")
|
| 172 |
-
except Exception as e:
|
| 173 |
-
print(f"[Bot] Error during shutdown: {e}")
|
|
|
|
| 6 |
from selenium.webdriver.chrome.service import Service as ChromeService
|
| 7 |
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
| 8 |
from selenium.webdriver.common.by import By
|
| 9 |
+
from selenium.webdriver.common.keys import Keys
|
| 10 |
from selenium.webdriver.support.ui import WebDriverWait
|
| 11 |
from selenium.webdriver.support import expected_conditions as EC
|
| 12 |
from selenium.common.exceptions import TimeoutException
|
| 13 |
|
| 14 |
class QuantumBot:
|
| 15 |
def __init__(self, socketio, app):
|
| 16 |
+
self.socketio = socketio; self.app = app; self.driver = None
|
| 17 |
+
self.DEFAULT_TIMEOUT = 30; self.termination_event = threading.Event()
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def _kill_chrome_processes(self):
|
| 20 |
try:
|
|
|
|
| 27 |
try:
|
| 28 |
self.micro_status("Initializing headless browser...")
|
| 29 |
self._kill_chrome_processes()
|
| 30 |
+
options = ChromeOptions(); options.binary_location = "/usr/bin/chromium"
|
| 31 |
+
options.add_argument("--headless=new"); options.add_argument("--no-sandbox")
|
| 32 |
+
options.add_argument("--disable-dev-shm-usage"); options.add_argument("--disable-gpu")
|
|
|
|
|
|
|
|
|
|
| 33 |
options.add_argument("--window-size=1920,1080")
|
| 34 |
user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 35 |
options.add_argument(f"--user-agent={user_agent}")
|
|
|
|
| 40 |
})
|
| 41 |
return True, None
|
| 42 |
except Exception as e:
|
| 43 |
+
error_message = f"Message: {str(e)}"; print(f"CRITICAL ERROR in WebDriver Initialization: {error_message}")
|
|
|
|
| 44 |
return False, error_message
|
| 45 |
|
| 46 |
def micro_status(self, message):
|
|
|
|
| 49 |
self.socketio.emit('micro_status_update', {'message': message})
|
| 50 |
|
| 51 |
def stop(self):
|
| 52 |
+
self.micro_status("Termination signal received..."); self.termination_event.set()
|
|
|
|
| 53 |
|
| 54 |
def login(self, username, password):
|
| 55 |
try:
|
| 56 |
+
self.micro_status("Navigating to login page..."); self.driver.get("https://gateway.quantumepay.com/")
|
| 57 |
+
time.sleep(2); self.micro_status("Entering credentials...")
|
|
|
|
|
|
|
| 58 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
|
| 59 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
|
| 60 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
|
|
|
|
| 62 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
|
| 63 |
return True, None
|
| 64 |
except Exception as e:
|
| 65 |
+
error_message = f"Error during login: {str(e)}"; print(f"[Bot] ERROR during login: {error_message}")
|
|
|
|
| 66 |
return False, error_message
|
| 67 |
|
| 68 |
def submit_otp(self, otp):
|
| 69 |
try:
|
| 70 |
+
self.micro_status(f"Submitting OTP..."); otp_digits = list(otp)
|
| 71 |
+
for i in range(6): self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
|
|
|
|
|
|
|
| 72 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
|
| 73 |
self.micro_status("Verifying login success...")
|
| 74 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
|
| 75 |
return True, None
|
| 76 |
except Exception as e:
|
| 77 |
+
error_message = f"Error during OTP submission: {str(e)}"; print(f"[Bot] ERROR during OTP submission: {error_message}")
|
|
|
|
| 78 |
return False, error_message
|
| 79 |
|
| 80 |
def process_patient_list(self, patient_data):
|
| 81 |
results = []
|
| 82 |
for index, record in enumerate(patient_data):
|
| 83 |
+
if self.termination_event.is_set(): print("[Bot] Termination detected."); break
|
| 84 |
+
patient_name = record['Name']; patient_prn = record.get('PRN', '')
|
|
|
|
|
|
|
|
|
|
| 85 |
self.micro_status(f"Processing '{patient_name}' ({index + 1}/{len(patient_data)})...")
|
| 86 |
status = self._process_single_patient(patient_name, patient_prn)
|
| 87 |
results.append({'Name': patient_name, 'PRN': patient_prn, 'Status': status})
|
| 88 |
with self.app.app_context():
|
| 89 |
self.socketio.emit('log_update', {'name': patient_name, 'prn': patient_prn, 'status': status})
|
| 90 |
+
self.socketio.emit('stats_update', {
|
| 91 |
+
'processed': len(results), 'remaining': len(patient_data) - len(results), 'status': status
|
| 92 |
+
})
|
| 93 |
return results
|
| 94 |
|
| 95 |
def _process_single_patient(self, patient_name, patient_prn):
|
|
|
|
| 102 |
self.micro_status(f"Searching... (Attempt {attempt + 1})")
|
| 103 |
WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
|
| 104 |
search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
|
| 105 |
+
search_box.click(); time.sleep(0.5); search_box.clear(); time.sleep(0.5)
|
| 106 |
+
search_box.send_keys(patient_name); search_successful = True; break
|
| 107 |
+
except Exception: time.sleep(1)
|
| 108 |
+
if not search_successful: raise Exception("Failed to search for patient.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
time.sleep(3)
|
| 110 |
self.micro_status("Opening transaction details...")
|
| 111 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
|
|
|
|
| 116 |
try:
|
| 117 |
self.micro_status("Verifying success and saving...")
|
| 118 |
company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
|
| 119 |
+
company_input.clear(); company_input.send_keys(patient_name)
|
|
|
|
| 120 |
|
| 121 |
# --- Definitive PRN Handling Logic ---
|
| 122 |
try:
|
| 123 |
+
contact_input = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.NAME, "company_contact")))
|
| 124 |
+
self.micro_status("Clearing Contact Name field...")
|
| 125 |
+
contact_input.click()
|
| 126 |
+
contact_input.send_keys(Keys.CONTROL + "a")
|
| 127 |
+
contact_input.send_keys(Keys.BACK_SPACE)
|
| 128 |
if patient_prn and pd.notna(patient_prn) and str(patient_prn).strip():
|
| 129 |
self.micro_status(f"Entering PRN: {patient_prn}...")
|
| 130 |
contact_input.send_keys(str(patient_prn))
|
| 131 |
else:
|
| 132 |
+
self.micro_status("No PRN provided, leaving Contact Name blank.")
|
| 133 |
except TimeoutException:
|
| 134 |
+
self.micro_status("Contact Name field not found, skipping PRN step.")
|
| 135 |
|
| 136 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
|
| 137 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
|
| 138 |
+
time.sleep(5); return 'Done'
|
|
|
|
| 139 |
except TimeoutException:
|
| 140 |
self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
|
| 141 |
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
|
| 142 |
return 'Bad'
|
| 143 |
except Exception as e:
|
| 144 |
+
print(f"An error occurred while processing {patient_name}: {e}"); return 'Error'
|
|
|
|
| 145 |
|
| 146 |
def shutdown(self):
|
| 147 |
try:
|
| 148 |
+
if self.driver: self.driver.quit()
|
|
|
|
| 149 |
self._kill_chrome_processes()
|
| 150 |
print("[Bot] Chrome session closed and cleaned up.")
|
| 151 |
+
except Exception as e: print(f"[Bot] Error during shutdown: {e}")
|
|
|