diff --git a/Clario/Downloads/Zpracovano/2026-06-10_10-05-01 77242113UCO3001 Clario eCOA DCRs.csv b/Clario/Downloads/Zpracovano/2026-06-10_10-05-01 77242113UCO3001 Clario eCOA DCRs.csv new file mode 100644 index 0000000..b37939b --- /dev/null +++ b/Clario/Downloads/Zpracovano/2026-06-10_10-05-01 77242113UCO3001 Clario eCOA DCRs.csv @@ -0,0 +1,209 @@ +"Protocol","Country","Site","PI Name","Subject ID","Age at Informed Consent","Baseline Stool Count","Confirm Baseline Stool Count","Data Correction ID","Creation Date UTC","Status","Description","Date of Last Action UTC","Total Open Period","Total Open Time (Days)","Current Status Time (Days)","Type","Next Action Required","Category","Query History","Reason for Change","Resolution" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","SW00703544","13-May-2026","Submitted","Please change answer to clinical remision from no to YES (week 12). Entry erros ","20-May-2026","15-21 Days","19","14","Query Active ","Site","New","(1) 20 May 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification Request. + +For us to process your request, please let us know the name of the form (with date) with question. + +Thank you. ERT/CLARIO Data Coordination Team + +","Entry Error","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","SW00696586","09-Apr-2026","ReadyForQC","Please correct date of endoscopy to date: 18 March 2026 (from 25 March 2026)","15-Apr-2026","Over 28 Days","41","37","Query Active ","Site","Site-Entered Data","","Entry Error","CLARIO RESOLUTION: + +Part 1: In Mayo Subscore (1) dated 08 Apr 2026 for I-0 visit, CLARIO to make the following changes: +- What was the date of endoscopy? (ENDODT1D): from 25 Mar 2026 to 18 Mar 2026 +- Data Flag (QSDFLG1B): from blank to check +" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","SW00704536","19-May-2026","ReadyForQC","Please change the endoscopy date to 19-FEB-2026. 06-MAR-2026 was entered in error. ","26-May-2026","15-21 Days","15","10","Query Active ","Site","Site-Entered Data","","Entry Error","CLARIO RESOLUTION: + +Part 1: In Mayo Subscore (1) dated 20 Mar 2026 for I-0 visit, CLARIO to make the following changes: +-What was the date of endoscopy? (ENDODT1D): from 06 Mar 2026 to 19 Feb 2026 +- Data Flag (QSDFLG1B): from blank to check +" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","SW00706684","01-Jun-2026","Submitted","The right endoscopy date is 23MAR2026, please change the date","05-Jun-2026","4-7 Days","7","2","Query Active ","Site","New","(1) 05 Jun 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification. + +Please confirm that if you are requesting following. + +Mayo Subscore (1) dated 07 Apr 2026 for I-0 +What was the date of endoscopy? (ENDODT1D): from 24 Mar 2026 to 23 Mar 2026 + +Thank you. ERT/CLARIO Data Coordination Team. + + +","Entry Error","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","SW00705646","26-May-2026","ReadyForQC","Correct visit date I-O is 12-May-2026. All questionaries were filled on paper and entered in tablet later. +Log-in issue. ","09-Jun-2026","8-14 Days","10","1","","Clario DM","Visit Data","(1) 01 Jun 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification. + + Please provide the timestamps for each of the assessments if you used paper forms and transcribed into the device. + If unknown, ERT will use a dummy timestamp. + +Thank you. ERT/CLARIO Data Coordination Team. + +(2) 01 Jun 2026 dstepek@vnbrno.cz (Site User): time is unknown + +","Changed Information","CLARIO RESOLUTION: + +Part 1: In the following forms for I-0, CLARIO to make the following changes: +-Report Date: from 26May 2026 to 12 May 2026 +-Report Start Date and time: from 26 May 2026 to 12 May 2026 23:59:59 +-Event End Date: from 26 May 2026 08:27:57 to 12 May 2026 23:59:59 + ++Tablet Training Module (1) ++Participant Start Instructions (1) ++IBDQ (1) ++PROMIS Fatigue – Short Form 7a (1) ++BASDAI (1) ++Participant End Instructions (1) ++Visit End (122) +" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","SW00708623","10-Jun-2026","Submitted","Correct date of I-2 is 26.5.2026. all questionaries were entered on paper at 07,45 and transmited later. ","10-Jun-2026","1 Day","1","","","Clario DM","New","","Changed Information","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","SW00706581","29-May-2026","ReadyForQC","baseline stool count reported by subject is 0, please change to 1 as per CRA request (subject has 1 stool in 2-3 days if in remission)","05-Jun-2026","4-7 Days","7","3","","Clario DM","Demographic","","Changed Information","CLARIO RESOLUTION: + +Part 1: In System Variables form, CLARIO to make the following changes: +- Baseline Stool Count (PT.Custom4): from 0 to 1 +" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","SW00705916","27-May-2026","ReadyForQC","As per ATS investigation (ATS26040111), please remove the below form which was entered as a duplicate + +- MAYO Diary (5) 24 Apr 2026","05-Jun-2026","8-14 Days","9","3","","Clario DM","Technical Revision","","Technical Revision - Other","CLARIO RESOLUTION: + +Part 1: CLARIO to delete MAYO Diary (5) dated 24 Apr 2026 +" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","SW00701729","06-May-2026","Completed","Dears, please delete data from visit I-0 (reported as 4th of May 2026) as this visit had to be postponed - see the previous DCR of this patient and change data request that was corrected. Patient has left the site before it was resolved and and new date of I-0 was planned. Patient continues to fill in his diary and patient is coming to I=0 visit within allowed window. We need the system and tablet to be ready to run new Mayo Score Report with updated and recent data (e.g. reflect new I-0 visit date, new eligible days -1 to -7.). +thank you, Jiri Skopek","19-May-2026","8-14 Days","8","","","","Visit Data","(1) 11 May 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification. + +Please note that the delete forms are allowed if the reason is one of the following. +If not, forms will move to unscheduled visit. + +Data collected by the wrong patient. +Data collected by someone other than the patient. +Data collected prior to informed consent, or after withdrawal from the study. +Duplicate data erroneously entered at an Unscheduled visit via paper transcription. +Data collected that is not expected per protocol. + +Also, I-0 visit is still ongoing. Please close the visit. +Once the visit was closed, we will process accoridngly. + +Thank you. ERT/CLARIO Data Coordination Team + +(2) 11 May 2026 jskopek (Site User): Dears, +I do not see any option that is adequate -from the list. Data are not needed to be deleted fully, they reflect the situation at May4th. Please mark it as unscheduled visit - as exactly that is the case. We need the system to be ready for I-0 visit planned for next week. +I will close the visit tomorrow - do you mean in tablet/ipad? +Thank you very much for your help! Jiri + +(3) 12 May 2026 venkata.ramana (Clario): Thank you for your response. +Please note that the visit I-0 was still ongoing but not closed yet. +So please close the visit. +Kind Regards, Clario Data Coordination Team. + +(4) 12 May 2026 jskopek (Site User): If I try to close the I-O visit in TABLET, it asks me if patient fulfils eligibility criteria to proceed to next visit based on these old data – if I answer NO, it asks me to DEACTIVATE patient. I do not want to DEACTIVATE patient – can you help WHERE and HOW to close this visit for you to change it to UNSCHEDULED and not to de-activate patient? +Thank you Jiri + + +","Other-delete visit I-0","CLARIO RESOLUTION: + +Part 1: In the following forms dated 04 May 2026, CLARIO to make the following changes: +-Event ID: from I-0 to Unscheduled Visit 1 +-Event At Entry: from I-0 to Unscheduled Visit 1 + ++Visit Start (49) ++ePRO Availability (1) ++Mayo Subscore (1) ++PGA (1) + +Part 2: CLARIO to delete the following forms dated 04 May 2026 for I-0 visit. + ++C-SSRS Since Last Visit (1) ++C-SSRS Since Last Visit Findings Report (1) + +Part 3: CLARIO to manually enter Visit End form for Unscheduled visit 1 with the following information: +-Protocol: 77242113UCO3001 +-Report Date: 04 May 2026 +-Report Start Date and Time: 04 May 2026 23:59:59 +-Event ID: Unscheduled Visit 1 +-Event End Date: 04 May 2026 23:59:59 +-Visit Status: Incomplete +-Phase At Entry: Screening +-Phase At Entry Timestamp: 13 Apr 2026 12:32:20 +-Event At Entry: Unscheduled visit 1 +-Event Start Date: 04 May 2026 23:59:59 +-Event Time Zone Offset in Milliseconds: 7200000 +-Session Repeat Number (SESREP1N): 0 +-Session Instance Id (SESINST1S): 3f1214f0-4788-11f1-a0cf-bb403212adce +" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","SW00701226","04-May-2026","Completed","Dears, we would like ask you to change the information I read on assignment form given by patient on April 13, 2026 (Visit 1), Baseline Stool Count (PT.Custom4) as 3 that should be reported as 1. +Patient has entered wrong number as he did not understood it should be number of stools when illness is in remission or absent. He is a child and did not reflected this question correctly. Therefore, please change Baseline Stool Count = 1. +Thank you, Jiri Skopek ","04-May-2026","1 Day","1","","","","Demographic","","Changed Information","(Clario instructions) + +1. Please make below changes in the assignment form: + +Baseline Stool Count (PT. Custom4): 03 to 01." +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","SW00699492","23-Apr-2026","ReadyForQC","Please correct the date of endoscopy done during screening visit of patient CZ100212001 to correct date 16-MAR-2026.","29-Apr-2026","Over 28 Days","32","28","Query Active ","Site","Site-Entered Data","","Changed Information","CLARIO RESOLUTION: + +Part 1: In the Mayo Subscore (1) dated 07 Apr 2026 for I-0 visit, CLARIO to make the following changes: +-What was the date of endoscopy? (ENDODT1D): from 24 Mar 2026 to 16 Mar 2026 +- Data Flag (QSDFLG1B): from blank to check +" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","SW00703322","12-May-2026","Completed","As per ATS investigation (ATS26040111), please remove the below form that's been entered as a duplicate + +- MAYO Diary (16) - 18 Mar 2026 +","20-May-2026","4-7 Days","6","","","","Technical Revision","","Technical Revision - Other","CLARIO RESOLUTION: + +Part 1: CLARIO to delete the MAYO Diary (16) dated 18 Mar 2026. +" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","SW00689748","09-Mar-2026","Completed","Dear all, + +Patient CZ 100222003 was randomized on 9 Mar 2026. Kindly correct the colonoscopy date to 11 Feb 2025. + +The date was initially entered as 21 Feb 2025 because the earlier date could not be entered in the system. The patient was rescreened.","02-Apr-2026","15-21 Days","17","","","","Site-Entered Data","(1) 13 Mar 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification. + +Could you please conform that if you are requesting following? + +Mayo Subscore (1) dated 09 Mar 2026 for I-0 visit +-What was the date of endoscopy? (ENDODT1D): from 23 Feb 2026 to 11 Feb 2025 + +Could you please confirm the year? This subject was assigned on 02 Mar 2026, you are providing that correct date is 11 Feb 2025 which a year ago. +If you are not requesting above, please provide us the name of the form with question. + +Thank you. ERT/CLARIO Data Coordination Team + + +(2) 13 Mar 2026 katerina.havlikova@clinoxus.com (Site User): confirm date of colonoscopy 11Feb2026 + +(3) 21 Mar 2026 msullivan (Clario): Dear Site, + +The requested changes to the Mayo data have been updated. Please navigate to the Mayo Score Report and resubmit the form for visit to log the updated Mayo Score form. Once done, please respond to this query confirming that the Mayo Score has been resubmitted. + +Thank you. ERT/CLARIO Data Coordination Team + +(4) 24 Mar 2026 jana.pomahacova@clinoxus.com (Site User): Thank you and sent + +","New Information","CLARIO RESOLUTION: + +Part 1: In the Mayo Subscore (1) dated 09 Mar 2026 for I-0 visit, CLARIO to make the following changes: +-What was the date of endoscopy? (ENDODT1D): from 23 Feb 2026 to 11 Feb 2025 +-Data Flag (QSDFLG1B): from blank to check" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","SW00705372","22-May-2026","Submitted","Dear all, please change Colonoscopz date from 8April2026 to date 01Apr2026 Thank you in advance","02-Jun-2026","8-14 Days","12","5","","Clario DM","New","(1) 29 May 2026 msullivan (Clario): Please confirm your request + +Dear Site. Thank you for submitting this Data Clarification. + +Please provide us the name of the form for this request. + +Thank you. ERT/CLARIO Data Coordination Team + +(2) 02 Jun 2026 katerina.havlikova@clinoxus.com (Site User): Dear all, please change Colonoscopy for Week I-12 date from 8April2026 to date 01Apr2026 Thank you in advance + +","Changed Information","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","SW00702538","08-May-2026","Completed","This TRR is to document the correction to the Mayo Subscore (1) form, where the following variables were populated with NULL values, due to a known core defect: +Event At Entry, Event Start Date, Event Time Zone Offset in Milliseconds.","12-May-2026","2-3 Days","2","","","","Technical Revision","","Technical Revision - Other","Please make the below changes in Mayo Subscore (1) dated 22 Apr 2026: + +-Event At Entry: I-0 +-Event Start Date: 09 Apr 2026 08:09:19 +-Event Time Zone Offset in Milliseconds: 7200000" diff --git a/Clario/Downloads/Zpracovano/2026-06-10_10-10-57 77242113UCO3001 Clario MayoDiary.csv b/Clario/Downloads/Zpracovano/2026-06-10_10-10-57 77242113UCO3001 Clario MayoDiary.csv new file mode 100644 index 0000000..f3153eb --- /dev/null +++ b/Clario/Downloads/Zpracovano/2026-06-10_10-10-57 77242113UCO3001 Clario MayoDiary.csv @@ -0,0 +1,1328 @@ +"Protocol","Country","Site","PI Name","Subject ID","Age at Informed Consent","Baseline Stool Count","Confirm Baseline Stool Count","Report Date","Report Start Date/Time","Report End Date/Time","Duration","Form Number","Role","Original Source","Current Source","Constipation (Code)","Constipation","Diarrhea (Code)","Diarrhea","Irregularity (Code)","Irregularity","Not Applicable (Code)","Not Applicable","Stool Frequency","Stool Frequency Confirmation (Code)","Stool Frequency Confirmation","MAYO050 (Code)","MAYO050","Data Comment","Retro Data Entry Visit Flag (Code)","Retro Data Entry Visit Flag","Admin Language","Admin Device (Code)","Admin Device","Data Flag (Code)","Data Flag","User Name","Paper Source (Code)","Paper Source" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","22-Jan-2026 ","22-Jan-2026 18:01:10","22-Jan-2026 18:01:44","00:34","1","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","23-Jan-2026 ","23-Jan-2026 21:22:44","23-Jan-2026 21:23:25","00:41","2","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","24-Jan-2026 ","24-Jan-2026 18:04:18","24-Jan-2026 18:04:36","00:18","3","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","27-Jan-2026 ","28-Jan-2026 18:33:09","28-Jan-2026 18:33:24","00:15","4","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","28-Jan-2026 ","28-Jan-2026 18:34:00","28-Jan-2026 18:34:20","00:20","5","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","29-Jan-2026 ","29-Jan-2026 21:36:05","29-Jan-2026 21:36:22","00:17","6","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","30-Jan-2026 ","31-Jan-2026 00:05:37","31-Jan-2026 00:05:58","00:21","7","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","31-Jan-2026 ","31-Jan-2026 23:13:18","31-Jan-2026 23:13:35","00:17","8","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","01-Feb-2026 ","01-Feb-2026 19:26:08","01-Feb-2026 19:26:36","00:28","9","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","02-Feb-2026 ","02-Feb-2026 18:01:48","02-Feb-2026 18:02:06","00:18","10","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","03-Feb-2026 ","03-Feb-2026 18:02:54","03-Feb-2026 18:03:20","00:26","11","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","04-Feb-2026 ","04-Feb-2026 18:02:37","04-Feb-2026 18:03:09","00:32","12","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","05-Feb-2026 ","05-Feb-2026 22:23:29","05-Feb-2026 22:23:46","00:17","13","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","06-Feb-2026 ","06-Feb-2026 22:20:04","06-Feb-2026 22:20:19","00:15","14","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","07-Feb-2026 ","07-Feb-2026 18:02:19","07-Feb-2026 18:02:51","00:32","15","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","08-Feb-2026 ","08-Feb-2026 18:03:25","08-Feb-2026 18:03:46","00:21","16","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","09-Feb-2026 ","09-Feb-2026 19:06:44","09-Feb-2026 19:07:24","00:40","17","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","10-Feb-2026 ","10-Feb-2026 22:25:12","10-Feb-2026 22:25:32","00:20","18","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","11-Feb-2026 ","12-Feb-2026 18:34:50","12-Feb-2026 18:35:15","00:25","19","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","12-Feb-2026 ","12-Feb-2026 18:35:49","12-Feb-2026 18:36:01","00:12","20","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","13-Feb-2026 ","13-Feb-2026 18:14:41","13-Feb-2026 18:14:55","00:14","21","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","14-Feb-2026 ","14-Feb-2026 18:01:52","14-Feb-2026 18:02:30","00:38","22","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","15-Feb-2026 ","16-Feb-2026 19:00:39","16-Feb-2026 19:00:56","00:17","23","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","16-Feb-2026 ","16-Feb-2026 19:01:41","16-Feb-2026 19:01:54","00:13","24","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","17-Feb-2026 ","17-Feb-2026 18:35:12","17-Feb-2026 18:35:26","00:14","25","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","18-Feb-2026 ","18-Feb-2026 18:02:21","18-Feb-2026 18:02:37","00:16","26","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","19-Feb-2026 ","20-Feb-2026 20:40:28","20-Feb-2026 20:40:55","00:27","27","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","20-Feb-2026 ","20-Feb-2026 20:41:50","20-Feb-2026 20:42:13","00:23","28","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","21-Feb-2026 ","21-Feb-2026 22:17:51","21-Feb-2026 22:18:05","00:14","29","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","22-Feb-2026 ","23-Feb-2026 18:59:42","23-Feb-2026 18:59:59","00:17","30","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","23-Feb-2026 ","23-Feb-2026 19:00:43","23-Feb-2026 19:00:57","00:14","31","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","24-Feb-2026 ","24-Feb-2026 18:33:05","24-Feb-2026 18:33:18","00:13","32","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","25-Feb-2026 ","25-Feb-2026 20:00:09","25-Feb-2026 20:00:22","00:13","33","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","26-Feb-2026 ","26-Feb-2026 21:45:08","26-Feb-2026 21:45:42","00:34","34","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","27-Feb-2026 ","27-Feb-2026 21:43:17","27-Feb-2026 21:43:33","00:16","35","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","28-Feb-2026 ","28-Feb-2026 18:53:08","28-Feb-2026 18:53:54","00:46","36","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","01-Mar-2026 ","01-Mar-2026 19:53:01","01-Mar-2026 19:53:40","00:39","37","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","02-Mar-2026 ","02-Mar-2026 18:05:10","02-Mar-2026 18:05:26","00:16","38","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","03-Mar-2026 ","03-Mar-2026 19:44:37","03-Mar-2026 19:45:11","00:34","39","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","04-Mar-2026 ","04-Mar-2026 18:45:15","04-Mar-2026 18:45:35","00:20","40","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","05-Mar-2026 ","05-Mar-2026 18:58:16","05-Mar-2026 18:58:36","00:20","41","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","06-Mar-2026 ","06-Mar-2026 18:28:33","06-Mar-2026 18:28:49","00:16","42","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","07-Mar-2026 ","07-Mar-2026 19:46:29","07-Mar-2026 19:46:47","00:18","43","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","08-Mar-2026 ","08-Mar-2026 20:05:17","08-Mar-2026 20:05:48","00:31","44","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","09-Mar-2026 ","09-Mar-2026 19:06:21","09-Mar-2026 19:06:43","00:22","45","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","10-Mar-2026 ","10-Mar-2026 18:19:08","10-Mar-2026 18:19:29","00:21","46","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","11-Mar-2026 ","11-Mar-2026 21:03:53","11-Mar-2026 21:04:07","00:14","47","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","12-Mar-2026 ","12-Mar-2026 18:17:30","12-Mar-2026 18:17:50","00:20","48","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","13-Mar-2026 ","13-Mar-2026 18:05:59","13-Mar-2026 18:07:01","01:02","49","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","14-Mar-2026 ","14-Mar-2026 20:44:54","14-Mar-2026 20:45:13","00:19","50","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","15-Mar-2026 ","15-Mar-2026 18:36:33","15-Mar-2026 18:36:52","00:19","51","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","16-Mar-2026 ","16-Mar-2026 22:48:59","16-Mar-2026 22:49:17","00:18","52","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","17-Mar-2026 ","17-Mar-2026 18:02:01","17-Mar-2026 18:02:18","00:17","53","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","18-Mar-2026 ","18-Mar-2026 21:00:45","18-Mar-2026 21:01:25","00:40","54","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","19-Mar-2026 ","19-Mar-2026 21:31:31","19-Mar-2026 21:32:18","00:47","55","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","20-Mar-2026 ","21-Mar-2026 14:10:40","21-Mar-2026 14:10:56","00:16","56","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","21-Mar-2026 ","21-Mar-2026 23:09:00","21-Mar-2026 23:09:18","00:18","57","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","22-Mar-2026 ","22-Mar-2026 19:08:29","22-Mar-2026 19:08:50","00:21","58","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","23-Mar-2026 ","23-Mar-2026 18:16:57","23-Mar-2026 18:17:14","00:17","59","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","24-Mar-2026 ","24-Mar-2026 21:05:21","24-Mar-2026 21:05:35","00:14","60","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","25-Mar-2026 ","25-Mar-2026 21:08:43","25-Mar-2026 21:09:39","00:56","61","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","26-Mar-2026 ","26-Mar-2026 20:46:45","26-Mar-2026 20:47:05","00:20","62","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","27-Mar-2026 ","27-Mar-2026 20:55:30","27-Mar-2026 20:55:57","00:27","63","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","28-Mar-2026 ","28-Mar-2026 20:00:16","28-Mar-2026 20:00:32","00:16","64","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","29-Mar-2026 ","29-Mar-2026 22:09:04","29-Mar-2026 22:09:18","00:14","65","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","30-Mar-2026 ","31-Mar-2026 11:57:59","31-Mar-2026 11:58:17","00:18","66","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","31-Mar-2026 ","01-Apr-2026 18:21:26","01-Apr-2026 18:22:46","01:20","67","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","01-Apr-2026 ","01-Apr-2026 18:23:44","01-Apr-2026 18:23:58","00:14","68","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","02-Apr-2026 ","02-Apr-2026 20:54:02","02-Apr-2026 20:54:22","00:20","69","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","03-Apr-2026 ","03-Apr-2026 19:54:40","03-Apr-2026 19:55:08","00:28","70","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","04-Apr-2026 ","04-Apr-2026 18:11:01","04-Apr-2026 18:11:24","00:23","71","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","05-Apr-2026 ","05-Apr-2026 23:04:39","05-Apr-2026 23:05:04","00:25","72","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","06-Apr-2026 ","06-Apr-2026 21:42:50","06-Apr-2026 21:43:12","00:22","73","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","07-Apr-2026 ","07-Apr-2026 21:43:47","07-Apr-2026 21:44:05","00:18","74","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","08-Apr-2026 ","09-Apr-2026 21:36:29","09-Apr-2026 21:36:45","00:16","75","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","09-Apr-2026 ","09-Apr-2026 21:39:42","09-Apr-2026 21:40:10","00:28","76","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","10-Apr-2026 ","10-Apr-2026 18:05:18","10-Apr-2026 18:05:37","00:19","77","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","11-Apr-2026 ","11-Apr-2026 23:16:18","11-Apr-2026 23:16:50","00:32","78","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","12-Apr-2026 ","12-Apr-2026 20:31:50","12-Apr-2026 20:32:07","00:17","79","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","13-Apr-2026 ","13-Apr-2026 22:52:20","13-Apr-2026 22:52:40","00:20","80","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","14-Apr-2026 ","14-Apr-2026 21:00:55","14-Apr-2026 21:01:12","00:17","81","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","15-Apr-2026 ","15-Apr-2026 21:26:28","15-Apr-2026 21:26:45","00:17","82","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","16-Apr-2026 ","16-Apr-2026 20:36:59","16-Apr-2026 20:37:29","00:30","83","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","17-Apr-2026 ","17-Apr-2026 18:35:45","17-Apr-2026 18:36:18","00:33","84","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","18-Apr-2026 ","19-Apr-2026 19:09:05","19-Apr-2026 19:09:27","00:22","85","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","19-Apr-2026 ","19-Apr-2026 19:10:48","19-Apr-2026 19:11:05","00:17","86","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","20-Apr-2026 ","20-Apr-2026 21:41:27","20-Apr-2026 21:42:05","00:38","87","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","21-Apr-2026 ","21-Apr-2026 18:16:50","21-Apr-2026 18:17:13","00:23","88","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","24-Apr-2026 ","25-Apr-2026 21:27:18","25-Apr-2026 21:27:37","00:19","89","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","25-Apr-2026 ","25-Apr-2026 21:28:48","25-Apr-2026 21:29:07","00:19","90","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","26-Apr-2026 ","26-Apr-2026 21:20:40","26-Apr-2026 21:21:09","00:29","91","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","27-Apr-2026 ","27-Apr-2026 19:23:32","27-Apr-2026 19:23:57","00:25","92","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","28-Apr-2026 ","28-Apr-2026 20:20:00","28-Apr-2026 20:20:16","00:16","93","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","29-Apr-2026 ","29-Apr-2026 18:40:54","29-Apr-2026 18:41:13","00:19","94","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","30-Apr-2026 ","01-May-2026 19:13:21","01-May-2026 19:13:42","00:21","95","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","01-May-2026 ","01-May-2026 19:15:10","01-May-2026 19:15:41","00:31","96","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","02-May-2026 ","02-May-2026 20:04:07","02-May-2026 20:04:25","00:18","97","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","03-May-2026 ","03-May-2026 20:30:45","03-May-2026 20:31:11","00:26","98","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","04-May-2026 ","04-May-2026 22:14:16","04-May-2026 22:14:39","00:23","99","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","05-May-2026 ","05-May-2026 18:03:33","05-May-2026 18:03:59","00:26","100","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","06-May-2026 ","06-May-2026 19:09:27","06-May-2026 19:10:10","00:43","101","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","07-May-2026 ","07-May-2026 22:20:20","07-May-2026 22:20:44","00:24","102","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","08-May-2026 ","08-May-2026 22:31:15","08-May-2026 22:31:30","00:15","103","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","09-May-2026 ","09-May-2026 22:14:16","09-May-2026 22:14:41","00:25","104","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","10-May-2026 ","10-May-2026 22:59:00","10-May-2026 22:59:24","00:24","105","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","11-May-2026 ","11-May-2026 21:17:29","11-May-2026 21:18:07","00:38","106","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","12-May-2026 ","12-May-2026 20:23:13","12-May-2026 20:24:06","00:53","107","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","13-May-2026 ","13-May-2026 18:08:45","13-May-2026 18:09:03","00:18","108","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","15-May-2026 ","15-May-2026 23:21:17","15-May-2026 23:21:35","00:18","109","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","16-May-2026 ","17-May-2026 18:26:06","17-May-2026 18:26:24","00:18","110","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","17-May-2026 ","17-May-2026 18:27:28","17-May-2026 18:27:43","00:15","111","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","18-May-2026 ","18-May-2026 20:31:21","18-May-2026 20:31:43","00:22","112","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","19-May-2026 ","19-May-2026 18:40:11","19-May-2026 18:40:28","00:17","113","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","20-May-2026 ","20-May-2026 22:17:46","20-May-2026 22:18:01","00:15","114","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","21-May-2026 ","21-May-2026 20:03:10","21-May-2026 20:03:28","00:18","115","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","22-May-2026 ","22-May-2026 18:35:01","22-May-2026 18:35:16","00:15","116","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","23-May-2026 ","23-May-2026 22:25:17","23-May-2026 22:26:05","00:48","117","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","24-May-2026 ","25-May-2026 18:08:43","25-May-2026 18:09:27","00:44","118","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","25-May-2026 ","25-May-2026 18:10:27","25-May-2026 18:11:02","00:35","119","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","26-May-2026 ","26-May-2026 20:54:59","26-May-2026 20:55:19","00:20","120","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","27-May-2026 ","27-May-2026 22:05:34","27-May-2026 22:06:07","00:33","121","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","28-May-2026 ","28-May-2026 18:48:45","28-May-2026 18:48:59","00:14","122","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","29-May-2026 ","29-May-2026 21:11:23","29-May-2026 21:11:52","00:29","123","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","30-May-2026 ","30-May-2026 18:29:24","30-May-2026 18:29:40","00:16","124","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","31-May-2026 ","31-May-2026 19:18:22","31-May-2026 19:18:40","00:18","125","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","01-Jun-2026 ","01-Jun-2026 21:22:48","01-Jun-2026 21:23:09","00:21","126","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","02-Jun-2026 ","02-Jun-2026 21:15:00","02-Jun-2026 21:15:21","00:21","127","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","03-Jun-2026 ","03-Jun-2026 21:18:20","03-Jun-2026 21:18:38","00:18","128","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","04-Jun-2026 ","04-Jun-2026 22:51:45","04-Jun-2026 22:52:11","00:26","129","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","05-Jun-2026 ","05-Jun-2026 19:07:47","05-Jun-2026 19:08:07","00:20","130","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","06-Jun-2026 ","07-Jun-2026 22:58:12","07-Jun-2026 22:58:37","00:25","131","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","07-Jun-2026 ","07-Jun-2026 22:59:15","07-Jun-2026 22:59:49","00:34","132","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","08-Jun-2026 ","09-Jun-2026 20:44:44","09-Jun-2026 20:45:02","00:18","133","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","48","1","","09-Jun-2026 ","09-Jun-2026 20:45:58","09-Jun-2026 20:46:16","00:18","134","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","02-Mar-2026 ","02-Mar-2026 18:03:24","02-Mar-2026 18:03:45","00:21","1","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","03-Mar-2026 ","03-Mar-2026 19:43:48","03-Mar-2026 19:45:18","01:30","2","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","04-Mar-2026 ","04-Mar-2026 19:03:27","04-Mar-2026 19:03:48","00:21","3","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","05-Mar-2026 ","05-Mar-2026 19:15:46","05-Mar-2026 19:16:04","00:18","4","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","06-Mar-2026 ","06-Mar-2026 18:23:46","06-Mar-2026 18:25:08","01:22","5","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","07-Mar-2026 ","07-Mar-2026 19:05:22","07-Mar-2026 19:05:45","00:23","6","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","08-Mar-2026 ","08-Mar-2026 19:09:52","08-Mar-2026 19:10:47","00:55","7","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","09-Mar-2026 ","09-Mar-2026 19:08:05","09-Mar-2026 19:08:19","00:14","8","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","10-Mar-2026 ","10-Mar-2026 18:15:08","10-Mar-2026 18:15:19","00:11","9","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","11-Mar-2026 ","11-Mar-2026 20:13:00","11-Mar-2026 20:13:15","00:15","10","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","12-Mar-2026 ","12-Mar-2026 19:05:47","12-Mar-2026 19:06:03","00:16","11","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","13-Mar-2026 ","13-Mar-2026 18:59:16","13-Mar-2026 18:59:52","00:36","12","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","14-Mar-2026 ","14-Mar-2026 20:12:42","14-Mar-2026 20:13:14","00:32","13","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","15-Mar-2026 ","15-Mar-2026 22:50:36","15-Mar-2026 22:50:57","00:21","14","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","16-Mar-2026 ","16-Mar-2026 18:15:35","16-Mar-2026 18:15:52","00:17","15","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","17-Mar-2026 ","17-Mar-2026 21:15:59","17-Mar-2026 21:16:11","00:12","16","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","18-Mar-2026 ","18-Mar-2026 18:32:31","18-Mar-2026 18:33:05","00:34","17","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","19-Mar-2026 ","19-Mar-2026 22:19:19","19-Mar-2026 22:19:35","00:16","18","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","20-Mar-2026 ","21-Mar-2026 21:58:25","21-Mar-2026 21:58:52","00:27","19","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","21-Mar-2026 ","21-Mar-2026 21:59:25","21-Mar-2026 21:59:50","00:25","20","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","22-Mar-2026 ","22-Mar-2026 18:16:36","22-Mar-2026 18:17:03","00:27","21","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","23-Mar-2026 ","23-Mar-2026 18:15:48","23-Mar-2026 18:16:13","00:25","22","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","24-Mar-2026 ","24-Mar-2026 19:56:01","24-Mar-2026 19:56:15","00:14","23","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","25-Mar-2026 ","25-Mar-2026 20:46:53","25-Mar-2026 20:47:12","00:19","24","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","26-Mar-2026 ","26-Mar-2026 20:54:13","26-Mar-2026 20:54:28","00:15","25","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","27-Mar-2026 ","27-Mar-2026 23:09:08","27-Mar-2026 23:09:26","00:18","26","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","28-Mar-2026 ","28-Mar-2026 20:02:00","28-Mar-2026 20:02:18","00:18","27","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","29-Mar-2026 ","29-Mar-2026 21:11:54","29-Mar-2026 21:12:13","00:19","28","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","30-Mar-2026 ","31-Mar-2026 11:57:02","31-Mar-2026 11:57:16","00:14","29","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","31-Mar-2026 ","01-Apr-2026 18:10:49","01-Apr-2026 18:11:06","00:17","30","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","01-Apr-2026 ","01-Apr-2026 18:11:44","01-Apr-2026 18:12:00","00:16","31","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","02-Apr-2026 ","02-Apr-2026 18:12:37","02-Apr-2026 18:12:55","00:18","32","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","03-Apr-2026 ","03-Apr-2026 21:58:59","03-Apr-2026 21:59:15","00:16","33","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","05-Apr-2026 ","05-Apr-2026 23:02:10","05-Apr-2026 23:02:29","00:19","34","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","06-Apr-2026 ","06-Apr-2026 22:14:46","06-Apr-2026 22:15:03","00:17","35","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","07-Apr-2026 ","07-Apr-2026 21:02:39","07-Apr-2026 21:02:52","00:13","36","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","08-Apr-2026 ","09-Apr-2026 21:40:54","09-Apr-2026 21:41:13","00:19","37","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","09-Apr-2026 ","09-Apr-2026 21:41:51","09-Apr-2026 21:42:08","00:17","38","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","10-Apr-2026 ","10-Apr-2026 20:05:38","10-Apr-2026 20:05:53","00:15","39","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","11-Apr-2026 ","11-Apr-2026 23:17:33","11-Apr-2026 23:17:46","00:13","40","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","12-Apr-2026 ","12-Apr-2026 18:21:45","12-Apr-2026 18:22:02","00:17","41","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","13-Apr-2026 ","13-Apr-2026 22:47:41","13-Apr-2026 22:48:01","00:20","42","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","14-Apr-2026 ","14-Apr-2026 22:06:10","14-Apr-2026 22:06:29","00:19","43","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","15-Apr-2026 ","15-Apr-2026 19:38:20","15-Apr-2026 19:38:45","00:25","44","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","16-Apr-2026 ","17-Apr-2026 18:32:51","17-Apr-2026 18:33:06","00:15","45","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","17-Apr-2026 ","17-Apr-2026 18:34:57","17-Apr-2026 18:35:14","00:17","46","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","18-Apr-2026 ","19-Apr-2026 18:11:33","19-Apr-2026 18:11:51","00:18","47","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","19-Apr-2026 ","19-Apr-2026 18:12:51","19-Apr-2026 18:13:09","00:18","48","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","20-Apr-2026 ","20-Apr-2026 21:40:01","20-Apr-2026 21:40:36","00:35","49","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","21-Apr-2026 ","21-Apr-2026 19:09:36","21-Apr-2026 19:09:49","00:13","50","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","24-Apr-2026 ","25-Apr-2026 19:44:57","25-Apr-2026 19:45:15","00:18","51","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","25-Apr-2026 ","25-Apr-2026 19:46:37","25-Apr-2026 19:47:02","00:25","52","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","26-Apr-2026 ","26-Apr-2026 21:19:11","26-Apr-2026 21:19:40","00:29","53","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","27-Apr-2026 ","28-Apr-2026 18:23:35","28-Apr-2026 18:23:51","00:16","54","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","28-Apr-2026 ","28-Apr-2026 18:25:15","28-Apr-2026 18:25:30","00:15","55","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","29-Apr-2026 ","29-Apr-2026 20:13:53","29-Apr-2026 20:14:08","00:15","56","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","30-Apr-2026 ","01-May-2026 19:10:14","01-May-2026 19:10:37","00:23","57","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","01-May-2026 ","01-May-2026 19:11:24","01-May-2026 19:11:41","00:17","58","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","02-May-2026 ","02-May-2026 18:48:16","02-May-2026 18:48:47","00:31","59","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","03-May-2026 ","03-May-2026 20:28:17","03-May-2026 20:28:34","00:17","60","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","04-May-2026 ","04-May-2026 21:19:12","04-May-2026 21:19:32","00:20","61","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","05-May-2026 ","05-May-2026 18:01:14","05-May-2026 18:01:32","00:18","62","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","06-May-2026 ","06-May-2026 19:34:21","06-May-2026 19:35:29","01:08","63","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","07-May-2026 ","07-May-2026 22:31:00","07-May-2026 22:31:21","00:21","64","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","08-May-2026 ","08-May-2026 22:36:30","08-May-2026 22:36:51","00:21","65","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","09-May-2026 ","09-May-2026 22:10:53","09-May-2026 22:11:08","00:15","66","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","10-May-2026 ","10-May-2026 20:03:46","10-May-2026 20:04:16","00:30","67","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","11-May-2026 ","11-May-2026 19:05:57","11-May-2026 19:06:14","00:17","68","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","12-May-2026 ","12-May-2026 20:13:55","12-May-2026 20:14:35","00:40","69","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","13-May-2026 ","13-May-2026 20:47:02","13-May-2026 20:47:37","00:35","70","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","14-May-2026 ","14-May-2026 20:49:31","14-May-2026 20:49:46","00:15","71","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","15-May-2026 ","15-May-2026 23:18:53","15-May-2026 23:19:24","00:31","72","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","16-May-2026 ","17-May-2026 18:08:11","17-May-2026 18:08:34","00:23","73","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","17-May-2026 ","17-May-2026 18:09:32","17-May-2026 18:10:09","00:37","74","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","18-May-2026 ","18-May-2026 21:06:45","18-May-2026 21:07:04","00:19","75","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","19-May-2026 ","19-May-2026 18:37:55","19-May-2026 18:38:08","00:13","76","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","20-May-2026 ","20-May-2026 22:15:44","20-May-2026 22:16:06","00:22","77","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","21-May-2026 ","21-May-2026 21:44:30","21-May-2026 21:44:46","00:16","78","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","22-May-2026 ","22-May-2026 18:49:41","22-May-2026 18:49:54","00:13","79","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","23-May-2026 ","23-May-2026 22:23:52","23-May-2026 22:24:37","00:45","80","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","24-May-2026 ","25-May-2026 18:07:16","25-May-2026 18:07:52","00:36","81","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","25-May-2026 ","25-May-2026 18:08:56","25-May-2026 18:09:26","00:30","82","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","26-May-2026 ","26-May-2026 21:43:50","26-May-2026 21:44:07","00:17","83","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","27-May-2026 ","27-May-2026 22:01:50","27-May-2026 22:02:46","00:56","84","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","28-May-2026 ","28-May-2026 19:31:12","28-May-2026 19:31:27","00:15","85","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","29-May-2026 ","29-May-2026 19:52:20","29-May-2026 19:52:38","00:18","86","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","30-May-2026 ","30-May-2026 18:53:09","30-May-2026 18:53:41","00:32","87","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","31-May-2026 ","31-May-2026 19:16:40","31-May-2026 19:16:55","00:15","88","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","01-Jun-2026 ","02-Jun-2026 19:05:37","02-Jun-2026 19:05:53","00:16","89","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","02-Jun-2026 ","02-Jun-2026 19:06:33","02-Jun-2026 19:06:51","00:18","90","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","03-Jun-2026 ","03-Jun-2026 22:04:49","03-Jun-2026 22:05:04","00:15","91","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","04-Jun-2026 ","04-Jun-2026 20:05:37","04-Jun-2026 20:06:01","00:24","92","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","05-Jun-2026 ","05-Jun-2026 18:05:00","05-Jun-2026 18:05:24","00:24","93","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","06-Jun-2026 ","06-Jun-2026 18:02:23","06-Jun-2026 18:02:42","00:19","94","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","07-Jun-2026 ","07-Jun-2026 22:56:44","07-Jun-2026 22:57:03","00:19","95","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","79","1","","08-Jun-2026 ","08-Jun-2026 21:10:36","08-Jun-2026 21:10:57","00:21","96","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","05-May-2026 ","05-May-2026 18:01:55","05-May-2026 18:02:09","00:14","1","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","06-May-2026 ","06-May-2026 20:29:15","06-May-2026 20:29:29","00:14","2","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","07-May-2026 ","07-May-2026 22:18:42","07-May-2026 22:19:01","00:19","3","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","08-May-2026 ","08-May-2026 22:31:03","08-May-2026 22:31:18","00:15","4","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","09-May-2026 ","09-May-2026 22:22:24","09-May-2026 22:22:43","00:19","5","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","10-May-2026 ","10-May-2026 23:12:50","10-May-2026 23:13:05","00:15","6","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","11-May-2026 ","11-May-2026 21:16:13","11-May-2026 21:16:37","00:24","7","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","12-May-2026 ","12-May-2026 22:36:19","12-May-2026 22:36:35","00:16","8","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","13-May-2026 ","13-May-2026 20:07:41","13-May-2026 20:08:02","00:21","9","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","15-May-2026 ","15-May-2026 23:23:21","15-May-2026 23:23:32","00:11","10","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","16-May-2026 ","17-May-2026 21:56:34","17-May-2026 21:56:46","00:12","11","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","17-May-2026 ","17-May-2026 21:57:29","17-May-2026 21:57:49","00:20","12","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","18-May-2026 ","18-May-2026 22:48:11","18-May-2026 22:48:25","00:14","13","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","19-May-2026 ","19-May-2026 18:38:52","19-May-2026 18:39:17","00:25","14","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","20-May-2026 ","20-May-2026 22:15:48","20-May-2026 22:16:02","00:14","15","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","21-May-2026 ","21-May-2026 19:42:51","21-May-2026 19:43:04","00:13","16","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","22-May-2026 ","22-May-2026 18:04:07","22-May-2026 18:04:26","00:19","17","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","23-May-2026 ","23-May-2026 22:26:18","23-May-2026 22:26:29","00:11","18","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","24-May-2026 ","25-May-2026 18:08:46","25-May-2026 18:09:14","00:28","19","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","25-May-2026 ","25-May-2026 18:10:28","25-May-2026 18:10:50","00:22","20","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","26-May-2026 ","26-May-2026 21:13:16","26-May-2026 21:13:33","00:17","21","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","27-May-2026 ","27-May-2026 22:03:15","27-May-2026 22:04:03","00:48","22","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","28-May-2026 ","28-May-2026 18:35:28","28-May-2026 18:35:57","00:29","23","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","29-May-2026 ","29-May-2026 23:05:33","29-May-2026 23:05:50","00:17","24","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","30-May-2026 ","30-May-2026 18:21:49","30-May-2026 18:22:05","00:16","25","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","31-May-2026 ","31-May-2026 18:08:43","31-May-2026 18:08:58","00:15","26","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","01-Jun-2026 ","01-Jun-2026 20:49:23","01-Jun-2026 20:49:46","00:23","27","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","02-Jun-2026 ","02-Jun-2026 21:14:37","02-Jun-2026 21:14:57","00:20","28","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","03-Jun-2026 ","03-Jun-2026 21:18:46","03-Jun-2026 21:19:06","00:20","29","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","04-Jun-2026 ","04-Jun-2026 20:03:56","04-Jun-2026 20:04:14","00:18","30","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","05-Jun-2026 ","05-Jun-2026 18:06:34","05-Jun-2026 18:07:13","00:39","31","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","06-Jun-2026 ","06-Jun-2026 18:04:33","06-Jun-2026 18:04:48","00:15","32","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","07-Jun-2026 ","07-Jun-2026 22:58:35","07-Jun-2026 22:58:55","00:20","33","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","08-Jun-2026 ","08-Jun-2026 19:59:04","08-Jun-2026 19:59:21","00:17","34","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","52","1","","09-Jun-2026 ","09-Jun-2026 20:51:47","09-Jun-2026 20:52:09","00:22","35","Patient","Handheld","Handheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","03-Jun-2026 ","03-Jun-2026 18:09:12","03-Jun-2026 18:10:34","01:22","1","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","04-Jun-2026 ","04-Jun-2026 18:01:20","04-Jun-2026 18:01:58","00:38","2","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","05-Jun-2026 ","05-Jun-2026 18:01:04","05-Jun-2026 18:01:28","00:24","3","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","06-Jun-2026 ","06-Jun-2026 18:07:13","06-Jun-2026 18:07:37","00:24","4","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","07-Jun-2026 ","07-Jun-2026 18:01:24","07-Jun-2026 18:01:49","00:25","5","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","08-Jun-2026 ","08-Jun-2026 19:18:48","08-Jun-2026 19:19:36","00:48","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012004","47","1","","09-Jun-2026 ","09-Jun-2026 18:01:23","09-Jun-2026 18:01:52","00:29","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","13-May-2026 ","13-May-2026 21:40:48","13-May-2026 21:41:52","01:04","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","14-May-2026 ","14-May-2026 21:39:37","14-May-2026 21:40:25","00:48","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","15-May-2026 ","15-May-2026 23:28:11","15-May-2026 23:28:26","00:15","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","16-May-2026 ","16-May-2026 22:36:22","16-May-2026 22:36:56","00:34","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","17-May-2026 ","17-May-2026 21:24:12","17-May-2026 21:24:34","00:22","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","18-May-2026 ","18-May-2026 22:07:40","18-May-2026 22:07:59","00:19","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","19-May-2026 ","19-May-2026 22:03:42","19-May-2026 22:03:55","00:13","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","20-May-2026 ","20-May-2026 23:04:59","20-May-2026 23:05:17","00:18","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","21-May-2026 ","21-May-2026 23:14:46","21-May-2026 23:15:04","00:18","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","22-May-2026 ","22-May-2026 23:02:18","22-May-2026 23:02:43","00:25","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","23-May-2026 ","23-May-2026 21:45:15","23-May-2026 21:45:28","00:13","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","24-May-2026 ","24-May-2026 22:28:16","24-May-2026 22:29:20","01:04","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","25-May-2026 ","25-May-2026 22:41:05","25-May-2026 22:48:09","07:04","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","26-May-2026 ","26-May-2026 23:09:08","26-May-2026 23:10:22","01:14","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","27-May-2026 ","27-May-2026 22:44:40","27-May-2026 22:45:03","00:23","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","28-May-2026 ","28-May-2026 23:20:33","28-May-2026 23:20:51","00:18","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","30-May-2026 ","30-May-2026 23:14:18","30-May-2026 23:14:30","00:12","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","31-May-2026 ","31-May-2026 22:46:18","31-May-2026 22:47:09","00:51","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","01-Jun-2026 ","01-Jun-2026 22:41:39","01-Jun-2026 22:42:31","00:52","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","02-Jun-2026 ","02-Jun-2026 22:46:47","02-Jun-2026 22:47:09","00:22","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","03-Jun-2026 ","03-Jun-2026 22:33:25","03-Jun-2026 22:33:46","00:21","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","04-Jun-2026 ","04-Jun-2026 23:21:32","04-Jun-2026 23:21:43","00:11","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","05-Jun-2026 ","05-Jun-2026 23:05:48","05-Jun-2026 23:06:12","00:24","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","06-Jun-2026 ","06-Jun-2026 23:01:55","06-Jun-2026 23:02:11","00:16","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","07-Jun-2026 ","07-Jun-2026 22:53:03","07-Jun-2026 22:53:18","00:15","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","30","2","","08-Jun-2026 ","08-Jun-2026 23:04:35","08-Jun-2026 23:04:46","00:11","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","14-Feb-2026 ","14-Feb-2026 18:03:39","14-Feb-2026 18:04:26","00:47","1","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","15-Feb-2026 ","15-Feb-2026 18:01:50","15-Feb-2026 18:02:24","00:34","2","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","16-Feb-2026 ","16-Feb-2026 18:01:30","16-Feb-2026 18:01:56","00:26","3","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","17-Feb-2026 ","17-Feb-2026 18:01:04","17-Feb-2026 18:01:14","00:10","4","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","18-Feb-2026 ","18-Feb-2026 18:02:17","18-Feb-2026 18:02:32","00:15","5","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","19-Feb-2026 ","19-Feb-2026 18:09:20","19-Feb-2026 18:09:50","00:30","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","20-Feb-2026 ","20-Feb-2026 18:14:47","20-Feb-2026 18:15:20","00:33","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","21-Feb-2026 ","21-Feb-2026 18:03:34","21-Feb-2026 18:03:56","00:22","8","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","22-Feb-2026 ","22-Feb-2026 20:08:20","22-Feb-2026 20:08:34","00:14","9","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","23-Feb-2026 ","23-Feb-2026 18:01:20","23-Feb-2026 18:01:41","00:21","10","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","24-Feb-2026 ","24-Feb-2026 18:01:33","24-Feb-2026 18:01:44","00:11","11","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","25-Feb-2026 ","25-Feb-2026 19:01:41","25-Feb-2026 19:01:55","00:14","12","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","26-Feb-2026 ","26-Feb-2026 18:03:34","26-Feb-2026 18:03:52","00:18","13","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","27-Feb-2026 ","27-Feb-2026 20:42:12","27-Feb-2026 20:42:21","00:09","14","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","28-Feb-2026 ","28-Feb-2026 18:01:04","28-Feb-2026 18:01:15","00:11","15","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","01-Mar-2026 ","01-Mar-2026 18:01:16","01-Mar-2026 18:01:32","00:16","16","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","02-Mar-2026 ","03-Mar-2026 05:26:34","03-Mar-2026 05:26:46","00:12","17","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","03-Mar-2026 ","03-Mar-2026 18:04:19","03-Mar-2026 18:04:31","00:12","18","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","04-Mar-2026 ","04-Mar-2026 18:01:30","04-Mar-2026 18:01:49","00:19","19","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","05-Mar-2026 ","05-Mar-2026 18:45:17","05-Mar-2026 18:45:33","00:16","20","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","06-Mar-2026 ","06-Mar-2026 18:01:03","06-Mar-2026 18:01:14","00:11","21","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","07-Mar-2026 ","07-Mar-2026 18:02:17","07-Mar-2026 18:02:32","00:15","22","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","08-Mar-2026 ","08-Mar-2026 19:22:45","08-Mar-2026 19:23:09","00:24","23","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","09-Mar-2026 ","09-Mar-2026 18:31:35","09-Mar-2026 18:31:47","00:12","24","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","10-Mar-2026 ","11-Mar-2026 04:23:18","11-Mar-2026 04:23:33","00:15","25","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","11-Mar-2026 ","11-Mar-2026 19:37:36","11-Mar-2026 19:37:46","00:10","26","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","12-Mar-2026 ","12-Mar-2026 18:38:17","12-Mar-2026 18:38:48","00:31","27","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","13-Mar-2026 ","13-Mar-2026 18:11:32","13-Mar-2026 18:12:40","01:08","28","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","14-Mar-2026 ","15-Mar-2026 18:01:21","15-Mar-2026 18:01:42","00:21","29","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","15-Mar-2026 ","15-Mar-2026 18:02:03","15-Mar-2026 18:02:15","00:12","30","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","16-Mar-2026 ","16-Mar-2026 18:01:07","16-Mar-2026 18:01:18","00:11","31","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","17-Mar-2026 ","17-Mar-2026 18:01:24","17-Mar-2026 18:01:45","00:21","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","18-Mar-2026 ","18-Mar-2026 18:01:37","18-Mar-2026 18:01:49","00:12","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","19-Mar-2026 ","19-Mar-2026 22:33:50","19-Mar-2026 22:34:02","00:12","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","20-Mar-2026 ","20-Mar-2026 18:15:17","20-Mar-2026 18:15:44","00:27","35","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","22-Mar-2026 ","23-Mar-2026 14:36:21","23-Mar-2026 14:36:42","00:21","36","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","15","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","23-Mar-2026 ","23-Mar-2026 18:01:19","23-Mar-2026 18:01:30","00:11","37","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","24-Mar-2026 ","24-Mar-2026 18:01:21","24-Mar-2026 18:01:30","00:09","38","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","25-Mar-2026 ","25-Mar-2026 18:25:02","25-Mar-2026 18:25:32","00:30","39","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","26-Mar-2026 ","26-Mar-2026 21:16:14","26-Mar-2026 21:16:57","00:43","40","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","27-Mar-2026 ","27-Mar-2026 18:01:41","27-Mar-2026 18:02:23","00:42","41","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","28-Mar-2026 ","28-Mar-2026 18:02:57","28-Mar-2026 18:03:21","00:24","42","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","29-Mar-2026 ","29-Mar-2026 19:21:39","29-Mar-2026 19:21:51","00:12","43","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","30-Mar-2026 ","30-Mar-2026 18:44:16","30-Mar-2026 18:44:28","00:12","44","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","31-Mar-2026 ","31-Mar-2026 21:37:52","31-Mar-2026 21:38:05","00:13","45","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","14","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","01-Apr-2026 ","01-Apr-2026 21:10:45","01-Apr-2026 21:11:02","00:17","46","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","12","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","02-Apr-2026 ","03-Apr-2026 18:42:35","03-Apr-2026 18:42:48","00:13","47","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","03-Apr-2026 ","03-Apr-2026 18:43:12","03-Apr-2026 18:43:21","00:09","48","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","04-Apr-2026 ","04-Apr-2026 22:55:47","04-Apr-2026 22:55:57","00:10","49","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","13","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","05-Apr-2026 ","06-Apr-2026 18:01:34","06-Apr-2026 18:01:49","00:15","50","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","06-Apr-2026 ","06-Apr-2026 18:02:13","06-Apr-2026 18:02:23","00:10","51","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","16","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","07-Apr-2026 ","07-Apr-2026 20:36:00","07-Apr-2026 20:36:10","00:10","52","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","08-Apr-2026 ","09-Apr-2026 18:12:35","09-Apr-2026 18:12:49","00:14","53","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","18","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","09-Apr-2026 ","09-Apr-2026 18:13:13","09-Apr-2026 18:13:27","00:14","54","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","18","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","10-Apr-2026 ","11-Apr-2026 18:01:29","11-Apr-2026 18:01:43","00:14","55","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","11-Apr-2026 ","11-Apr-2026 18:02:09","11-Apr-2026 18:02:19","00:10","56","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","19","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","12-Apr-2026 ","12-Apr-2026 18:01:35","12-Apr-2026 18:01:55","00:20","57","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","20","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","13-Apr-2026 ","13-Apr-2026 18:01:30","13-Apr-2026 18:01:51","00:21","58","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","22","1","Yes, I confirm this is the correct stool count","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","14-Apr-2026 ","14-Apr-2026 19:02:00","14-Apr-2026 19:02:23","00:23","59","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","15-Apr-2026 ","15-Apr-2026 18:26:38","15-Apr-2026 18:26:49","00:11","60","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","12","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","18-Apr-2026 ","19-Apr-2026 14:44:39","19-Apr-2026 14:44:50","00:11","61","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","19-Apr-2026 ","19-Apr-2026 18:01:18","19-Apr-2026 18:01:27","00:09","62","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","20-Apr-2026 ","20-Apr-2026 20:33:26","20-Apr-2026 20:33:37","00:11","63","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","21-Apr-2026 ","22-Apr-2026 21:55:40","22-Apr-2026 21:55:51","00:11","64","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","22-Apr-2026 ","22-Apr-2026 21:56:21","22-Apr-2026 21:56:30","00:09","65","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","23-Apr-2026 ","23-Apr-2026 18:46:35","23-Apr-2026 18:46:49","00:14","66","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","24-Apr-2026 ","25-Apr-2026 18:23:56","25-Apr-2026 18:24:09","00:13","67","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","25-Apr-2026 ","25-Apr-2026 18:24:36","25-Apr-2026 18:24:46","00:10","68","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","26-Apr-2026 ","26-Apr-2026 21:00:47","26-Apr-2026 21:00:56","00:09","69","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","27-Apr-2026 ","27-Apr-2026 18:22:29","27-Apr-2026 18:22:37","00:08","70","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","28-Apr-2026 ","28-Apr-2026 22:44:39","28-Apr-2026 22:44:49","00:10","71","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","29-Apr-2026 ","29-Apr-2026 22:49:50","29-Apr-2026 22:49:59","00:09","72","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","30-Apr-2026 ","30-Apr-2026 20:47:29","30-Apr-2026 20:47:37","00:08","73","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","01-May-2026 ","01-May-2026 21:43:23","01-May-2026 21:43:33","00:10","74","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","02-May-2026 ","02-May-2026 21:07:46","02-May-2026 21:07:56","00:10","75","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","03-May-2026 ","03-May-2026 21:43:35","03-May-2026 21:43:47","00:12","76","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","05-May-2026 ","06-May-2026 18:20:31","06-May-2026 18:20:43","00:12","77","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","06-May-2026 ","06-May-2026 18:21:12","06-May-2026 18:21:22","00:10","78","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","07-May-2026 ","07-May-2026 20:37:45","07-May-2026 20:38:13","00:28","79","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","08-May-2026 ","08-May-2026 18:31:28","08-May-2026 18:31:38","00:10","80","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","09-May-2026 ","09-May-2026 21:38:50","09-May-2026 21:38:58","00:08","81","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","10-May-2026 ","10-May-2026 22:01:51","10-May-2026 22:02:04","00:13","82","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","11-May-2026 ","11-May-2026 22:42:31","11-May-2026 22:42:39","00:08","83","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","12-May-2026 ","12-May-2026 19:03:36","12-May-2026 19:03:48","00:12","84","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","13-May-2026 ","13-May-2026 20:59:19","13-May-2026 20:59:30","00:11","85","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","14-May-2026 ","14-May-2026 20:18:42","14-May-2026 20:18:54","00:12","86","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","15-May-2026 ","15-May-2026 20:48:11","15-May-2026 20:48:21","00:10","87","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","16-May-2026 ","17-May-2026 19:32:54","17-May-2026 19:33:03","00:09","88","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","17-May-2026 ","17-May-2026 19:33:35","17-May-2026 19:33:43","00:08","89","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","18-May-2026 ","18-May-2026 20:55:22","18-May-2026 20:55:31","00:09","90","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","19-May-2026 ","19-May-2026 22:00:42","19-May-2026 22:00:50","00:08","91","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","20-May-2026 ","20-May-2026 19:56:20","20-May-2026 19:56:35","00:15","92","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","21-May-2026 ","21-May-2026 20:02:23","21-May-2026 20:02:32","00:09","93","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","22-May-2026 ","22-May-2026 22:00:12","22-May-2026 22:00:25","00:13","94","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","23-May-2026 ","23-May-2026 22:09:23","23-May-2026 22:09:31","00:08","95","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","24-May-2026 ","25-May-2026 19:37:07","25-May-2026 19:37:19","00:12","96","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","25-May-2026 ","25-May-2026 19:37:51","25-May-2026 19:38:01","00:10","97","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","26-May-2026 ","26-May-2026 19:50:53","26-May-2026 19:51:00","00:07","98","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","27-May-2026 ","27-May-2026 21:24:04","27-May-2026 21:24:12","00:08","99","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","28-May-2026 ","29-May-2026 20:53:24","29-May-2026 20:53:44","00:20","100","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","29-May-2026 ","29-May-2026 20:54:07","29-May-2026 20:54:17","00:10","101","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","30-May-2026 ","30-May-2026 22:07:38","30-May-2026 22:07:46","00:08","102","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","31-May-2026 ","31-May-2026 20:02:26","31-May-2026 20:02:34","00:08","103","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","02-Jun-2026 ","03-Jun-2026 10:53:02","03-Jun-2026 10:53:13","00:11","104","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","03-Jun-2026 ","03-Jun-2026 18:50:49","03-Jun-2026 18:50:56","00:07","105","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","04-Jun-2026 ","04-Jun-2026 19:07:13","04-Jun-2026 19:07:21","00:08","106","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","05-Jun-2026 ","05-Jun-2026 18:01:36","05-Jun-2026 18:01:44","00:08","107","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","06-Jun-2026 ","06-Jun-2026 18:01:19","06-Jun-2026 18:01:33","00:14","108","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","07-Jun-2026 ","07-Jun-2026 18:10:45","07-Jun-2026 18:11:13","00:28","109","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","19","1","","08-Jun-2026 ","08-Jun-2026 19:11:54","08-Jun-2026 19:12:05","00:11","110","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","20-Apr-2026 ","20-Apr-2026 21:15:38","20-Apr-2026 21:16:41","01:03","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","21-Apr-2026 ","21-Apr-2026 18:03:25","21-Apr-2026 18:04:56","01:31","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","22-Apr-2026 ","22-Apr-2026 19:12:37","22-Apr-2026 19:13:09","00:32","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","23-Apr-2026 ","23-Apr-2026 20:30:34","23-Apr-2026 20:31:02","00:28","4","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","24-Apr-2026 ","24-Apr-2026 20:09:04","24-Apr-2026 20:09:46","00:42","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","25-Apr-2026 ","25-Apr-2026 20:06:32","25-Apr-2026 20:07:07","00:35","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","26-Apr-2026 ","26-Apr-2026 21:59:32","26-Apr-2026 22:00:01","00:29","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","27-Apr-2026 ","27-Apr-2026 19:38:17","27-Apr-2026 19:38:44","00:27","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","28-Apr-2026 ","28-Apr-2026 23:29:55","28-Apr-2026 23:30:19","00:24","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","29-Apr-2026 ","29-Apr-2026 19:44:25","29-Apr-2026 19:44:49","00:24","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","30-Apr-2026 ","30-Apr-2026 18:03:54","30-Apr-2026 18:04:24","00:30","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","01-May-2026 ","01-May-2026 21:14:03","01-May-2026 21:14:43","00:40","12","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","02-May-2026 ","02-May-2026 18:01:20","02-May-2026 18:01:55","00:35","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","03-May-2026 ","03-May-2026 23:02:06","03-May-2026 23:02:39","00:33","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","04-May-2026 ","04-May-2026 22:02:36","04-May-2026 22:03:10","00:34","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","05-May-2026 ","05-May-2026 20:28:15","05-May-2026 20:29:13","00:58","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","06-May-2026 ","06-May-2026 18:07:07","06-May-2026 18:08:40","01:33","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","07-May-2026 ","07-May-2026 21:39:42","07-May-2026 21:40:09","00:27","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","08-May-2026 ","08-May-2026 19:18:31","08-May-2026 19:19:10","00:39","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","09-May-2026 ","09-May-2026 22:03:28","09-May-2026 22:04:07","00:39","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","10-May-2026 ","10-May-2026 21:59:21","10-May-2026 22:00:01","00:40","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","11-May-2026 ","11-May-2026 20:50:01","11-May-2026 20:50:22","00:21","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","12-May-2026 ","12-May-2026 22:35:56","12-May-2026 22:36:50","00:54","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","13-May-2026 ","13-May-2026 21:41:50","13-May-2026 21:42:19","00:29","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","14-May-2026 ","14-May-2026 21:16:25","14-May-2026 21:17:02","00:37","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","15-May-2026 ","15-May-2026 21:52:56","15-May-2026 21:53:22","00:26","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","16-May-2026 ","16-May-2026 18:31:25","16-May-2026 18:31:51","00:26","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","17-May-2026 ","17-May-2026 20:47:42","17-May-2026 20:48:00","00:18","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","18-May-2026 ","19-May-2026 06:20:57","19-May-2026 06:21:20","00:23","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","19-May-2026 ","19-May-2026 20:42:32","19-May-2026 20:42:58","00:26","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","20-May-2026 ","21-May-2026 06:15:07","21-May-2026 06:15:24","00:17","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","21-May-2026 ","21-May-2026 21:38:28","21-May-2026 21:39:00","00:32","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","22-May-2026 ","22-May-2026 22:14:30","22-May-2026 22:14:59","00:29","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","23-May-2026 ","23-May-2026 22:46:03","23-May-2026 22:46:25","00:22","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","24-May-2026 ","25-May-2026 07:10:54","25-May-2026 07:11:24","00:30","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","25-May-2026 ","25-May-2026 19:21:24","25-May-2026 19:21:48","00:24","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","26-May-2026 ","26-May-2026 20:04:29","26-May-2026 20:05:07","00:38","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","28-May-2026 ","29-May-2026 12:32:12","29-May-2026 12:32:32","00:20","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","29-May-2026 ","29-May-2026 20:10:11","29-May-2026 20:10:30","00:19","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","30-May-2026 ","31-May-2026 00:20:30","31-May-2026 00:20:55","00:25","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","31-May-2026 ","01-Jun-2026 06:36:31","01-Jun-2026 06:36:58","00:27","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","01-Jun-2026 ","02-Jun-2026 01:57:42","02-Jun-2026 01:58:05","00:23","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","02-Jun-2026 ","02-Jun-2026 18:01:25","02-Jun-2026 18:01:58","00:33","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","03-Jun-2026 ","03-Jun-2026 20:16:54","03-Jun-2026 20:17:14","00:20","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","04-Jun-2026 ","05-Jun-2026 04:51:56","05-Jun-2026 04:52:23","00:27","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","05-Jun-2026 ","05-Jun-2026 19:24:11","05-Jun-2026 19:24:31","00:20","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","06-Jun-2026 ","06-Jun-2026 22:24:11","06-Jun-2026 22:24:27","00:16","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","07-Jun-2026 ","07-Jun-2026 22:42:45","07-Jun-2026 22:43:07","00:22","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","08-Jun-2026 ","08-Jun-2026 19:51:38","08-Jun-2026 19:51:53","00:15","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","49","1","","09-Jun-2026 ","09-Jun-2026 20:08:09","09-Jun-2026 20:08:31","00:22","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","31-Mar-2026 ","31-Mar-2026 18:29:57","31-Mar-2026 18:47:37","17:40","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","01-Apr-2026 ","01-Apr-2026 18:31:44","01-Apr-2026 18:33:40","01:56","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","02-Apr-2026 ","02-Apr-2026 18:36:48","02-Apr-2026 18:37:55","01:07","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","03-Apr-2026 ","03-Apr-2026 18:33:32","03-Apr-2026 18:34:22","00:50","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","04-Apr-2026 ","04-Apr-2026 18:34:52","04-Apr-2026 18:35:36","00:44","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","05-Apr-2026 ","05-Apr-2026 19:08:11","05-Apr-2026 19:08:38","00:27","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","06-Apr-2026 ","06-Apr-2026 18:33:36","06-Apr-2026 18:34:13","00:37","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","07-Apr-2026 ","07-Apr-2026 18:47:34","07-Apr-2026 18:48:10","00:36","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","08-Apr-2026 ","09-Apr-2026 18:23:22","09-Apr-2026 18:24:09","00:47","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","09-Apr-2026 ","09-Apr-2026 18:24:36","09-Apr-2026 18:25:06","00:30","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","10-Apr-2026 ","10-Apr-2026 18:01:29","10-Apr-2026 18:02:12","00:43","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","11-Apr-2026 ","11-Apr-2026 18:17:12","11-Apr-2026 18:18:02","00:50","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","12-Apr-2026 ","12-Apr-2026 18:04:08","12-Apr-2026 18:05:08","01:00","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","13-Apr-2026 ","13-Apr-2026 18:29:53","13-Apr-2026 18:30:18","00:25","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","14-Apr-2026 ","14-Apr-2026 18:16:55","14-Apr-2026 18:17:17","00:22","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","15-Apr-2026 ","15-Apr-2026 18:11:38","15-Apr-2026 18:12:08","00:30","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","16-Apr-2026 ","16-Apr-2026 18:09:46","16-Apr-2026 18:10:31","00:45","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","17-Apr-2026 ","17-Apr-2026 18:42:09","17-Apr-2026 18:43:13","01:04","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","18-Apr-2026 ","18-Apr-2026 18:15:17","18-Apr-2026 18:16:21","01:04","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","19-Apr-2026 ","19-Apr-2026 19:05:45","19-Apr-2026 19:06:13","00:28","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","20-Apr-2026 ","20-Apr-2026 18:27:20","20-Apr-2026 18:28:23","01:03","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","21-Apr-2026 ","21-Apr-2026 18:19:48","21-Apr-2026 18:20:25","00:37","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","22-Apr-2026 ","22-Apr-2026 18:12:05","22-Apr-2026 18:12:42","00:37","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","23-Apr-2026 ","23-Apr-2026 18:05:12","23-Apr-2026 18:05:40","00:28","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","24-Apr-2026 ","24-Apr-2026 18:15:58","24-Apr-2026 18:16:23","00:25","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","25-Apr-2026 ","25-Apr-2026 18:27:21","25-Apr-2026 18:27:43","00:22","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","26-Apr-2026 ","26-Apr-2026 19:37:21","26-Apr-2026 19:37:44","00:23","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","27-Apr-2026 ","27-Apr-2026 18:32:55","27-Apr-2026 18:33:18","00:23","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","28-Apr-2026 ","28-Apr-2026 18:12:37","28-Apr-2026 18:13:06","00:29","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","29-Apr-2026 ","29-Apr-2026 18:22:57","29-Apr-2026 18:23:18","00:21","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","30-Apr-2026 ","30-Apr-2026 18:05:56","30-Apr-2026 18:06:38","00:42","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","01-May-2026 ","01-May-2026 18:06:30","01-May-2026 18:06:53","00:23","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","02-May-2026 ","02-May-2026 18:06:44","02-May-2026 18:07:10","00:26","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","03-May-2026 ","03-May-2026 18:20:14","03-May-2026 18:20:40","00:26","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","04-May-2026 ","04-May-2026 18:10:14","04-May-2026 18:10:39","00:25","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","05-May-2026 ","05-May-2026 18:13:52","05-May-2026 18:14:14","00:22","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","06-May-2026 ","06-May-2026 18:27:25","06-May-2026 18:27:43","00:18","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","07-May-2026 ","07-May-2026 18:16:09","07-May-2026 18:16:34","00:25","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","08-May-2026 ","08-May-2026 18:05:30","08-May-2026 18:05:50","00:20","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","09-May-2026 ","09-May-2026 18:03:43","09-May-2026 18:04:01","00:18","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","10-May-2026 ","10-May-2026 18:13:20","10-May-2026 18:13:42","00:22","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","11-May-2026 ","11-May-2026 18:02:26","11-May-2026 18:02:54","00:28","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","12-May-2026 ","12-May-2026 18:16:11","12-May-2026 18:16:34","00:23","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","13-May-2026 ","13-May-2026 18:13:23","13-May-2026 18:13:42","00:19","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","14-May-2026 ","14-May-2026 18:18:23","14-May-2026 18:18:56","00:33","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","15-May-2026 ","15-May-2026 18:05:23","15-May-2026 18:05:47","00:24","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","16-May-2026 ","16-May-2026 19:15:24","16-May-2026 19:15:50","00:26","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","17-May-2026 ","17-May-2026 18:42:31","17-May-2026 18:42:46","00:15","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","18-May-2026 ","18-May-2026 18:07:10","18-May-2026 18:07:31","00:21","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","19-May-2026 ","19-May-2026 18:08:00","19-May-2026 18:08:22","00:22","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","20-May-2026 ","20-May-2026 18:46:47","20-May-2026 18:47:10","00:23","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","21-May-2026 ","21-May-2026 18:25:08","21-May-2026 18:25:33","00:25","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","22-May-2026 ","22-May-2026 18:10:28","22-May-2026 18:10:57","00:29","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","23-May-2026 ","23-May-2026 18:49:12","23-May-2026 18:49:37","00:25","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","24-May-2026 ","24-May-2026 18:24:57","24-May-2026 18:25:19","00:22","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","25-May-2026 ","25-May-2026 18:59:42","25-May-2026 19:00:03","00:21","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","26-May-2026 ","26-May-2026 19:02:50","26-May-2026 19:03:13","00:23","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","27-May-2026 ","27-May-2026 18:16:05","27-May-2026 18:16:24","00:19","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","28-May-2026 ","28-May-2026 18:12:59","28-May-2026 18:13:22","00:23","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","29-May-2026 ","29-May-2026 18:14:55","29-May-2026 18:15:12","00:17","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","30-May-2026 ","30-May-2026 18:15:22","30-May-2026 18:15:41","00:19","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","31-May-2026 ","31-May-2026 18:10:53","31-May-2026 18:11:09","00:16","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","01-Jun-2026 ","01-Jun-2026 18:10:37","01-Jun-2026 18:11:05","00:28","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","02-Jun-2026 ","02-Jun-2026 21:17:02","02-Jun-2026 21:17:20","00:18","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","03-Jun-2026 ","03-Jun-2026 18:03:12","03-Jun-2026 18:03:28","00:16","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","04-Jun-2026 ","04-Jun-2026 18:03:16","04-Jun-2026 18:03:40","00:24","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","05-Jun-2026 ","05-Jun-2026 19:02:22","05-Jun-2026 19:02:43","00:21","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","06-Jun-2026 ","06-Jun-2026 21:05:43","06-Jun-2026 21:05:58","00:15","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","07-Jun-2026 ","07-Jun-2026 18:02:51","07-Jun-2026 18:03:05","00:14","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","08-Jun-2026 ","08-Jun-2026 18:06:52","08-Jun-2026 18:07:10","00:18","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","49","1","","09-Jun-2026 ","09-Jun-2026 18:08:15","09-Jun-2026 18:08:43","00:28","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","16-Apr-2026 ","16-Apr-2026 18:04:28","16-Apr-2026 18:06:48","02:20","1","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","17-Apr-2026 ","17-Apr-2026 21:22:55","17-Apr-2026 21:25:27","02:32","2","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","18-Apr-2026 ","19-Apr-2026 20:05:25","19-Apr-2026 20:07:39","02:14","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","19-Apr-2026 ","19-Apr-2026 20:08:07","19-Apr-2026 20:12:24","04:17","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","20-Apr-2026 ","21-Apr-2026 21:33:43","21-Apr-2026 21:36:30","02:47","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","21-Apr-2026 ","21-Apr-2026 21:37:06","21-Apr-2026 21:38:37","01:31","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","22-Apr-2026 ","23-Apr-2026 22:28:26","23-Apr-2026 22:30:27","02:01","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","23-Apr-2026 ","23-Apr-2026 22:31:08","23-Apr-2026 22:32:58","01:50","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","24-Apr-2026 ","25-Apr-2026 19:10:46","25-Apr-2026 19:13:10","02:24","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","25-Apr-2026 ","25-Apr-2026 19:13:39","25-Apr-2026 19:14:21","00:42","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","26-Apr-2026 ","26-Apr-2026 20:00:34","26-Apr-2026 20:01:48","01:14","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","27-Apr-2026 ","27-Apr-2026 20:59:09","27-Apr-2026 21:03:03","03:54","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","28-Apr-2026 ","29-Apr-2026 12:06:59","29-Apr-2026 12:09:04","02:05","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","29-Apr-2026 ","30-Apr-2026 20:47:12","30-Apr-2026 20:48:25","01:13","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","30-Apr-2026 ","30-Apr-2026 20:48:54","30-Apr-2026 20:49:27","00:33","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","01-May-2026 ","01-May-2026 23:05:43","01-May-2026 23:08:41","02:58","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","02-May-2026 ","02-May-2026 21:59:39","02-May-2026 22:00:07","00:28","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","03-May-2026 ","03-May-2026 20:48:50","03-May-2026 20:49:39","00:49","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","04-May-2026 ","04-May-2026 23:06:12","04-May-2026 23:07:05","00:53","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","05-May-2026 ","05-May-2026 22:34:33","05-May-2026 22:36:46","02:13","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","06-May-2026 ","07-May-2026 20:20:36","07-May-2026 20:21:23","00:47","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","07-May-2026 ","07-May-2026 20:21:51","07-May-2026 20:22:22","00:31","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","09-May-2026 ","10-May-2026 09:56:26","10-May-2026 09:57:46","01:20","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","12-May-2026 ","12-May-2026 23:17:00","12-May-2026 23:17:43","00:43","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","13-May-2026 ","13-May-2026 19:25:14","13-May-2026 19:26:42","01:28","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","0","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","14-May-2026 ","14-May-2026 18:38:36","14-May-2026 18:39:02","00:26","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","15-May-2026 ","15-May-2026 22:58:22","15-May-2026 22:59:14","00:52","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","16-May-2026 ","16-May-2026 23:23:13","16-May-2026 23:23:30","00:17","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","17-May-2026 ","18-May-2026 12:46:24","18-May-2026 12:47:00","00:36","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092002","55","1","","18-May-2026 ","18-May-2026 23:24:01","18-May-2026 23:26:28","02:27","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","18-Mar-2026 ","18-Mar-2026 18:17:26","18-Mar-2026 18:18:51","01:25","1","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","19-Mar-2026 ","19-Mar-2026 18:01:22","19-Mar-2026 18:02:22","01:00","2","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","20-Mar-2026 ","20-Mar-2026 18:01:14","20-Mar-2026 18:02:17","01:03","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","21-Mar-2026 ","21-Mar-2026 18:28:25","21-Mar-2026 18:28:40","00:15","4","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","22-Mar-2026 ","22-Mar-2026 18:01:58","22-Mar-2026 18:02:15","00:17","5","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","23-Mar-2026 ","24-Mar-2026 00:33:23","24-Mar-2026 00:33:51","00:28","6","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","24-Mar-2026 ","24-Mar-2026 18:01:20","24-Mar-2026 18:01:42","00:22","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","25-Mar-2026 ","25-Mar-2026 18:05:59","25-Mar-2026 18:06:09","00:10","8","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","26-Mar-2026 ","26-Mar-2026 18:57:42","26-Mar-2026 18:58:00","00:18","9","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","27-Mar-2026 ","27-Mar-2026 18:05:26","27-Mar-2026 18:05:44","00:18","10","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","28-Mar-2026 ","28-Mar-2026 18:34:55","28-Mar-2026 18:35:13","00:18","11","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","29-Mar-2026 ","29-Mar-2026 18:01:26","29-Mar-2026 18:01:40","00:14","12","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","30-Mar-2026 ","30-Mar-2026 18:15:14","30-Mar-2026 18:15:34","00:20","13","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","31-Mar-2026 ","31-Mar-2026 18:03:24","31-Mar-2026 18:03:43","00:19","14","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","01-Apr-2026 ","01-Apr-2026 18:19:46","01-Apr-2026 18:20:00","00:14","15","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","02-Apr-2026 ","02-Apr-2026 19:54:55","02-Apr-2026 19:55:05","00:10","16","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","03-Apr-2026 ","03-Apr-2026 22:16:32","03-Apr-2026 22:16:45","00:13","17","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","04-Apr-2026 ","04-Apr-2026 18:04:41","04-Apr-2026 18:05:01","00:20","18","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","05-Apr-2026 ","05-Apr-2026 22:47:52","05-Apr-2026 22:48:02","00:10","19","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","06-Apr-2026 ","07-Apr-2026 06:06:47","07-Apr-2026 06:07:10","00:23","20","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","07-Apr-2026 ","07-Apr-2026 18:10:33","07-Apr-2026 18:11:05","00:32","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","08-Apr-2026 ","09-Apr-2026 21:11:31","09-Apr-2026 21:12:00","00:29","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","09-Apr-2026 ","09-Apr-2026 21:12:19","09-Apr-2026 21:12:49","00:30","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","10-Apr-2026 ","10-Apr-2026 18:04:26","10-Apr-2026 18:04:42","00:16","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","11-Apr-2026 ","11-Apr-2026 19:27:38","11-Apr-2026 19:27:54","00:16","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","12-Apr-2026 ","12-Apr-2026 18:01:17","12-Apr-2026 18:01:29","00:12","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","13-Apr-2026 ","14-Apr-2026 05:43:24","14-Apr-2026 05:43:41","00:17","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","14-Apr-2026 ","14-Apr-2026 18:23:04","14-Apr-2026 18:23:16","00:12","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","15-Apr-2026 ","15-Apr-2026 18:01:41","15-Apr-2026 18:01:50","00:09","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","16-Apr-2026 ","16-Apr-2026 18:20:20","16-Apr-2026 18:20:33","00:13","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","17-Apr-2026 ","17-Apr-2026 18:30:28","17-Apr-2026 18:30:46","00:18","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","18-Apr-2026 ","18-Apr-2026 22:43:21","18-Apr-2026 22:43:31","00:10","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","19-Apr-2026 ","19-Apr-2026 18:20:54","19-Apr-2026 18:21:17","00:23","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","20-Apr-2026 ","21-Apr-2026 06:17:37","21-Apr-2026 06:18:03","00:26","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","21-Apr-2026 ","21-Apr-2026 18:01:02","21-Apr-2026 18:01:33","00:31","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","22-Apr-2026 ","22-Apr-2026 19:26:08","22-Apr-2026 19:26:32","00:24","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","23-Apr-2026 ","23-Apr-2026 18:11:27","23-Apr-2026 18:11:39","00:12","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","24-Apr-2026 ","24-Apr-2026 21:33:17","24-Apr-2026 21:33:38","00:21","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","25-Apr-2026 ","25-Apr-2026 21:50:02","25-Apr-2026 21:50:24","00:22","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","26-Apr-2026 ","26-Apr-2026 19:05:52","26-Apr-2026 19:06:03","00:11","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","27-Apr-2026 ","27-Apr-2026 18:39:18","27-Apr-2026 18:39:30","00:12","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","28-Apr-2026 ","28-Apr-2026 18:31:50","28-Apr-2026 18:32:10","00:20","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","29-Apr-2026 ","29-Apr-2026 18:46:29","29-Apr-2026 18:46:58","00:29","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","30-Apr-2026 ","30-Apr-2026 18:14:43","30-Apr-2026 18:15:07","00:24","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","01-May-2026 ","01-May-2026 22:22:43","01-May-2026 22:22:57","00:14","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","02-May-2026 ","02-May-2026 18:04:04","02-May-2026 18:04:16","00:12","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","03-May-2026 ","03-May-2026 18:02:06","03-May-2026 18:03:05","00:59","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","04-May-2026 ","04-May-2026 18:33:13","04-May-2026 18:33:26","00:13","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","05-May-2026 ","05-May-2026 19:02:55","05-May-2026 19:03:06","00:11","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","06-May-2026 ","06-May-2026 18:03:45","06-May-2026 18:04:11","00:26","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","07-May-2026 ","07-May-2026 21:26:53","07-May-2026 21:27:10","00:17","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","08-May-2026 ","08-May-2026 18:36:10","08-May-2026 18:36:31","00:21","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","09-May-2026 ","10-May-2026 19:42:33","10-May-2026 19:42:51","00:18","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","10-May-2026 ","10-May-2026 19:43:17","10-May-2026 19:43:27","00:10","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","11-May-2026 ","12-May-2026 06:36:35","12-May-2026 06:36:58","00:23","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","12-May-2026 ","13-May-2026 05:13:59","13-May-2026 05:14:14","00:15","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","13-May-2026 ","13-May-2026 18:05:45","13-May-2026 18:05:57","00:12","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","14-May-2026 ","14-May-2026 18:41:45","14-May-2026 18:41:59","00:14","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","15-May-2026 ","15-May-2026 19:46:08","15-May-2026 19:46:37","00:29","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","16-May-2026 ","16-May-2026 19:28:50","16-May-2026 19:29:04","00:14","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","17-May-2026 ","17-May-2026 19:42:34","17-May-2026 19:42:45","00:11","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","18-May-2026 ","18-May-2026 18:05:59","18-May-2026 18:06:14","00:15","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","19-May-2026 ","19-May-2026 19:20:23","19-May-2026 19:20:39","00:16","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","20-May-2026 ","20-May-2026 18:44:46","20-May-2026 18:44:58","00:12","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","21-May-2026 ","21-May-2026 18:26:11","21-May-2026 18:26:22","00:11","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","22-May-2026 ","22-May-2026 18:00:39","22-May-2026 18:01:09","00:30","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","23-May-2026 ","23-May-2026 19:15:06","23-May-2026 19:15:15","00:09","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","24-May-2026 ","24-May-2026 18:08:48","24-May-2026 18:09:01","00:13","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","25-May-2026 ","25-May-2026 21:52:52","25-May-2026 21:53:04","00:12","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","26-May-2026 ","26-May-2026 19:41:29","26-May-2026 19:41:42","00:13","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","27-May-2026 ","27-May-2026 19:08:12","27-May-2026 19:08:25","00:13","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","28-May-2026 ","28-May-2026 19:53:30","28-May-2026 19:53:47","00:17","72","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","29-May-2026 ","29-May-2026 19:03:08","29-May-2026 19:03:25","00:17","73","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","30-May-2026 ","30-May-2026 18:28:54","30-May-2026 18:29:12","00:18","74","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","31-May-2026 ","31-May-2026 20:52:43","31-May-2026 20:52:56","00:13","75","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","01-Jun-2026 ","01-Jun-2026 18:01:44","01-Jun-2026 18:01:55","00:11","76","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","02-Jun-2026 ","02-Jun-2026 19:23:05","02-Jun-2026 19:23:18","00:13","77","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","03-Jun-2026 ","03-Jun-2026 20:16:32","03-Jun-2026 20:16:45","00:13","78","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","04-Jun-2026 ","04-Jun-2026 19:01:12","04-Jun-2026 19:01:34","00:22","79","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","05-Jun-2026 ","05-Jun-2026 18:39:19","05-Jun-2026 18:39:33","00:14","80","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","06-Jun-2026 ","06-Jun-2026 19:12:33","06-Jun-2026 19:14:55","02:22","81","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","07-Jun-2026 ","07-Jun-2026 18:23:14","07-Jun-2026 18:23:33","00:19","82","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","08-Jun-2026 ","08-Jun-2026 18:04:33","08-Jun-2026 18:04:43","00:10","83","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","22","5","Yes, I confirm this is the correct stool count.","09-Jun-2026 ","09-Jun-2026 18:07:36","09-Jun-2026 18:08:18","00:42","84","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","27-Feb-2026 ","27-Feb-2026 23:03:44","27-Feb-2026 23:05:27","01:43","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","28-Feb-2026 ","28-Feb-2026 22:00:47","28-Feb-2026 22:01:40","00:53","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","01-Mar-2026 ","01-Mar-2026 23:09:45","01-Mar-2026 23:10:07","00:22","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","02-Mar-2026 ","03-Mar-2026 21:17:31","03-Mar-2026 21:18:07","00:36","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","03-Mar-2026 ","03-Mar-2026 21:18:25","03-Mar-2026 21:18:51","00:26","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","04-Mar-2026 ","04-Mar-2026 21:07:08","04-Mar-2026 21:07:32","00:24","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","05-Mar-2026 ","05-Mar-2026 21:46:49","05-Mar-2026 21:47:07","00:18","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","06-Mar-2026 ","06-Mar-2026 20:40:38","06-Mar-2026 20:40:58","00:20","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","07-Mar-2026 ","07-Mar-2026 21:42:08","07-Mar-2026 21:42:22","00:14","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","08-Mar-2026 ","08-Mar-2026 21:03:19","08-Mar-2026 21:04:22","01:03","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","09-Mar-2026 ","09-Mar-2026 19:46:43","09-Mar-2026 19:47:09","00:26","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","10-Mar-2026 ","10-Mar-2026 20:21:28","10-Mar-2026 20:21:44","00:16","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","11-Mar-2026 ","11-Mar-2026 19:06:20","11-Mar-2026 19:06:34","00:14","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","12-Mar-2026 ","12-Mar-2026 21:14:54","12-Mar-2026 21:15:33","00:39","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","13-Mar-2026 ","13-Mar-2026 20:57:55","13-Mar-2026 20:58:17","00:22","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","14-Mar-2026 ","15-Mar-2026 07:17:23","15-Mar-2026 07:18:13","00:50","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","15-Mar-2026 ","15-Mar-2026 20:10:10","15-Mar-2026 20:10:42","00:32","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","16-Mar-2026 ","16-Mar-2026 22:25:52","16-Mar-2026 22:26:14","00:22","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","17-Mar-2026 ","17-Mar-2026 22:09:11","17-Mar-2026 22:09:28","00:17","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","18-Mar-2026 ","18-Mar-2026 20:38:45","18-Mar-2026 20:39:02","00:17","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","19-Mar-2026 ","19-Mar-2026 20:16:41","19-Mar-2026 20:16:54","00:13","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","20-Mar-2026 ","20-Mar-2026 22:41:13","20-Mar-2026 22:41:30","00:17","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","21-Mar-2026 ","21-Mar-2026 21:18:07","21-Mar-2026 21:19:04","00:57","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","22-Mar-2026 ","22-Mar-2026 22:15:27","22-Mar-2026 22:15:47","00:20","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","23-Mar-2026 ","23-Mar-2026 20:42:59","23-Mar-2026 20:43:09","00:10","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","24-Mar-2026 ","24-Mar-2026 20:00:24","24-Mar-2026 20:01:00","00:36","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","25-Mar-2026 ","25-Mar-2026 20:21:59","25-Mar-2026 20:22:17","00:18","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","26-Mar-2026 ","26-Mar-2026 21:44:02","26-Mar-2026 21:44:15","00:13","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","27-Mar-2026 ","28-Mar-2026 01:49:30","28-Mar-2026 01:49:52","00:22","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","28-Mar-2026 ","28-Mar-2026 20:43:13","28-Mar-2026 20:43:29","00:16","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","29-Mar-2026 ","29-Mar-2026 22:28:00","29-Mar-2026 22:28:35","00:35","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","30-Mar-2026 ","30-Mar-2026 21:46:17","30-Mar-2026 21:46:29","00:12","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","31-Mar-2026 ","31-Mar-2026 20:40:46","31-Mar-2026 20:40:58","00:12","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","01-Apr-2026 ","01-Apr-2026 21:24:04","01-Apr-2026 21:24:19","00:15","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","02-Apr-2026 ","02-Apr-2026 21:40:44","02-Apr-2026 21:40:59","00:15","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","03-Apr-2026 ","03-Apr-2026 22:39:57","03-Apr-2026 22:40:09","00:12","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","04-Apr-2026 ","04-Apr-2026 22:20:46","04-Apr-2026 22:21:00","00:14","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","05-Apr-2026 ","05-Apr-2026 22:23:58","05-Apr-2026 22:24:11","00:13","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","06-Apr-2026 ","06-Apr-2026 22:45:16","06-Apr-2026 22:45:36","00:20","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","07-Apr-2026 ","07-Apr-2026 23:22:12","07-Apr-2026 23:22:24","00:12","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","08-Apr-2026 ","09-Apr-2026 18:14:46","09-Apr-2026 18:14:59","00:13","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","09-Apr-2026 ","09-Apr-2026 18:15:24","09-Apr-2026 18:15:34","00:10","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","10-Apr-2026 ","10-Apr-2026 22:19:01","10-Apr-2026 22:19:17","00:16","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","11-Apr-2026 ","11-Apr-2026 22:52:56","11-Apr-2026 22:53:14","00:18","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","12-Apr-2026 ","12-Apr-2026 21:27:59","12-Apr-2026 21:28:08","00:09","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","13-Apr-2026 ","14-Apr-2026 00:16:06","14-Apr-2026 00:16:21","00:15","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","14-Apr-2026 ","14-Apr-2026 21:23:53","14-Apr-2026 21:24:03","00:10","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","15-Apr-2026 ","15-Apr-2026 18:45:29","15-Apr-2026 18:45:37","00:08","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","16-Apr-2026 ","16-Apr-2026 21:46:45","16-Apr-2026 21:46:56","00:11","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","17-Apr-2026 ","17-Apr-2026 21:47:44","17-Apr-2026 21:47:55","00:11","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","18-Apr-2026 ","18-Apr-2026 21:08:02","18-Apr-2026 21:08:16","00:14","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","19-Apr-2026 ","19-Apr-2026 21:06:45","19-Apr-2026 21:06:58","00:13","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","20-Apr-2026 ","20-Apr-2026 21:23:26","20-Apr-2026 21:23:36","00:10","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","21-Apr-2026 ","21-Apr-2026 21:56:38","21-Apr-2026 21:56:58","00:20","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","22-Apr-2026 ","22-Apr-2026 21:18:10","22-Apr-2026 21:18:20","00:10","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","23-Apr-2026 ","23-Apr-2026 21:46:41","23-Apr-2026 21:46:54","00:13","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","24-Apr-2026 ","24-Apr-2026 21:59:32","24-Apr-2026 21:59:56","00:24","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","25-Apr-2026 ","25-Apr-2026 22:34:04","25-Apr-2026 22:34:16","00:12","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","26-Apr-2026 ","26-Apr-2026 18:32:29","26-Apr-2026 18:32:39","00:10","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","27-Apr-2026 ","27-Apr-2026 20:48:50","27-Apr-2026 20:49:05","00:15","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","28-Apr-2026 ","28-Apr-2026 21:33:40","28-Apr-2026 21:33:56","00:16","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","29-Apr-2026 ","29-Apr-2026 22:24:17","29-Apr-2026 22:24:31","00:14","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","30-Apr-2026 ","30-Apr-2026 20:44:59","30-Apr-2026 20:45:10","00:11","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","01-May-2026 ","01-May-2026 22:08:20","01-May-2026 22:08:36","00:16","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","02-May-2026 ","02-May-2026 22:47:23","02-May-2026 22:47:36","00:13","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","03-May-2026 ","03-May-2026 22:04:34","03-May-2026 22:04:46","00:12","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","04-May-2026 ","04-May-2026 22:18:22","04-May-2026 22:18:37","00:15","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","05-May-2026 ","05-May-2026 20:52:05","05-May-2026 20:52:21","00:16","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","06-May-2026 ","06-May-2026 21:53:26","06-May-2026 21:53:40","00:14","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","07-May-2026 ","07-May-2026 22:03:10","07-May-2026 22:03:25","00:15","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","08-May-2026 ","09-May-2026 08:05:31","09-May-2026 08:06:06","00:35","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","09-May-2026 ","09-May-2026 22:35:37","09-May-2026 22:36:16","00:39","72","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","10-May-2026 ","10-May-2026 22:57:33","10-May-2026 22:57:43","00:10","73","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","11-May-2026 ","11-May-2026 22:05:17","11-May-2026 22:05:27","00:10","74","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","12-May-2026 ","12-May-2026 22:11:54","12-May-2026 22:12:06","00:12","75","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","13-May-2026 ","13-May-2026 19:03:38","13-May-2026 19:03:48","00:10","76","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","14-May-2026 ","14-May-2026 21:45:03","14-May-2026 21:45:12","00:09","77","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","15-May-2026 ","15-May-2026 21:23:03","15-May-2026 21:23:15","00:12","78","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","16-May-2026 ","16-May-2026 22:50:45","16-May-2026 22:50:54","00:09","79","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","17-May-2026 ","17-May-2026 23:21:33","17-May-2026 23:21:47","00:14","80","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","18-May-2026 ","18-May-2026 20:53:38","18-May-2026 20:53:47","00:09","81","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","19-May-2026 ","19-May-2026 22:30:38","19-May-2026 22:30:47","00:09","82","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","20-May-2026 ","20-May-2026 22:46:06","20-May-2026 22:46:26","00:20","83","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","21-May-2026 ","21-May-2026 21:37:09","21-May-2026 21:37:24","00:15","84","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","22-May-2026 ","22-May-2026 22:05:40","22-May-2026 22:05:59","00:19","85","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","23-May-2026 ","24-May-2026 10:05:24","24-May-2026 10:05:44","00:20","86","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","24-May-2026 ","24-May-2026 22:03:18","24-May-2026 22:03:28","00:10","87","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","25-May-2026 ","26-May-2026 07:34:48","26-May-2026 07:35:03","00:15","88","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","26-May-2026 ","26-May-2026 21:00:23","26-May-2026 21:00:32","00:09","89","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","27-May-2026 ","27-May-2026 23:02:15","27-May-2026 23:02:28","00:13","90","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","28-May-2026 ","28-May-2026 21:49:06","28-May-2026 21:49:34","00:28","91","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","29-May-2026 ","29-May-2026 22:58:10","29-May-2026 22:58:24","00:14","92","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","30-May-2026 ","30-May-2026 23:07:43","30-May-2026 23:08:00","00:17","93","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","31-May-2026 ","31-May-2026 21:26:36","31-May-2026 21:26:46","00:10","94","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","01-Jun-2026 ","01-Jun-2026 22:06:48","01-Jun-2026 22:06:58","00:10","95","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","02-Jun-2026 ","02-Jun-2026 22:38:41","02-Jun-2026 22:38:55","00:14","96","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","03-Jun-2026 ","03-Jun-2026 20:06:21","03-Jun-2026 20:06:46","00:25","97","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","04-Jun-2026 ","04-Jun-2026 21:44:46","04-Jun-2026 21:45:12","00:26","98","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","05-Jun-2026 ","05-Jun-2026 22:33:45","05-Jun-2026 22:34:12","00:27","99","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","06-Jun-2026 ","06-Jun-2026 23:08:52","06-Jun-2026 23:09:03","00:11","100","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","07-Jun-2026 ","07-Jun-2026 22:47:49","07-Jun-2026 22:47:59","00:10","101","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","08-Jun-2026 ","08-Jun-2026 22:23:03","08-Jun-2026 22:23:16","00:13","102","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","29","1","","09-Jun-2026 ","09-Jun-2026 21:56:31","09-Jun-2026 21:56:42","00:11","103","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","01-Apr-2026 ","01-Apr-2026 20:42:48","01-Apr-2026 20:44:08","01:20","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","02-Apr-2026 ","02-Apr-2026 22:34:46","02-Apr-2026 22:35:23","00:37","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","03-Apr-2026 ","03-Apr-2026 19:40:50","03-Apr-2026 19:41:26","00:36","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","04-Apr-2026 ","04-Apr-2026 21:34:52","04-Apr-2026 21:35:20","00:28","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","05-Apr-2026 ","05-Apr-2026 20:47:56","05-Apr-2026 20:48:17","00:21","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","06-Apr-2026 ","06-Apr-2026 23:05:55","06-Apr-2026 23:06:13","00:18","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","07-Apr-2026 ","07-Apr-2026 21:39:21","07-Apr-2026 21:39:43","00:22","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","08-Apr-2026 ","09-Apr-2026 18:02:39","09-Apr-2026 18:03:05","00:26","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","09-Apr-2026 ","09-Apr-2026 18:03:29","09-Apr-2026 18:03:43","00:14","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","10-Apr-2026 ","10-Apr-2026 18:17:13","10-Apr-2026 18:17:32","00:19","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","11-Apr-2026 ","11-Apr-2026 18:02:06","11-Apr-2026 18:02:31","00:25","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","12-Apr-2026 ","12-Apr-2026 20:40:55","12-Apr-2026 20:41:15","00:20","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","13-Apr-2026 ","13-Apr-2026 19:31:34","13-Apr-2026 19:31:48","00:14","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","14-Apr-2026 ","14-Apr-2026 20:27:48","14-Apr-2026 20:28:12","00:24","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","15-Apr-2026 ","16-Apr-2026 07:42:13","16-Apr-2026 07:42:32","00:19","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","16-Apr-2026 ","16-Apr-2026 19:55:45","16-Apr-2026 19:56:05","00:20","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","17-Apr-2026 ","17-Apr-2026 21:11:08","17-Apr-2026 21:11:44","00:36","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","18-Apr-2026 ","18-Apr-2026 18:24:43","18-Apr-2026 18:24:57","00:14","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","19-Apr-2026 ","20-Apr-2026 09:13:58","20-Apr-2026 09:14:22","00:24","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","20-Apr-2026 ","20-Apr-2026 18:50:09","20-Apr-2026 18:50:29","00:20","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","21-Apr-2026 ","21-Apr-2026 18:02:37","21-Apr-2026 18:05:29","02:52","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","18","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","22-Apr-2026 ","22-Apr-2026 22:35:07","22-Apr-2026 22:35:25","00:18","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","23-Apr-2026 ","24-Apr-2026 08:04:27","24-Apr-2026 08:04:41","00:14","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","24-Apr-2026 ","25-Apr-2026 09:53:31","25-Apr-2026 09:53:51","00:20","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","25-Apr-2026 ","25-Apr-2026 21:15:46","25-Apr-2026 21:15:58","00:12","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","26-Apr-2026 ","26-Apr-2026 22:16:21","26-Apr-2026 22:16:32","00:11","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","27-Apr-2026 ","27-Apr-2026 21:04:26","27-Apr-2026 21:04:39","00:13","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","28-Apr-2026 ","29-Apr-2026 17:52:10","29-Apr-2026 17:53:02","00:52","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","29-Apr-2026 ","30-Apr-2026 22:35:16","30-Apr-2026 22:36:01","00:45","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","30-Apr-2026 ","30-Apr-2026 22:36:45","30-Apr-2026 22:37:21","00:36","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","01-May-2026 ","01-May-2026 21:49:37","01-May-2026 21:49:54","00:17","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","02-May-2026 ","02-May-2026 22:03:41","02-May-2026 22:03:53","00:12","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","03-May-2026 ","03-May-2026 20:30:41","03-May-2026 20:30:52","00:11","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","04-May-2026 ","04-May-2026 23:27:30","04-May-2026 23:27:43","00:13","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","06-May-2026 ","07-May-2026 18:06:55","07-May-2026 18:07:08","00:13","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","07-May-2026 ","07-May-2026 18:07:33","07-May-2026 18:07:44","00:11","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","08-May-2026 ","08-May-2026 22:42:00","08-May-2026 22:42:16","00:16","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","09-May-2026 ","10-May-2026 21:18:31","10-May-2026 21:18:44","00:13","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","10-May-2026 ","10-May-2026 21:20:11","10-May-2026 21:20:22","00:11","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","11-May-2026 ","12-May-2026 10:48:02","12-May-2026 10:48:16","00:14","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","12-May-2026 ","12-May-2026 22:37:05","12-May-2026 22:37:24","00:19","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","15-May-2026 ","16-May-2026 21:29:32","16-May-2026 21:29:53","00:21","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","16-May-2026 ","16-May-2026 21:30:21","16-May-2026 21:31:03","00:42","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","17-May-2026 ","17-May-2026 20:09:06","17-May-2026 20:09:20","00:14","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","19-May-2026 ","20-May-2026 22:54:46","20-May-2026 22:55:16","00:30","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","20-May-2026 ","20-May-2026 22:56:47","20-May-2026 22:57:06","00:19","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","21-May-2026 ","22-May-2026 18:02:38","22-May-2026 18:02:50","00:12","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","22-May-2026 ","22-May-2026 18:03:46","22-May-2026 18:04:09","00:23","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","23-May-2026 ","23-May-2026 21:39:50","23-May-2026 21:40:02","00:12","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","25-May-2026 ","26-May-2026 18:01:24","26-May-2026 18:01:41","00:17","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","26-May-2026 ","26-May-2026 18:02:05","26-May-2026 18:02:19","00:14","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","28-May-2026 ","29-May-2026 21:55:32","29-May-2026 21:55:59","00:27","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","29-May-2026 ","29-May-2026 21:56:21","29-May-2026 21:56:43","00:22","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","30-May-2026 ","30-May-2026 18:42:12","30-May-2026 18:42:36","00:24","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","31-May-2026 ","31-May-2026 22:45:51","31-May-2026 22:46:12","00:21","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","01-Jun-2026 ","01-Jun-2026 21:39:27","01-Jun-2026 21:39:44","00:17","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","03-Jun-2026 ","04-Jun-2026 18:31:00","04-Jun-2026 18:31:18","00:18","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","04-Jun-2026 ","04-Jun-2026 18:31:41","04-Jun-2026 18:32:03","00:22","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","05-Jun-2026 ","05-Jun-2026 20:37:46","05-Jun-2026 20:38:06","00:20","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","06-Jun-2026 ","06-Jun-2026 20:46:33","06-Jun-2026 20:46:43","00:10","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","29","1","","07-Jun-2026 ","07-Jun-2026 23:24:20","07-Jun-2026 23:24:31","00:11","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","26-May-2026 ","26-May-2026 20:55:38","26-May-2026 20:56:02","00:24","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","27-May-2026 ","27-May-2026 19:26:19","27-May-2026 19:26:34","00:15","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","28-May-2026 ","29-May-2026 18:54:58","29-May-2026 18:55:16","00:18","3","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","29-May-2026 ","29-May-2026 18:55:33","29-May-2026 18:55:55","00:22","4","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","30-May-2026 ","30-May-2026 18:08:30","30-May-2026 18:08:48","00:18","5","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","31-May-2026 ","31-May-2026 19:32:37","31-May-2026 19:32:51","00:14","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","01-Jun-2026 ","01-Jun-2026 18:39:58","01-Jun-2026 18:40:16","00:18","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","02-Jun-2026 ","03-Jun-2026 20:11:30","03-Jun-2026 20:11:47","00:17","8","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","03-Jun-2026 ","03-Jun-2026 20:12:04","03-Jun-2026 20:12:17","00:13","9","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","04-Jun-2026 ","04-Jun-2026 20:46:30","04-Jun-2026 20:46:40","00:10","10","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","05-Jun-2026 ","06-Jun-2026 21:25:58","06-Jun-2026 21:26:30","00:32","11","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","06-Jun-2026 ","06-Jun-2026 21:26:47","06-Jun-2026 21:27:02","00:15","12","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","1","Yes","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","07-Jun-2026 ","07-Jun-2026 20:00:23","07-Jun-2026 20:00:41","00:18","13","Patient","BYODHandheld","BYODHandheld","0","","0","","1","Yes","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","08-Jun-2026 ","08-Jun-2026 19:53:26","08-Jun-2026 19:53:36","00:10","14","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","49","1","","09-Jun-2026 ","09-Jun-2026 22:13:36","09-Jun-2026 22:13:47","00:11","15","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","21-Apr-2026 ","21-Apr-2026 19:20:32","21-Apr-2026 19:22:54","02:22","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","14","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","22-Apr-2026 ","22-Apr-2026 19:08:50","22-Apr-2026 19:10:28","01:38","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","12","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","23-Apr-2026 ","23-Apr-2026 18:47:18","23-Apr-2026 18:47:46","00:28","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","24-Apr-2026 ","24-Apr-2026 20:09:26","24-Apr-2026 20:10:25","00:59","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","25-Apr-2026 ","25-Apr-2026 19:59:57","25-Apr-2026 20:00:25","00:28","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","26-Apr-2026 ","26-Apr-2026 18:17:01","26-Apr-2026 18:17:45","00:44","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","27-Apr-2026 ","27-Apr-2026 19:51:27","27-Apr-2026 19:52:00","00:33","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","28-Apr-2026 ","28-Apr-2026 20:01:23","28-Apr-2026 20:01:47","00:24","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","12","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","29-Apr-2026 ","29-Apr-2026 18:53:09","29-Apr-2026 18:53:39","00:30","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","30-Apr-2026 ","30-Apr-2026 19:59:27","30-Apr-2026 19:59:51","00:24","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","01-May-2026 ","01-May-2026 19:30:00","01-May-2026 19:30:47","00:47","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","02-May-2026 ","02-May-2026 23:09:28","02-May-2026 23:09:52","00:24","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","03-May-2026 ","03-May-2026 21:23:26","03-May-2026 21:23:59","00:33","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","04-May-2026 ","04-May-2026 19:29:17","04-May-2026 19:29:50","00:33","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","05-May-2026 ","05-May-2026 22:29:06","05-May-2026 22:29:41","00:35","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","14","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","06-May-2026 ","06-May-2026 20:10:08","06-May-2026 20:10:42","00:34","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","07-May-2026 ","07-May-2026 19:28:33","07-May-2026 19:28:49","00:16","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","12","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","08-May-2026 ","08-May-2026 19:32:48","08-May-2026 19:33:22","00:34","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","09-May-2026 ","09-May-2026 19:58:49","09-May-2026 20:00:29","01:40","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","23","1","Yes, I confirm this is the correct stool count","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","10-May-2026 ","10-May-2026 20:11:37","10-May-2026 20:12:05","00:28","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","21","1","Yes, I confirm this is the correct stool count","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","11-May-2026 ","11-May-2026 21:26:07","11-May-2026 21:26:32","00:25","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","21","1","Yes, I confirm this is the correct stool count","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","12-May-2026 ","12-May-2026 20:55:03","12-May-2026 20:55:28","00:25","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","18","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","13-May-2026 ","13-May-2026 20:35:38","13-May-2026 20:35:54","00:16","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","19","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","14-May-2026 ","14-May-2026 20:01:12","14-May-2026 20:01:34","00:22","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","15-May-2026 ","15-May-2026 20:30:56","15-May-2026 20:31:10","00:14","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","20","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","16-May-2026 ","16-May-2026 19:36:34","16-May-2026 19:36:54","00:20","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","18","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","17-May-2026 ","17-May-2026 19:57:53","17-May-2026 19:58:15","00:22","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","20","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","18-May-2026 ","18-May-2026 19:28:19","18-May-2026 19:28:52","00:33","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","18","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","19-May-2026 ","19-May-2026 20:01:15","19-May-2026 20:01:41","00:26","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","17","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","20-May-2026 ","20-May-2026 19:30:02","20-May-2026 19:30:24","00:22","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","21-May-2026 ","21-May-2026 19:34:46","21-May-2026 19:35:04","00:18","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","22-May-2026 ","22-May-2026 20:01:59","22-May-2026 20:02:19","00:20","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","23-May-2026 ","23-May-2026 22:20:42","23-May-2026 22:20:58","00:16","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","24-May-2026 ","24-May-2026 21:51:20","24-May-2026 21:51:41","00:21","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","25-May-2026 ","25-May-2026 20:47:57","25-May-2026 20:48:23","00:26","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","26-May-2026 ","26-May-2026 19:54:28","26-May-2026 19:54:46","00:18","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","15","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","27-May-2026 ","27-May-2026 19:35:37","27-May-2026 19:35:58","00:21","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","14","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","28-May-2026 ","28-May-2026 20:08:12","28-May-2026 20:08:36","00:24","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","29-May-2026 ","29-May-2026 19:30:44","29-May-2026 19:31:19","00:35","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","30-May-2026 ","30-May-2026 19:27:08","30-May-2026 19:27:36","00:28","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","31-May-2026 ","31-May-2026 19:11:49","31-May-2026 19:12:10","00:21","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","01-Jun-2026 ","01-Jun-2026 19:24:44","01-Jun-2026 19:25:12","00:28","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","02-Jun-2026 ","02-Jun-2026 20:39:30","02-Jun-2026 20:39:47","00:17","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","03-Jun-2026 ","03-Jun-2026 19:42:20","03-Jun-2026 19:43:04","00:44","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","04-Jun-2026 ","04-Jun-2026 20:01:23","04-Jun-2026 20:01:40","00:17","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","05-Jun-2026 ","05-Jun-2026 20:01:18","05-Jun-2026 20:02:06","00:48","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","06-Jun-2026 ","06-Jun-2026 19:22:48","06-Jun-2026 19:23:33","00:45","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","13","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","07-Jun-2026 ","07-Jun-2026 19:42:08","07-Jun-2026 19:42:26","00:18","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","08-Jun-2026 ","08-Jun-2026 19:21:59","08-Jun-2026 19:22:35","00:36","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","48","1","","09-Jun-2026 ","09-Jun-2026 19:09:42","09-Jun-2026 19:10:07","00:25","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","27-May-2026 ","27-May-2026 22:05:52","27-May-2026 22:06:46","00:54","1","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","28-May-2026 ","28-May-2026 20:33:03","28-May-2026 20:33:32","00:29","2","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","29-May-2026 ","29-May-2026 19:58:09","29-May-2026 19:58:29","00:20","3","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","30-May-2026 ","30-May-2026 22:00:15","30-May-2026 22:00:26","00:11","4","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","31-May-2026 ","31-May-2026 22:29:28","31-May-2026 22:29:42","00:14","5","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","01-Jun-2026 ","01-Jun-2026 19:08:59","01-Jun-2026 19:09:09","00:10","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","02-Jun-2026 ","02-Jun-2026 18:49:31","02-Jun-2026 18:49:45","00:14","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","03-Jun-2026 ","03-Jun-2026 18:26:02","03-Jun-2026 18:26:26","00:24","8","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","04-Jun-2026 ","04-Jun-2026 22:23:10","04-Jun-2026 22:23:25","00:15","9","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","05-Jun-2026 ","05-Jun-2026 22:21:56","05-Jun-2026 22:22:11","00:15","10","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","06-Jun-2026 ","06-Jun-2026 21:31:51","06-Jun-2026 21:32:04","00:13","11","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","07-Jun-2026 ","07-Jun-2026 18:09:53","07-Jun-2026 18:10:05","00:12","12","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162002","42","1","","08-Jun-2026 ","08-Jun-2026 20:54:29","08-Jun-2026 20:54:37","00:08","13","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","13-Apr-2026 ","13-Apr-2026 18:02:00","13-Apr-2026 18:03:08","01:08","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","14-Apr-2026 ","14-Apr-2026 18:01:42","14-Apr-2026 18:02:26","00:44","2","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","15-Apr-2026 ","15-Apr-2026 18:01:38","15-Apr-2026 18:02:04","00:26","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","16-Apr-2026 ","16-Apr-2026 18:32:28","16-Apr-2026 18:32:52","00:24","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","17-Apr-2026 ","17-Apr-2026 18:01:23","17-Apr-2026 18:01:40","00:17","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","18-Apr-2026 ","18-Apr-2026 18:01:26","18-Apr-2026 18:01:37","00:11","6","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","19-Apr-2026 ","19-Apr-2026 18:01:23","19-Apr-2026 18:01:40","00:17","7","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","20-Apr-2026 ","20-Apr-2026 20:46:30","20-Apr-2026 20:46:51","00:21","8","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","21-Apr-2026 ","21-Apr-2026 18:13:48","21-Apr-2026 18:13:57","00:09","9","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","22-Apr-2026 ","22-Apr-2026 18:06:03","22-Apr-2026 18:06:18","00:15","10","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","23-Apr-2026 ","23-Apr-2026 18:09:32","23-Apr-2026 18:09:44","00:12","11","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","24-Apr-2026 ","24-Apr-2026 18:01:08","24-Apr-2026 18:01:22","00:14","12","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","25-Apr-2026 ","25-Apr-2026 18:36:43","25-Apr-2026 18:36:56","00:13","13","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","26-Apr-2026 ","26-Apr-2026 18:01:56","26-Apr-2026 18:02:12","00:16","14","Patient","BYODHandheld","BYODHandheld","1","Yes","0","","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","27-Apr-2026 ","27-Apr-2026 18:01:05","27-Apr-2026 18:01:19","00:14","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","28-Apr-2026 ","28-Apr-2026 21:12:00","28-Apr-2026 21:12:13","00:13","16","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","29-Apr-2026 ","29-Apr-2026 18:01:04","29-Apr-2026 18:01:15","00:11","17","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","30-Apr-2026 ","30-Apr-2026 18:56:10","30-Apr-2026 18:56:21","00:11","18","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","01-May-2026 ","01-May-2026 18:01:23","01-May-2026 18:01:39","00:16","19","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","02-May-2026 ","02-May-2026 18:01:21","02-May-2026 18:01:39","00:18","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","03-May-2026 ","03-May-2026 18:01:14","03-May-2026 18:01:26","00:12","21","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","04-May-2026 ","04-May-2026 18:01:29","04-May-2026 18:02:07","00:38","22","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","05-May-2026 ","05-May-2026 18:18:48","05-May-2026 18:19:11","00:23","23","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","06-May-2026 ","06-May-2026 18:01:52","06-May-2026 18:02:05","00:13","24","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","07-May-2026 ","07-May-2026 18:01:54","07-May-2026 18:02:38","00:44","25","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","08-May-2026 ","08-May-2026 18:01:35","08-May-2026 18:01:45","00:10","26","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","09-May-2026 ","09-May-2026 18:01:07","09-May-2026 18:01:23","00:16","27","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","10-May-2026 ","10-May-2026 18:01:29","10-May-2026 18:02:07","00:38","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","11-May-2026 ","11-May-2026 20:05:03","11-May-2026 20:05:14","00:11","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","12-May-2026 ","12-May-2026 19:46:23","12-May-2026 19:46:39","00:16","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","13-May-2026 ","13-May-2026 21:37:54","13-May-2026 21:38:14","00:20","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","14-May-2026 ","14-May-2026 19:47:34","14-May-2026 19:48:33","00:59","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","15-May-2026 ","15-May-2026 18:04:38","15-May-2026 18:04:49","00:11","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","16-May-2026 ","16-May-2026 18:01:42","16-May-2026 18:02:35","00:53","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","17-May-2026 ","17-May-2026 19:30:43","17-May-2026 19:31:40","00:57","35","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","18-May-2026 ","18-May-2026 19:46:33","18-May-2026 19:46:59","00:26","36","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","19-May-2026 ","19-May-2026 18:02:50","19-May-2026 18:03:08","00:18","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","20-May-2026 ","20-May-2026 18:02:27","20-May-2026 18:02:40","00:13","38","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","21-May-2026 ","21-May-2026 20:51:11","21-May-2026 20:51:22","00:11","39","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","22-May-2026 ","22-May-2026 21:27:22","22-May-2026 21:27:31","00:09","40","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","23-May-2026 ","23-May-2026 22:20:05","23-May-2026 22:20:21","00:16","41","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","24-May-2026 ","24-May-2026 20:24:10","24-May-2026 20:24:27","00:17","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","25-May-2026 ","25-May-2026 21:49:27","25-May-2026 21:49:36","00:09","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","26-May-2026 ","26-May-2026 22:32:04","26-May-2026 22:32:17","00:13","44","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","27-May-2026 ","27-May-2026 22:44:49","27-May-2026 22:45:01","00:12","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","31-May-2026 ","01-Jun-2026 07:59:37","01-Jun-2026 08:00:22","00:45","46","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","01-Jun-2026 ","01-Jun-2026 18:01:51","01-Jun-2026 18:02:13","00:22","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","02-Jun-2026 ","02-Jun-2026 23:16:09","02-Jun-2026 23:16:22","00:13","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","03-Jun-2026 ","03-Jun-2026 23:03:37","03-Jun-2026 23:03:52","00:15","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","04-Jun-2026 ","04-Jun-2026 22:05:17","04-Jun-2026 22:05:30","00:13","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","05-Jun-2026 ","05-Jun-2026 22:11:56","05-Jun-2026 22:12:31","00:35","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","06-Jun-2026 ","06-Jun-2026 23:20:55","06-Jun-2026 23:21:07","00:12","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","07-Jun-2026 ","07-Jun-2026 22:25:26","07-Jun-2026 22:25:38","00:12","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","08-Jun-2026 ","08-Jun-2026 22:51:31","08-Jun-2026 22:51:41","00:10","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","15","1","","09-Jun-2026 ","09-Jun-2026 22:56:10","09-Jun-2026 22:56:22","00:12","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","01-Mar-2026 ","02-Mar-2026 18:40:39","02-Mar-2026 18:42:35","01:56","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","02-Mar-2026 ","02-Mar-2026 18:42:58","02-Mar-2026 18:44:02","01:04","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","03-Mar-2026 ","03-Mar-2026 18:16:16","03-Mar-2026 18:17:20","01:04","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","04-Mar-2026 ","04-Mar-2026 19:00:11","04-Mar-2026 19:00:54","00:43","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","05-Mar-2026 ","05-Mar-2026 18:08:45","05-Mar-2026 18:09:18","00:33","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","06-Mar-2026 ","06-Mar-2026 18:18:34","06-Mar-2026 18:19:08","00:34","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","07-Mar-2026 ","07-Mar-2026 18:01:32","07-Mar-2026 18:02:01","00:29","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","08-Mar-2026 ","08-Mar-2026 18:09:54","08-Mar-2026 18:10:19","00:25","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","09-Mar-2026 ","09-Mar-2026 18:50:13","09-Mar-2026 18:50:41","00:28","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","10-Mar-2026 ","10-Mar-2026 18:24:14","10-Mar-2026 18:24:43","00:29","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","11-Mar-2026 ","11-Mar-2026 19:06:52","11-Mar-2026 19:07:11","00:19","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","12-Mar-2026 ","12-Mar-2026 18:07:54","12-Mar-2026 18:08:18","00:24","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","13-Mar-2026 ","13-Mar-2026 18:12:06","13-Mar-2026 18:12:30","00:24","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","14-Mar-2026 ","15-Mar-2026 18:30:45","15-Mar-2026 18:31:18","00:33","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","15-Mar-2026 ","15-Mar-2026 18:32:00","15-Mar-2026 18:32:31","00:31","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","16-Mar-2026 ","16-Mar-2026 19:00:29","16-Mar-2026 19:01:41","01:12","16","Patient","BYODHandheld","BYODHandheld","1","Yes","0","","0","","0","","24","1","Yes, I confirm this is the correct stool count","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","17-Mar-2026 ","17-Mar-2026 18:03:41","17-Mar-2026 18:04:05","00:24","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","18-Mar-2026 ","18-Mar-2026 19:07:09","18-Mar-2026 19:07:36","00:27","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","19-Mar-2026 ","19-Mar-2026 19:24:39","19-Mar-2026 19:25:08","00:29","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","20-Mar-2026 ","20-Mar-2026 18:39:21","20-Mar-2026 18:39:53","00:32","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","21-Mar-2026 ","21-Mar-2026 18:49:04","21-Mar-2026 18:49:27","00:23","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","12","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","22-Mar-2026 ","22-Mar-2026 18:12:30","22-Mar-2026 18:12:57","00:27","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","23-Mar-2026 ","23-Mar-2026 18:07:23","23-Mar-2026 18:07:47","00:24","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","24-Mar-2026 ","24-Mar-2026 19:10:30","24-Mar-2026 19:11:07","00:37","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","25-Mar-2026 ","25-Mar-2026 19:10:29","25-Mar-2026 19:10:52","00:23","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","26-Mar-2026 ","26-Mar-2026 19:14:57","26-Mar-2026 19:15:22","00:25","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","27-Mar-2026 ","27-Mar-2026 18:12:05","27-Mar-2026 18:12:28","00:23","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","28-Mar-2026 ","28-Mar-2026 18:02:25","28-Mar-2026 18:02:48","00:23","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","12","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","29-Mar-2026 ","29-Mar-2026 18:09:12","29-Mar-2026 18:09:37","00:25","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","30-Mar-2026 ","30-Mar-2026 18:09:29","30-Mar-2026 18:09:52","00:23","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","31-Mar-2026 ","31-Mar-2026 18:18:41","31-Mar-2026 18:19:11","00:30","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","01-Apr-2026 ","01-Apr-2026 19:27:09","01-Apr-2026 19:27:31","00:22","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","02-Apr-2026 ","02-Apr-2026 18:11:54","02-Apr-2026 18:12:24","00:30","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","03-Apr-2026 ","03-Apr-2026 18:38:38","03-Apr-2026 18:38:58","00:20","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","04-Apr-2026 ","04-Apr-2026 18:03:04","04-Apr-2026 18:03:28","00:24","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","05-Apr-2026 ","05-Apr-2026 18:13:29","05-Apr-2026 18:13:50","00:21","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","06-Apr-2026 ","06-Apr-2026 20:42:37","06-Apr-2026 20:42:57","00:20","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","11","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","07-Apr-2026 ","07-Apr-2026 18:58:01","07-Apr-2026 18:58:20","00:19","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","08-Apr-2026 ","09-Apr-2026 18:05:31","09-Apr-2026 18:05:59","00:28","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","09-Apr-2026 ","09-Apr-2026 18:06:30","09-Apr-2026 18:07:07","00:37","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","10-Apr-2026 ","10-Apr-2026 18:13:58","10-Apr-2026 18:14:20","00:22","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","11-Apr-2026 ","11-Apr-2026 18:02:38","11-Apr-2026 18:03:02","00:24","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","12-Apr-2026 ","12-Apr-2026 18:08:16","12-Apr-2026 18:08:44","00:28","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","13-Apr-2026 ","13-Apr-2026 19:20:43","13-Apr-2026 19:21:05","00:22","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","14-Apr-2026 ","14-Apr-2026 18:18:45","14-Apr-2026 18:19:12","00:27","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","15-Apr-2026 ","15-Apr-2026 18:01:54","15-Apr-2026 18:02:17","00:23","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","16-Apr-2026 ","16-Apr-2026 18:04:15","16-Apr-2026 18:04:39","00:24","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","17-Apr-2026 ","17-Apr-2026 18:05:38","17-Apr-2026 18:06:11","00:33","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","18-Apr-2026 ","18-Apr-2026 18:02:10","18-Apr-2026 18:02:38","00:28","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","19-Apr-2026 ","19-Apr-2026 19:24:43","19-Apr-2026 19:25:10","00:27","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","20-Apr-2026 ","20-Apr-2026 18:36:26","20-Apr-2026 18:36:54","00:28","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","21-Apr-2026 ","21-Apr-2026 18:10:57","21-Apr-2026 18:11:17","00:20","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","22-Apr-2026 ","22-Apr-2026 18:47:22","22-Apr-2026 18:47:44","00:22","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","23-Apr-2026 ","23-Apr-2026 18:04:27","23-Apr-2026 18:04:52","00:25","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","24-Apr-2026 ","24-Apr-2026 18:03:12","24-Apr-2026 18:03:36","00:24","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","25-Apr-2026 ","25-Apr-2026 20:48:03","25-Apr-2026 20:48:23","00:20","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","26-Apr-2026 ","26-Apr-2026 19:10:03","26-Apr-2026 19:10:28","00:25","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","27-Apr-2026 ","27-Apr-2026 18:10:02","27-Apr-2026 18:10:25","00:23","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","28-Apr-2026 ","28-Apr-2026 18:03:12","28-Apr-2026 18:03:35","00:23","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","29-Apr-2026 ","29-Apr-2026 18:09:26","29-Apr-2026 18:10:02","00:36","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","30-Apr-2026 ","01-May-2026 18:26:24","01-May-2026 18:27:02","00:38","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","01-May-2026 ","01-May-2026 18:27:45","01-May-2026 18:28:20","00:35","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","02-May-2026 ","02-May-2026 19:54:00","02-May-2026 19:54:30","00:30","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","03-May-2026 ","03-May-2026 18:24:50","03-May-2026 18:25:14","00:24","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","04-May-2026 ","05-May-2026 19:36:17","05-May-2026 19:36:48","00:31","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","05-May-2026 ","05-May-2026 19:37:23","05-May-2026 19:37:43","00:20","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","06-May-2026 ","06-May-2026 18:18:59","06-May-2026 18:19:20","00:21","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","07-May-2026 ","07-May-2026 18:04:36","07-May-2026 18:05:00","00:24","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","08-May-2026 ","08-May-2026 18:22:35","08-May-2026 18:22:53","00:18","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","09-May-2026 ","09-May-2026 18:39:28","09-May-2026 18:39:51","00:23","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","10-May-2026 ","10-May-2026 18:30:41","10-May-2026 18:31:24","00:43","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","11-May-2026 ","11-May-2026 18:23:54","11-May-2026 18:24:14","00:20","72","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","12-May-2026 ","12-May-2026 18:23:43","12-May-2026 18:24:00","00:17","73","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","13-May-2026 ","13-May-2026 18:27:48","13-May-2026 18:28:14","00:26","74","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","14-May-2026 ","14-May-2026 18:11:36","14-May-2026 18:11:53","00:17","75","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","15-May-2026 ","15-May-2026 19:19:15","15-May-2026 19:19:34","00:19","76","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","16-May-2026 ","16-May-2026 18:44:47","16-May-2026 18:45:07","00:20","77","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","17-May-2026 ","17-May-2026 19:48:23","17-May-2026 19:48:53","00:30","78","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","18-May-2026 ","18-May-2026 18:08:29","18-May-2026 18:08:53","00:24","79","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","19-May-2026 ","19-May-2026 18:09:31","19-May-2026 18:10:01","00:30","80","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","20-May-2026 ","20-May-2026 18:18:41","20-May-2026 18:19:07","00:26","81","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","21-May-2026 ","21-May-2026 18:23:15","21-May-2026 18:24:11","00:56","82","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","22-May-2026 ","22-May-2026 18:06:32","22-May-2026 18:07:05","00:33","83","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","23-May-2026 ","23-May-2026 18:20:11","23-May-2026 18:20:38","00:27","84","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","24-May-2026 ","24-May-2026 18:08:45","24-May-2026 18:09:06","00:21","85","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","25-May-2026 ","25-May-2026 18:11:49","25-May-2026 18:12:14","00:25","86","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","26-May-2026 ","26-May-2026 18:15:47","26-May-2026 18:16:11","00:24","87","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","27-May-2026 ","27-May-2026 18:14:45","27-May-2026 18:15:04","00:19","88","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","28-May-2026 ","29-May-2026 18:26:37","29-May-2026 18:27:06","00:29","89","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","29-May-2026 ","29-May-2026 18:27:39","29-May-2026 18:27:57","00:18","90","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","30-May-2026 ","30-May-2026 18:08:14","30-May-2026 18:08:43","00:29","91","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","31-May-2026 ","31-May-2026 18:33:56","31-May-2026 18:34:17","00:21","92","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","01-Jun-2026 ","01-Jun-2026 19:16:03","01-Jun-2026 19:16:21","00:18","93","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","02-Jun-2026 ","02-Jun-2026 20:03:09","02-Jun-2026 20:03:35","00:26","94","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","03-Jun-2026 ","03-Jun-2026 18:23:04","03-Jun-2026 18:23:25","00:21","95","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","04-Jun-2026 ","04-Jun-2026 18:52:32","04-Jun-2026 18:52:53","00:21","96","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","05-Jun-2026 ","05-Jun-2026 18:07:44","05-Jun-2026 18:08:05","00:21","97","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","06-Jun-2026 ","06-Jun-2026 18:40:33","06-Jun-2026 18:40:58","00:25","98","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","07-Jun-2026 ","07-Jun-2026 18:15:53","07-Jun-2026 18:16:39","00:46","99","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","08-Jun-2026 ","08-Jun-2026 18:20:13","08-Jun-2026 18:20:38","00:25","100","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","61","1","","09-Jun-2026 ","09-Jun-2026 18:02:00","09-Jun-2026 18:02:20","00:20","101","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","15-Jan-2026 ","15-Jan-2026 22:48:04","15-Jan-2026 22:48:28","00:24","1","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","9","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","16-Jan-2026 ","16-Jan-2026 18:55:34","16-Jan-2026 18:56:14","00:40","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","17-Jan-2026 ","17-Jan-2026 18:01:51","17-Jan-2026 18:02:19","00:28","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","18-Jan-2026 ","18-Jan-2026 18:25:55","18-Jan-2026 18:26:19","00:24","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","19-Jan-2026 ","19-Jan-2026 18:10:54","19-Jan-2026 18:11:19","00:25","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","20-Jan-2026 ","20-Jan-2026 18:04:44","20-Jan-2026 18:05:02","00:18","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","21-Jan-2026 ","21-Jan-2026 18:06:08","21-Jan-2026 18:06:32","00:24","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","22-Jan-2026 ","22-Jan-2026 18:49:45","22-Jan-2026 18:50:08","00:23","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","23-Jan-2026 ","23-Jan-2026 20:50:48","23-Jan-2026 20:51:10","00:22","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","3","Blood alone passed","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","24-Jan-2026 ","24-Jan-2026 23:07:44","24-Jan-2026 23:08:20","00:36","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","25-Jan-2026 ","25-Jan-2026 20:58:21","25-Jan-2026 20:58:46","00:25","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","26-Jan-2026 ","26-Jan-2026 20:36:58","26-Jan-2026 20:37:09","00:11","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","27-Jan-2026 ","27-Jan-2026 20:54:52","27-Jan-2026 20:55:08","00:16","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","28-Jan-2026 ","28-Jan-2026 18:14:23","28-Jan-2026 18:14:40","00:17","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","29-Jan-2026 ","29-Jan-2026 18:50:46","29-Jan-2026 18:51:05","00:19","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","30-Jan-2026 ","30-Jan-2026 18:26:27","30-Jan-2026 18:26:45","00:18","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","31-Jan-2026 ","31-Jan-2026 19:52:40","31-Jan-2026 19:52:54","00:14","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","01-Feb-2026 ","01-Feb-2026 18:40:02","01-Feb-2026 18:40:16","00:14","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","02-Feb-2026 ","02-Feb-2026 19:32:39","02-Feb-2026 19:32:58","00:19","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","03-Feb-2026 ","03-Feb-2026 18:01:25","03-Feb-2026 18:01:39","00:14","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","04-Feb-2026 ","04-Feb-2026 18:59:17","04-Feb-2026 18:59:46","00:29","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","05-Feb-2026 ","05-Feb-2026 18:13:09","05-Feb-2026 18:13:33","00:24","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","06-Feb-2026 ","06-Feb-2026 18:17:15","06-Feb-2026 18:17:32","00:17","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","07-Feb-2026 ","07-Feb-2026 18:55:47","07-Feb-2026 18:56:19","00:32","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","08-Feb-2026 ","08-Feb-2026 18:01:33","08-Feb-2026 18:01:46","00:13","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","09-Feb-2026 ","09-Feb-2026 18:31:24","09-Feb-2026 18:31:36","00:12","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","10-Feb-2026 ","11-Feb-2026 18:49:47","11-Feb-2026 18:50:04","00:17","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","11-Feb-2026 ","11-Feb-2026 18:50:24","11-Feb-2026 18:50:38","00:14","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","12-Feb-2026 ","12-Feb-2026 18:36:48","12-Feb-2026 18:37:03","00:15","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","13-Feb-2026 ","13-Feb-2026 18:44:19","13-Feb-2026 18:44:46","00:27","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","14-Feb-2026 ","15-Feb-2026 19:02:43","15-Feb-2026 19:03:08","00:25","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","15-Feb-2026 ","15-Feb-2026 19:03:35","15-Feb-2026 19:03:54","00:19","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","16-Feb-2026 ","16-Feb-2026 18:24:47","16-Feb-2026 18:24:59","00:12","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","17-Feb-2026 ","17-Feb-2026 18:19:20","17-Feb-2026 18:19:33","00:13","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","18-Feb-2026 ","19-Feb-2026 21:30:15","19-Feb-2026 21:30:32","00:17","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","19-Feb-2026 ","19-Feb-2026 21:30:55","19-Feb-2026 21:31:06","00:11","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","20-Feb-2026 ","20-Feb-2026 18:07:24","20-Feb-2026 18:07:39","00:15","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","21-Feb-2026 ","21-Feb-2026 18:04:27","21-Feb-2026 18:04:45","00:18","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","22-Feb-2026 ","22-Feb-2026 18:27:54","22-Feb-2026 18:28:07","00:13","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","23-Feb-2026 ","23-Feb-2026 20:13:02","23-Feb-2026 20:13:16","00:14","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","24-Feb-2026 ","24-Feb-2026 18:10:58","24-Feb-2026 18:11:27","00:29","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","9","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","25-Feb-2026 ","25-Feb-2026 19:28:29","25-Feb-2026 19:28:55","00:26","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","26-Feb-2026 ","26-Feb-2026 18:36:10","26-Feb-2026 18:36:28","00:18","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","27-Feb-2026 ","28-Feb-2026 22:20:16","28-Feb-2026 22:20:35","00:19","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","28-Feb-2026 ","28-Feb-2026 22:20:57","28-Feb-2026 22:21:08","00:11","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222001","41","3","","01-Mar-2026 ","01-Mar-2026 18:29:02","01-Mar-2026 18:29:21","00:19","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","23-Jan-2026 ","23-Jan-2026 18:44:00","23-Jan-2026 18:44:48","00:48","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","24-Jan-2026 ","24-Jan-2026 20:04:49","24-Jan-2026 20:05:45","00:56","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","25-Jan-2026 ","26-Jan-2026 18:05:41","26-Jan-2026 18:06:13","00:32","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","26-Jan-2026 ","26-Jan-2026 18:06:31","26-Jan-2026 18:06:49","00:18","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","27-Jan-2026 ","27-Jan-2026 22:05:35","27-Jan-2026 22:05:59","00:24","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","28-Jan-2026 ","28-Jan-2026 19:49:36","28-Jan-2026 19:49:50","00:14","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","29-Jan-2026 ","29-Jan-2026 19:33:56","29-Jan-2026 19:34:09","00:13","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","30-Jan-2026 ","30-Jan-2026 18:48:41","30-Jan-2026 18:48:57","00:16","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","31-Jan-2026 ","31-Jan-2026 20:02:38","31-Jan-2026 20:02:51","00:13","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","01-Feb-2026 ","01-Feb-2026 20:59:03","01-Feb-2026 20:59:19","00:16","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","02-Feb-2026 ","02-Feb-2026 18:01:41","02-Feb-2026 18:01:59","00:18","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","03-Feb-2026 ","03-Feb-2026 18:22:38","03-Feb-2026 18:23:07","00:29","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","04-Feb-2026 ","04-Feb-2026 18:59:50","04-Feb-2026 19:00:03","00:13","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","05-Feb-2026 ","05-Feb-2026 19:40:08","05-Feb-2026 19:40:20","00:12","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","06-Feb-2026 ","06-Feb-2026 18:11:13","06-Feb-2026 18:11:32","00:19","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","07-Feb-2026 ","08-Feb-2026 14:02:35","08-Feb-2026 14:03:10","00:35","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","08-Feb-2026 ","08-Feb-2026 18:25:38","08-Feb-2026 18:26:04","00:26","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","09-Feb-2026 ","09-Feb-2026 20:19:15","09-Feb-2026 20:19:25","00:10","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","10-Feb-2026 ","10-Feb-2026 21:54:07","10-Feb-2026 21:54:42","00:35","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","11-Feb-2026 ","11-Feb-2026 18:34:20","11-Feb-2026 18:34:44","00:24","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","12-Feb-2026 ","12-Feb-2026 19:11:56","12-Feb-2026 19:12:14","00:18","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","13-Feb-2026 ","13-Feb-2026 19:51:22","13-Feb-2026 19:51:34","00:12","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","14-Feb-2026 ","14-Feb-2026 18:02:05","14-Feb-2026 18:03:12","01:07","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","15-Feb-2026 ","15-Feb-2026 18:45:30","15-Feb-2026 18:45:56","00:26","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","16-Feb-2026 ","16-Feb-2026 19:01:35","16-Feb-2026 19:02:07","00:32","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","17-Feb-2026 ","17-Feb-2026 19:58:00","17-Feb-2026 19:58:26","00:26","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","39","1","","18-Feb-2026 ","18-Feb-2026 19:35:30","18-Feb-2026 19:36:48","01:18","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","04-Mar-2026 ","04-Mar-2026 18:45:26","04-Mar-2026 18:45:57","00:31","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","05-Mar-2026 ","05-Mar-2026 20:32:08","05-Mar-2026 20:33:10","01:02","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","06-Mar-2026 ","06-Mar-2026 19:51:49","06-Mar-2026 19:52:07","00:18","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","07-Mar-2026 ","07-Mar-2026 20:14:59","07-Mar-2026 20:15:18","00:19","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","08-Mar-2026 ","08-Mar-2026 21:42:48","08-Mar-2026 21:42:58","00:10","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","09-Mar-2026 ","09-Mar-2026 21:47:51","09-Mar-2026 21:48:12","00:21","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","10-Mar-2026 ","10-Mar-2026 21:33:00","10-Mar-2026 21:34:34","01:34","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","11-Mar-2026 ","11-Mar-2026 21:05:27","11-Mar-2026 21:05:46","00:19","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","12-Mar-2026 ","12-Mar-2026 21:40:59","12-Mar-2026 21:41:44","00:45","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","13-Mar-2026 ","13-Mar-2026 19:50:45","13-Mar-2026 19:51:55","01:10","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","14-Mar-2026 ","14-Mar-2026 18:04:37","14-Mar-2026 18:04:50","00:13","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","15-Mar-2026 ","15-Mar-2026 19:58:48","15-Mar-2026 19:59:01","00:13","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","16-Mar-2026 ","16-Mar-2026 20:52:43","16-Mar-2026 20:53:00","00:17","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","17-Mar-2026 ","17-Mar-2026 20:33:05","17-Mar-2026 20:33:21","00:16","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","18-Mar-2026 ","18-Mar-2026 22:02:13","18-Mar-2026 22:02:45","00:32","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","19-Mar-2026 ","19-Mar-2026 21:20:17","19-Mar-2026 21:20:33","00:16","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","20-Mar-2026 ","20-Mar-2026 19:07:44","20-Mar-2026 19:07:56","00:12","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","21-Mar-2026 ","21-Mar-2026 18:42:15","21-Mar-2026 18:42:28","00:13","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","22-Mar-2026 ","22-Mar-2026 19:24:00","22-Mar-2026 19:25:07","01:07","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","23-Mar-2026 ","23-Mar-2026 18:24:50","23-Mar-2026 18:25:19","00:29","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","24-Mar-2026 ","24-Mar-2026 20:58:13","24-Mar-2026 20:58:34","00:21","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","25-Mar-2026 ","25-Mar-2026 18:01:28","25-Mar-2026 18:01:40","00:12","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","26-Mar-2026 ","26-Mar-2026 19:59:46","26-Mar-2026 19:59:59","00:13","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","27-Mar-2026 ","27-Mar-2026 19:05:42","27-Mar-2026 19:06:36","00:54","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","28-Mar-2026 ","28-Mar-2026 21:40:08","28-Mar-2026 21:40:33","00:25","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","29-Mar-2026 ","30-Mar-2026 02:54:17","30-Mar-2026 02:55:12","00:55","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","30-Mar-2026 ","30-Mar-2026 19:29:58","30-Mar-2026 19:30:29","00:31","28","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","31-Mar-2026 ","31-Mar-2026 19:54:16","31-Mar-2026 19:54:28","00:12","29","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","01-Apr-2026 ","01-Apr-2026 19:00:10","01-Apr-2026 19:01:09","00:59","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","02-Apr-2026 ","03-Apr-2026 05:28:24","03-Apr-2026 05:28:44","00:20","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","03-Apr-2026 ","03-Apr-2026 20:36:14","03-Apr-2026 20:36:50","00:36","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","04-Apr-2026 ","04-Apr-2026 18:40:00","04-Apr-2026 18:41:36","01:36","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","05-Apr-2026 ","05-Apr-2026 18:53:00","05-Apr-2026 18:53:58","00:58","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","06-Apr-2026 ","06-Apr-2026 18:09:33","06-Apr-2026 18:10:09","00:36","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","07-Apr-2026 ","07-Apr-2026 19:18:44","07-Apr-2026 19:19:47","01:03","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","08-Apr-2026 ","09-Apr-2026 12:37:30","09-Apr-2026 12:37:48","00:18","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","09-Apr-2026 ","09-Apr-2026 20:55:59","09-Apr-2026 20:56:29","00:30","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","10-Apr-2026 ","10-Apr-2026 22:53:17","10-Apr-2026 22:53:40","00:23","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","11-Apr-2026 ","11-Apr-2026 20:04:52","11-Apr-2026 20:05:06","00:14","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","12-Apr-2026 ","12-Apr-2026 21:58:52","12-Apr-2026 21:59:11","00:19","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","13-Apr-2026 ","13-Apr-2026 21:45:51","13-Apr-2026 21:46:02","00:11","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","14-Apr-2026 ","14-Apr-2026 20:39:25","14-Apr-2026 20:39:58","00:33","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","15-Apr-2026 ","15-Apr-2026 20:14:56","15-Apr-2026 20:16:04","01:08","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","16-Apr-2026 ","16-Apr-2026 20:20:38","16-Apr-2026 20:21:56","01:18","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","17-Apr-2026 ","18-Apr-2026 04:36:28","18-Apr-2026 04:36:51","00:23","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","18-Apr-2026 ","18-Apr-2026 18:45:45","18-Apr-2026 18:46:34","00:49","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","19-Apr-2026 ","19-Apr-2026 21:11:57","19-Apr-2026 21:12:23","00:26","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","20-Apr-2026 ","20-Apr-2026 18:30:44","20-Apr-2026 18:31:27","00:43","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","21-Apr-2026 ","21-Apr-2026 19:33:38","21-Apr-2026 19:34:05","00:27","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","22-Apr-2026 ","23-Apr-2026 05:04:23","23-Apr-2026 05:04:33","00:10","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","23-Apr-2026 ","23-Apr-2026 21:46:23","23-Apr-2026 21:47:03","00:40","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","25-Apr-2026 ","26-Apr-2026 06:44:01","26-Apr-2026 06:44:16","00:15","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","26-Apr-2026 ","26-Apr-2026 19:53:35","26-Apr-2026 19:54:20","00:45","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","27-Apr-2026 ","27-Apr-2026 18:16:59","27-Apr-2026 18:17:38","00:39","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","28-Apr-2026 ","28-Apr-2026 18:27:59","28-Apr-2026 18:28:13","00:14","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","29-Apr-2026 ","29-Apr-2026 20:30:50","29-Apr-2026 20:31:01","00:11","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","30-Apr-2026 ","30-Apr-2026 21:43:01","30-Apr-2026 21:43:29","00:28","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","01-May-2026 ","01-May-2026 21:30:29","01-May-2026 21:30:42","00:13","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","02-May-2026 ","02-May-2026 22:01:00","02-May-2026 22:01:10","00:10","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","03-May-2026 ","03-May-2026 20:11:42","03-May-2026 20:12:16","00:34","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","04-May-2026 ","04-May-2026 18:41:10","04-May-2026 18:41:25","00:15","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","05-May-2026 ","05-May-2026 19:24:22","05-May-2026 19:24:54","00:32","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","06-May-2026 ","06-May-2026 21:53:22","06-May-2026 21:54:12","00:50","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","07-May-2026 ","07-May-2026 21:12:19","07-May-2026 21:12:29","00:10","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","08-May-2026 ","08-May-2026 18:11:04","08-May-2026 18:11:14","00:10","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","09-May-2026 ","09-May-2026 19:52:08","09-May-2026 19:52:17","00:09","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","10-May-2026 ","10-May-2026 19:39:21","10-May-2026 19:40:22","01:01","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","11-May-2026 ","11-May-2026 18:56:35","11-May-2026 18:56:46","00:11","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","12-May-2026 ","12-May-2026 18:17:34","12-May-2026 18:18:06","00:32","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","13-May-2026 ","13-May-2026 21:41:52","13-May-2026 21:42:04","00:12","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","14-May-2026 ","14-May-2026 20:29:40","14-May-2026 20:30:52","01:12","72","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","15-May-2026 ","15-May-2026 18:44:45","15-May-2026 18:44:59","00:14","73","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","16-May-2026 ","16-May-2026 18:11:24","16-May-2026 18:11:52","00:28","74","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","17-May-2026 ","18-May-2026 18:42:40","18-May-2026 18:43:20","00:40","75","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","18-May-2026 ","18-May-2026 18:43:51","18-May-2026 18:44:00","00:09","76","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","19-May-2026 ","19-May-2026 21:58:19","19-May-2026 21:58:29","00:10","77","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","20-May-2026 ","20-May-2026 18:41:02","20-May-2026 18:41:24","00:22","78","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","21-May-2026 ","21-May-2026 21:05:33","21-May-2026 21:05:54","00:21","79","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","22-May-2026 ","22-May-2026 21:17:24","22-May-2026 21:17:38","00:14","80","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","23-May-2026 ","23-May-2026 18:42:12","23-May-2026 18:42:28","00:16","81","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","24-May-2026 ","24-May-2026 21:24:54","24-May-2026 21:25:21","00:27","82","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","25-May-2026 ","25-May-2026 19:37:39","25-May-2026 19:38:41","01:02","83","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","26-May-2026 ","26-May-2026 20:52:27","26-May-2026 20:53:19","00:52","84","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","27-May-2026 ","27-May-2026 18:37:02","27-May-2026 18:37:22","00:20","85","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","28-May-2026 ","28-May-2026 19:26:08","28-May-2026 19:26:48","00:40","86","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","29-May-2026 ","29-May-2026 18:44:54","29-May-2026 18:45:07","00:13","87","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","30-May-2026 ","30-May-2026 19:51:02","30-May-2026 19:53:12","02:10","88","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","31-May-2026 ","31-May-2026 18:13:14","31-May-2026 18:13:29","00:15","89","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","01-Jun-2026 ","01-Jun-2026 20:24:43","01-Jun-2026 20:25:02","00:19","90","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","02-Jun-2026 ","02-Jun-2026 19:16:07","02-Jun-2026 19:16:33","00:26","91","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","03-Jun-2026 ","03-Jun-2026 20:34:33","03-Jun-2026 20:35:00","00:27","92","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","04-Jun-2026 ","05-Jun-2026 05:56:13","05-Jun-2026 05:56:29","00:16","93","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","05-Jun-2026 ","05-Jun-2026 22:01:38","05-Jun-2026 22:01:49","00:11","94","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","06-Jun-2026 ","06-Jun-2026 19:55:28","06-Jun-2026 19:55:37","00:09","95","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","07-Jun-2026 ","07-Jun-2026 21:29:03","07-Jun-2026 21:29:14","00:11","96","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","39","1","","08-Jun-2026 ","08-Jun-2026 18:48:04","08-Jun-2026 18:48:19","00:15","97","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","03-Mar-2026 ","04-Mar-2026 11:19:17","04-Mar-2026 11:53:02","33:45","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","8","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","04-Mar-2026 ","04-Mar-2026 18:04:55","04-Mar-2026 18:05:39","00:44","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","05-Mar-2026 ","05-Mar-2026 18:01:34","05-Mar-2026 18:02:10","00:36","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","06-Mar-2026 ","06-Mar-2026 18:01:58","06-Mar-2026 18:02:42","00:44","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","07-Mar-2026 ","07-Mar-2026 18:52:44","07-Mar-2026 18:53:15","00:31","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","08-Mar-2026 ","08-Mar-2026 19:16:55","08-Mar-2026 19:17:25","00:30","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","09-Mar-2026 ","09-Mar-2026 18:03:55","09-Mar-2026 18:04:22","00:27","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","10-Mar-2026 ","10-Mar-2026 22:05:01","10-Mar-2026 22:05:54","00:53","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","11-Mar-2026 ","11-Mar-2026 19:47:14","11-Mar-2026 19:47:34","00:20","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","12-Mar-2026 ","12-Mar-2026 18:09:02","12-Mar-2026 18:09:41","00:39","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","13-Mar-2026 ","14-Mar-2026 18:47:40","14-Mar-2026 18:48:18","00:38","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","14-Mar-2026 ","14-Mar-2026 18:48:43","14-Mar-2026 18:49:08","00:25","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","16-Mar-2026 ","17-Mar-2026 02:08:04","17-Mar-2026 02:08:39","00:35","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","17-Mar-2026 ","17-Mar-2026 18:27:58","17-Mar-2026 18:28:17","00:19","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","18-Mar-2026 ","18-Mar-2026 18:16:46","18-Mar-2026 18:17:14","00:28","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","19-Mar-2026 ","20-Mar-2026 18:47:51","20-Mar-2026 18:48:14","00:23","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","20-Mar-2026 ","20-Mar-2026 18:48:37","20-Mar-2026 18:48:54","00:17","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","21-Mar-2026 ","21-Mar-2026 18:03:32","21-Mar-2026 18:04:08","00:36","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","7","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222004","62","2","","23-Mar-2026 ","24-Mar-2026 01:29:03","24-Mar-2026 01:29:24","00:21","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","04-Mar-2026 ","04-Mar-2026 20:05:13","04-Mar-2026 20:06:22","01:09","1","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","05-Mar-2026 ","05-Mar-2026 20:56:18","05-Mar-2026 20:56:52","00:34","2","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","06-Mar-2026 ","06-Mar-2026 20:35:33","06-Mar-2026 20:36:03","00:30","3","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","07-Mar-2026 ","07-Mar-2026 21:51:03","07-Mar-2026 21:51:41","00:38","4","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","08-Mar-2026 ","08-Mar-2026 21:58:24","08-Mar-2026 21:58:44","00:20","5","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","09-Mar-2026 ","09-Mar-2026 20:43:38","09-Mar-2026 20:43:55","00:17","6","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","10-Mar-2026 ","10-Mar-2026 21:52:06","10-Mar-2026 21:52:26","00:20","7","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","11-Mar-2026 ","11-Mar-2026 21:26:55","11-Mar-2026 21:27:18","00:23","8","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","12-Mar-2026 ","13-Mar-2026 06:06:13","13-Mar-2026 06:06:39","00:26","9","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","13-Mar-2026 ","13-Mar-2026 20:35:28","13-Mar-2026 20:35:47","00:19","10","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","14-Mar-2026 ","14-Mar-2026 21:12:23","14-Mar-2026 21:12:39","00:16","11","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","15-Mar-2026 ","16-Mar-2026 02:24:42","16-Mar-2026 02:25:07","00:25","12","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","16-Mar-2026 ","16-Mar-2026 19:52:46","16-Mar-2026 19:53:05","00:19","13","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","17-Mar-2026 ","17-Mar-2026 20:44:50","17-Mar-2026 20:45:06","00:16","14","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","18-Mar-2026 ","18-Mar-2026 18:02:48","18-Mar-2026 18:03:12","00:24","15","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","19-Mar-2026 ","19-Mar-2026 18:24:13","19-Mar-2026 18:24:27","00:14","16","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","20-Mar-2026 ","20-Mar-2026 20:53:18","20-Mar-2026 20:53:34","00:16","17","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","21-Mar-2026 ","21-Mar-2026 19:58:33","21-Mar-2026 19:58:52","00:19","18","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","22-Mar-2026 ","22-Mar-2026 20:57:32","22-Mar-2026 20:57:48","00:16","19","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","23-Mar-2026 ","23-Mar-2026 20:25:57","23-Mar-2026 20:26:19","00:22","20","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","24-Mar-2026 ","24-Mar-2026 20:00:09","24-Mar-2026 20:00:26","00:17","21","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","25-Mar-2026 ","25-Mar-2026 19:46:02","25-Mar-2026 19:46:19","00:17","22","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","26-Mar-2026 ","26-Mar-2026 20:34:57","26-Mar-2026 20:35:36","00:39","23","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","27-Mar-2026 ","27-Mar-2026 20:26:42","27-Mar-2026 20:27:00","00:18","24","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","6","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","28-Mar-2026 ","28-Mar-2026 19:58:42","28-Mar-2026 19:58:55","00:13","25","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","29-Mar-2026 ","29-Mar-2026 20:46:26","29-Mar-2026 20:46:38","00:12","26","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","30-Mar-2026 ","30-Mar-2026 22:11:21","30-Mar-2026 22:11:35","00:14","27","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","31-Mar-2026 ","31-Mar-2026 22:46:39","31-Mar-2026 22:47:25","00:46","28","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","01-Apr-2026 ","01-Apr-2026 21:38:53","01-Apr-2026 21:39:31","00:38","29","Patient","BYODHandheld","BYODHandheld","0","","1","Yes","0","","0","","5","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","02-Apr-2026 ","02-Apr-2026 21:22:31","02-Apr-2026 21:22:58","00:27","30","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","03-Apr-2026 ","03-Apr-2026 21:13:09","03-Apr-2026 21:13:24","00:15","31","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","04-Apr-2026 ","04-Apr-2026 21:48:32","04-Apr-2026 21:48:47","00:15","32","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","05-Apr-2026 ","05-Apr-2026 22:11:06","05-Apr-2026 22:11:18","00:12","33","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","06-Apr-2026 ","06-Apr-2026 22:24:02","06-Apr-2026 22:24:13","00:11","34","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","07-Apr-2026 ","07-Apr-2026 21:17:38","07-Apr-2026 21:17:49","00:11","35","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","08-Apr-2026 ","09-Apr-2026 19:34:11","09-Apr-2026 19:35:01","00:50","36","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","09-Apr-2026 ","09-Apr-2026 19:35:23","09-Apr-2026 19:35:44","00:21","37","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","10-Apr-2026 ","10-Apr-2026 20:45:38","10-Apr-2026 20:45:51","00:13","38","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","11-Apr-2026 ","11-Apr-2026 21:18:17","11-Apr-2026 21:18:37","00:20","39","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","12-Apr-2026 ","12-Apr-2026 21:52:59","12-Apr-2026 21:53:16","00:17","40","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","13-Apr-2026 ","13-Apr-2026 20:06:14","13-Apr-2026 20:06:28","00:14","41","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","14-Apr-2026 ","14-Apr-2026 20:32:26","14-Apr-2026 20:33:15","00:49","42","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","15-Apr-2026 ","15-Apr-2026 22:47:23","15-Apr-2026 22:47:48","00:25","43","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","16-Apr-2026 ","16-Apr-2026 22:09:53","16-Apr-2026 22:26:28","16:35","44","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","17-Apr-2026 ","17-Apr-2026 21:45:10","17-Apr-2026 21:45:34","00:24","45","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","18-Apr-2026 ","18-Apr-2026 20:13:56","18-Apr-2026 20:14:28","00:32","46","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","19-Apr-2026 ","19-Apr-2026 21:05:25","19-Apr-2026 21:05:47","00:22","47","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","5","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","20-Apr-2026 ","20-Apr-2026 19:50:05","20-Apr-2026 19:50:31","00:26","48","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","21-Apr-2026 ","21-Apr-2026 21:06:17","21-Apr-2026 21:06:38","00:21","49","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","22-Apr-2026 ","22-Apr-2026 21:58:23","22-Apr-2026 21:58:37","00:14","50","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","1","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","23-Apr-2026 ","23-Apr-2026 20:20:07","23-Apr-2026 20:20:30","00:23","51","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","24-Apr-2026 ","24-Apr-2026 21:02:15","24-Apr-2026 21:02:44","00:29","52","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","25-Apr-2026 ","25-Apr-2026 21:46:33","25-Apr-2026 21:46:44","00:11","53","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","26-Apr-2026 ","26-Apr-2026 21:47:59","26-Apr-2026 21:48:16","00:17","54","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","27-Apr-2026 ","27-Apr-2026 22:14:24","27-Apr-2026 22:14:48","00:24","55","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","28-Apr-2026 ","28-Apr-2026 22:15:05","28-Apr-2026 22:15:20","00:15","56","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","29-Apr-2026 ","29-Apr-2026 21:40:35","29-Apr-2026 21:40:53","00:18","57","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","30-Apr-2026 ","30-Apr-2026 22:25:00","30-Apr-2026 22:25:11","00:11","58","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","01-May-2026 ","01-May-2026 22:29:53","01-May-2026 22:30:10","00:17","59","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","02-May-2026 ","02-May-2026 20:46:50","02-May-2026 20:47:02","00:12","60","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","03-May-2026 ","03-May-2026 22:35:34","03-May-2026 22:35:49","00:15","61","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","04-May-2026 ","05-May-2026 05:52:09","05-May-2026 05:52:35","00:26","62","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","05-May-2026 ","05-May-2026 21:58:12","05-May-2026 21:58:35","00:23","63","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","06-May-2026 ","07-May-2026 08:49:07","07-May-2026 08:49:20","00:13","64","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","07-May-2026 ","07-May-2026 21:53:49","07-May-2026 21:54:04","00:15","65","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","08-May-2026 ","08-May-2026 21:54:15","08-May-2026 21:54:26","00:11","66","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","0","No blood seen","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","09-May-2026 ","09-May-2026 21:16:14","09-May-2026 21:16:33","00:19","67","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","10-May-2026 ","10-May-2026 22:31:35","10-May-2026 22:31:45","00:10","68","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","11-May-2026 ","11-May-2026 22:06:26","11-May-2026 22:06:52","00:26","69","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","12-May-2026 ","12-May-2026 21:01:56","12-May-2026 21:02:12","00:16","70","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","13-May-2026 ","13-May-2026 22:30:52","13-May-2026 22:31:05","00:13","71","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","14-May-2026 ","14-May-2026 21:57:20","14-May-2026 21:57:33","00:13","72","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","15-May-2026 ","15-May-2026 22:18:39","15-May-2026 22:18:59","00:20","73","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","16-May-2026 ","16-May-2026 22:35:15","16-May-2026 22:35:26","00:11","74","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","17-May-2026 ","17-May-2026 19:49:55","17-May-2026 19:51:44","01:49","75","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","18-May-2026 ","18-May-2026 20:08:02","18-May-2026 20:08:15","00:13","76","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","19-May-2026 ","19-May-2026 23:07:57","19-May-2026 23:08:13","00:16","77","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","20-May-2026 ","20-May-2026 21:05:24","20-May-2026 21:05:35","00:11","78","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","21-May-2026 ","21-May-2026 22:39:47","21-May-2026 22:40:29","00:42","79","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","22-May-2026 ","22-May-2026 22:47:30","22-May-2026 22:47:42","00:12","80","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","23-May-2026 ","23-May-2026 21:43:46","23-May-2026 21:43:56","00:10","81","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","24-May-2026 ","24-May-2026 22:10:38","24-May-2026 22:10:48","00:10","82","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","25-May-2026 ","25-May-2026 21:58:41","25-May-2026 21:58:55","00:14","83","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","26-May-2026 ","26-May-2026 21:30:28","26-May-2026 21:30:46","00:18","84","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","10","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","27-May-2026 ","27-May-2026 22:00:22","27-May-2026 22:00:36","00:14","85","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","4","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","28-May-2026 ","29-May-2026 19:50:01","29-May-2026 19:50:17","00:16","86","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","29-May-2026 ","29-May-2026 19:50:40","29-May-2026 19:50:53","00:13","87","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","30-May-2026 ","30-May-2026 22:31:10","30-May-2026 22:31:21","00:11","88","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","31-May-2026 ","31-May-2026 21:51:51","31-May-2026 21:52:13","00:22","89","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","01-Jun-2026 ","01-Jun-2026 21:12:38","01-Jun-2026 21:12:51","00:13","90","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","02-Jun-2026 ","02-Jun-2026 22:43:49","02-Jun-2026 22:43:59","00:10","91","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","2","Obvious blood with stool most of the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","03-Jun-2026 ","03-Jun-2026 21:19:57","03-Jun-2026 21:20:07","00:10","92","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","04-Jun-2026 ","04-Jun-2026 22:08:15","04-Jun-2026 22:08:26","00:11","93","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","05-Jun-2026 ","05-Jun-2026 20:59:26","05-Jun-2026 20:59:37","00:11","94","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","06-Jun-2026 ","06-Jun-2026 21:19:49","06-Jun-2026 21:20:02","00:13","95","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","07-Jun-2026 ","07-Jun-2026 22:22:32","07-Jun-2026 22:22:45","00:13","96","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","08-Jun-2026 ","08-Jun-2026 21:21:58","08-Jun-2026 21:22:25","00:27","97","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","3","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" +"77242113UCO3001","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","33","1","","09-Jun-2026 ","09-Jun-2026 21:24:49","09-Jun-2026 21:24:59","00:10","98","Patient","BYODHandheld","BYODHandheld","0","","0","","0","","1","Yes","2","","","1","Streaks of blood with stool less than half the time","","","","Czech (Czech Republic)","3","Handheld","","","Participant","","" diff --git a/Clario/Downloads/Zpracovano/2026-06-10_10-14-46 77242113UCO3001 Clario MayoScore.csv b/Clario/Downloads/Zpracovano/2026-06-10_10-14-46 77242113UCO3001 Clario MayoScore.csv new file mode 100644 index 0000000..47d3c0b --- /dev/null +++ b/Clario/Downloads/Zpracovano/2026-06-10_10-14-46 77242113UCO3001 Clario MayoScore.csv @@ -0,0 +1,53 @@ +"Protocol","Study Population","Country","Site","Principal Investigator","Participant ID","Baseline Stool Frequency","Visit","Visit Date","Endoscopy Completed?","Endoscopy Date","Bowel Preparation Start Date 1","Bowel Preparation End Date 1","Bowel Preparation Start Date 2","Bowel Preparation End Date 2","Central Endoscopy Score","Local Endoscopy Score","PGA Score","Eligible Day (-1)","Day (-1) Excluded Reason(s)","Eligible Day (-2)","Day (-2) Excluded Reason(s)","Eligible Day (-3)","Day (-3) Excluded Reason(s)","Eligible Day (-4)","Day (-4) Excluded Reason(s)","Eligible Day (-5)","Day (-5) Excluded Reason(s)","Eligible Day (-6)","Day (-6) Excluded Reason(s)","Eligible Day (-7)","Day (-7) Excluded Reason(s)","Eligible Day (-8)","Day (-8) Excluded Reason(s)","Eligible Day (-9)","Day (-9) Excluded Reason(s)","Eligible Day (-10)","Day (-10) Excluded Reason(s)","Eligible Day (-1) Stool Count","Eligible Day (-2) Stool Count","Eligible Day (-3) Stool Count","Eligible Day (-4) Stool Count","Eligible Day (-5) Stool Count","Eligible Day (-6) Stool Count","Eligible Day (-7) Stool Count","Eligible Day (-8) Stool Count","Eligible Day (-9) Stool Count","Eligible Day (-10) Stool Count","Stool Frequency Sub-score","Eligible Day (-1) Rectal Bleeding Score","Eligible Day (-2) Rectal Bleeding Score","Eligible Day (-3) Rectal Bleeding Score","Eligible Day (-4) Rectal Bleeding Score","Eligible Day (-5) Rectal Bleeding Score","Eligible Day (-6) Rectal Bleeding Score","Eligible Day (-7) Rectal Bleeding Score","Eligible Day (-8) Rectal Bleeding Score","Eligible Day (-9) Rectal Bleeding Score","Eligible Day (-10) Rectal Bleeding Score","Rectal Bleeding Sub-score","Partial Mayo Score","Modified Mayo Score","Full Mayo Score","Site Action","Last Mayo Score Submission","Week I-12 Clinical Responder","Week I-12 Clinical Remission","Clinical Flare","Loss of Response","Partial Mayo Response Post Loss of Response","Partial Mayo Response for Clinical Non-Responders" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-0","19 Feb 2026","Yes","05 Feb 2026","04 Feb 2026","04 Feb 2026","-","-","2","-","3","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Day Not Applicable for Calculation","10 Feb 2026","Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","10","8","7","5","7","8","8","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","7","6","9","-","08 Apr 2026 07:11:25","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-2","04 Mar 2026","-","-","-","-","-","-","-","-","3","03 Mar 2026","-","02 Mar 2026","-","01 Mar 2026","-","28 Feb 2026","-","27 Feb 2026","-","26 Feb 2026","-","25 Feb 2026","-","24 Feb 2026","Day Not Applicable for Calculation","23 Feb 2026","Day Not Applicable for Calculation","22 Feb 2026","Day Not Applicable for Calculation","5","4","5","4","5","6","6","-","-","-","2","1","0","1","0","1","0","1","-","-","-","1","6","","","-","28 May 2026 10:04:05","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-4","18 Mar 2026","-","-","-","-","-","-","-","-","2","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","-","11 Mar 2026","-","10 Mar 2026","Day Not Applicable for Calculation","09 Mar 2026","Day Not Applicable for Calculation","08 Mar 2026","Day Not Applicable for Calculation","5","5","5","4","5","4","5","-","-","-","2","1","0","0","1","1","1","0","-","-","-","1","5","","","-","08 Apr 2026 11:04:49","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-8","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","4","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","28 May 2026 14:42:53","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","I-12","13 May 2026","Yes","06 May 2026","05 May 2026","05 May 2026","-","-","1","-","1","12 May 2026","-","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","Endoscopy","05 May 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","04 May 2026","-","03 May 2026","Day Not Applicable for Calculation","5","4","6","5","5","5","-","-","3","-","2","1","0","1","1","1","1","-","-","1","-","1","4","4","5","-","10 Jun 2026 07:16:05","Clinical Responder","No","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012001","1","M-4","10 Jun 2026","-","-","-","-","-","-","-","-","1","09 Jun 2026","-","08 Jun 2026","-","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","Day Not Applicable for Calculation","01 Jun 2026","Day Not Applicable for Calculation","31 May 2026","Day Not Applicable for Calculation","4","5","3","4","5","4","5","-","-","-","2","0","0","0","0","1","0","1","-","-","-","0","3","","","-","10 Jun 2026 07:15:50","N/A","N/A","No","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-0","08 Apr 2026","Yes","18 Mar 2026","17 Mar 2026","18 Mar 2026","-","-","2","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","Missing Diary","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","-","3","3","4","-","-","-","1","0","0","0","-","0","0","1","-","-","-","0","3","3","5","-","10 Jun 2026 08:42:08","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-2","23 Apr 2026","-","-","-","-","-","-","-","-","2","22 Apr 2026","Missing Diary","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","Day Not Applicable for Calculation","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","-","3","3","6","5","5","4","-","-","-","2","-","0","0","1","1","1","1","-","-","-","1","5","","","-","10 Jun 2026 08:42:33","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-4","06 May 2026","-","-","-","-","-","-","-","-","1","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","6","3","2","3","3","3","3","-","-","-","1","1","0","0","0","1","1","0","-","-","-","0","2","","","-","28 May 2026 14:43:38","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012002","1","I-8","04 Jun 2026","-","-","-","-","-","-","-","-","1","03 Jun 2026","-","02 Jun 2026","-","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","Day Not Applicable for Calculation","26 May 2026","Day Not Applicable for Calculation","25 May 2026","Day Not Applicable for Calculation","3","4","3","3","3","3","4","-","-","-","1","0","0","0","0","0","0","1","-","-","-","0","2","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","1","I-0","27 May 2026","Yes","13 May 2026","12 May 2026","12 May 2026","-","-","3","-","2","26 May 2026","-","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","Day Not Applicable for Calculation","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","6","9","7","8","9","7","8","-","-","-","3","2","2","2","2","1","1","1","-","-","-","2","7","8","10","-","27 May 2026 07:24:39","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10001","Matej Falc","CZ100012003","1","I-2","10 Jun 2026","-","-","-","-","-","-","-","-","2","09 Jun 2026","-","08 Jun 2026","-","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","Day Not Applicable for Calculation","01 Jun 2026","Day Not Applicable for Calculation","31 May 2026","Day Not Applicable for Calculation","7","8","8","7","6","8","6","-","-","-","3","2","2","1","2","2","2","1","-","-","-","2","7","","","-","10 Jun 2026 07:30:18","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10003","Leksa Vaclav","CZ100032001","2","I-0","10 Jun 2026","Yes","27 May 2026","26 May 2026","26 May 2026","-","-","2","-","2","09 Jun 2026","Missing Diary","08 Jun 2026","-","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","Day Not Applicable for Calculation","01 Jun 2026","Day Not Applicable for Calculation","31 May 2026","Day Not Applicable for Calculation","-","4","4","4","5","4","5","-","-","-","1","-","2","2","2","2","2","2","-","-","-","2","5","5","7","-","10 Jun 2026 08:48:09","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-0","20 Mar 2026","Yes","19 Feb 2026","-","-","-","-","3","-","3","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","-","15 Mar 2026","-","14 Mar 2026","-","13 Mar 2026","-","12 Mar 2026","Day Not Applicable for Calculation","11 Mar 2026","Day Not Applicable for Calculation","10 Mar 2026","Day Not Applicable for Calculation","7","7","8","8","7","8","5","-","-","-","3","2","1","1","1","1","1","0","-","-","-","1","7","7","10","-","20 Mar 2026 07:03:23","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","Medication For Diarrhea","06 Apr 2026","Medication For Diarrhea","05 Apr 2026","Medication For Diarrhea","04 Apr 2026","Medication For Diarrhea","03 Apr 2026","Medication For Diarrhea","02 Apr 2026","Medication For Diarrhea","01 Apr 2026","Medication For Diarrhea","31 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","30 Mar 2026","Medication For Diarrhea;Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","-","-","-","-","-","-","-","-","-","-","Non-Evaluable","Non-Evaluable","Non-Evaluable","Non-Evaluable","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-4","15 Apr 2026","-","-","-","-","-","-","-","-","3","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","-","11 Apr 2026","-","10 Apr 2026","-","09 Apr 2026","-","08 Apr 2026","-","07 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","06 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","05 Apr 2026","Medication For Diarrhea;Day Not Applicable for Calculation","9","22","20","19","17","18","18","-","-","-","3","1","3","2","2","2","2","2","-","-","-","2","8","","","-","04 May 2026 22:06:03","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-8","18 May 2026","-","-","-","-","-","-","-","-","2","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","7","5","9","7","7","8","8","-","-","-","3","1","1","1","1","1","1","1","-","-","-","1","6","","","-","04 Jun 2026 21:46:30","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062001","1","I-12","08 Jun 2026","Yes","28 May 2026","-","-","-","-","3","-","3","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","-","01 Jun 2026","Missing Diary","31 May 2026","Day Not Applicable for Calculation","30 May 2026","Day Not Applicable for Calculation","29 May 2026","Day Not Applicable for Calculation","6","5","5","5","7","6","-","-","-","-","3","1","1","0","0","1","0","-","-","-","-","1","7","7","10","-","-","Clinical Nonresponder","No","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","1","I-0","26 May 2026","Yes","14 May 2026","13 May 2026","13 May 2026","-","-","2","-","2","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","8","8","6","7","7","6","7","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","7","7","9","-","29 May 2026 15:45:00","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10006","Michal Konecny","CZ100062002","1","I-2","09 Jun 2026","-","-","-","-","-","-","-","-","2","08 Jun 2026","-","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","-","01 Jun 2026","Day Not Applicable for Calculation","31 May 2026","Day Not Applicable for Calculation","30 May 2026","Day Not Applicable for Calculation","7","8","7","7","7","5","7","-","-","-","3","2","1","1","1","2","2","2","-","-","-","2","7","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-0","05 May 2026","Yes","24 Apr 2026","23 Apr 2026","23 Apr 2026","-","-","2","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","5","5","5","5","5","5","5","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","5","5","7","-","05 May 2026 11:19:40","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-2","19 May 2026","-","-","-","-","-","-","-","-","1","18 May 2026","-","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","Day Not Applicable for Calculation","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","5","4","5","5","5","4","6","-","-","-","2","1","1","1","1","1","1","1","-","-","-","1","4","","","-","19 May 2026 10:38:25","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10009","Jiri Pumprla","CZ100092001","1","I-4","04 Jun 2026","-","-","-","-","-","-","-","-","1","03 Jun 2026","-","02 Jun 2026","-","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","Day Not Applicable for Calculation","26 May 2026","Day Not Applicable for Calculation","25 May 2026","Day Not Applicable for Calculation","2","3","2","3","3","2","3","-","-","-","1","0","0","0","0","0","0","0","-","-","-","0","2","","","-","04 Jun 2026 09:24:54","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-0","07 Apr 2026","Yes","24 Mar 2026","22 Mar 2026","22 Mar 2026","-","-","2","-","2","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","8","11","5","9","11","10","13","-","-","-","3","1","2","2","2","2","2","2","-","-","-","2","7","7","9","-","04 May 2026 08:44:52","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","7","5","6","6","7","8","2","-","-","-","1","1","0","1","1","1","2","0","-","-","-","1","4","","","-","04 May 2026 08:45:07","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-4","07 May 2026","-","-","-","-","-","-","-","-","1","06 May 2026","-","05 May 2026","-","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","Day Not Applicable for Calculation","28 Apr 2026","Day Not Applicable for Calculation","27 Apr 2026","Day Not Applicable for Calculation","8","7","7","8","4","11","7","-","-","-","1","2","1","1","1","0","1","1","-","-","-","1","3","","","-","01 Jun 2026 00:57:35","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10012","Stefan Konecny","CZ100122001","5","I-8","03 Jun 2026","-","-","-","-","-","-","-","-","2","02 Jun 2026","-","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","-","26 May 2026","Day Not Applicable for Calculation","25 May 2026","Day Not Applicable for Calculation","24 May 2026","Day Not Applicable for Calculation","5","9","7","5","5","9","7","-","-","-","1","1","1","1","0","3","0","1","-","-","-","1","4","","","-","03 Jun 2026 17:47:25","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-0","24 Mar 2026","Yes","12 Mar 2026","11 Mar 2026","11 Mar 2026","-","-","2","-","2","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","-","18 Mar 2026","-","17 Mar 2026","-","16 Mar 2026","Day Not Applicable for Calculation","15 Mar 2026","Day Not Applicable for Calculation","14 Mar 2026","Day Not Applicable for Calculation","8","6","5","7","6","7","6","-","-","-","3","1","1","1","0","1","1","1","-","-","-","1","6","6","8","-","05 Apr 2026 22:41:27","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-2","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","5","2","3","6","5","5","5","-","-","-","2","0","0","0","0","1","1","0","-","-","-","0","4","","","-","28 May 2026 23:19:03","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132001","1","I-4","21 Apr 2026","-","-","-","-","-","-","-","-","0","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","4","3","4","3","3","4","4","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","2","","","-","27 May 2026 12:54:41","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-0","12 May 2026","Yes","21 Apr 2026","20 Apr 2026","21 Apr 2026","-","-","2","-","2","11 May 2026","-","10 May 2026","-","09 May 2026","-","08 May 2026","-","07 May 2026","-","06 May 2026","-","05 May 2026","Missing Diary","04 May 2026","Day Not Applicable for Calculation","03 May 2026","Day Not Applicable for Calculation","02 May 2026","Day Not Applicable for Calculation","2","1","1","1","1","2","-","-","-","-","0","0","0","0","0","0","0","-","-","-","-","0","2","2","4","-","28 May 2026 23:19:30","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132002","1","I-2","26 May 2026","-","-","-","-","-","-","-","-","1","25 May 2026","-","24 May 2026","Missing Diary","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","-","19 May 2026","-","18 May 2026","Missing Diary;Day Not Applicable for Calculation","17 May 2026","Day Not Applicable for Calculation","16 May 2026","Day Not Applicable for Calculation","1","-","1","2","1","2","2","-","-","-","1","0","-","0","0","0","0","0","-","-","-","0","2","","","-","28 May 2026 23:19:51","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","1","I-0","02 Jun 2026","Yes","25 May 2026","24 May 2026","24 May 2026","-","-","2","-","2","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","-","26 May 2026","-","25 May 2026","Endoscopy;Missing Diary;Day Not Applicable for Calculation","24 May 2026","Bowel Preparation for Procedure;Missing Diary;Day Not Applicable for Calculation","23 May 2026","Missing Diary;Day Not Applicable for Calculation","8","8","11","10","10","11","6","-","-","-","3","2","2","1","2","1","2","2","-","-","-","2","7","7","9","-","02 Jun 2026 08:17:40","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10013","David Stepek","CZ100132003","1","I-2","10 Jun 2026","-","-","-","-","-","-","-","-","2","09 Jun 2026","-","08 Jun 2026","-","07 Jun 2026","-","06 Jun 2026","-","05 Jun 2026","-","04 Jun 2026","-","03 Jun 2026","-","02 Jun 2026","Day Not Applicable for Calculation","01 Jun 2026","Day Not Applicable for Calculation","31 May 2026","Day Not Applicable for Calculation","9","2","1","4","2","4","2","-","-","-","1","1","1","0","1","1","1","0","-","-","-","1","4","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10016","Robert Mudr","CZ100162001","1","I-0","28 May 2026","Yes","19 May 2026","18 May 2026","19 May 2026","-","-","3","-","3","27 May 2026","-","26 May 2026","-","25 May 2026","-","24 May 2026","-","23 May 2026","-","22 May 2026","-","21 May 2026","-","20 May 2026","Day Not Applicable for Calculation","19 May 2026","Endoscopy;Bowel Preparation for Procedure;Day Not Applicable for Calculation","18 May 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","14","15","15","15","15","15","15","-","-","-","3","2","3","3","2","2","3","3","-","-","-","3","9","9","12","-","28 May 2026 10:19:28","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","Unscheduled 1","04 May 2026","Yes","20 Apr 2026","12 Apr 2026","15 Apr 2026","-","-","2","-","3","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Day Not Applicable for Calculation","5","6","6","7","6","3","3","-","-","-","2","0","0","0","0","0","0","0","-","-","-","0","5","4","7","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","I-0","18 May 2026","Yes","01 May 2026","01 May 2026","01 May 2026","-","-","2","-","3","17 May 2026","-","16 May 2026","-","15 May 2026","-","14 May 2026","-","13 May 2026","-","12 May 2026","-","11 May 2026","-","10 May 2026","Day Not Applicable for Calculation","09 May 2026","Day Not Applicable for Calculation","08 May 2026","Day Not Applicable for Calculation","6","6","6","6","6","6","6","-","-","-","3","0","0","0","0","0","0","0","-","-","-","0","6","5","8","-","18 May 2026 08:39:27","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adolescent","Czech Republic","DD5-CZ10020","Lucie Gonsorcikova","CZ100201001","1","I-2","01 Jun 2026","-","-","-","-","-","-","-","-","3","31 May 2026","-","30 May 2026","Missing Diary","29 May 2026","Missing Diary","28 May 2026","Missing Diary","27 May 2026","-","26 May 2026","-","25 May 2026","-","24 May 2026","Day Not Applicable for Calculation","23 May 2026","Day Not Applicable for Calculation","22 May 2026","Day Not Applicable for Calculation","6","-","-","-","6","6","6","-","-","-","3","0","-","-","-","0","0","0","-","-","-","0","6","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-0","07 Apr 2026","Yes","16 Mar 2026","15 Mar 2026","16 Mar 2026","-","-","3","-","3","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","-","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","28 Mar 2026","Day Not Applicable for Calculation","11","11","10","11","11","10","9","-","-","-","3","2","2","2","2","2","2","2","-","-","-","2","8","8","11","-","20 Apr 2026 09:27:58","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-2","20 Apr 2026","-","-","-","-","-","-","-","-","3","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","-","13 Apr 2026","-","12 Apr 2026","Day Not Applicable for Calculation","11 Apr 2026","Day Not Applicable for Calculation","10 Apr 2026","Day Not Applicable for Calculation","8","7","9","8","8","7","8","-","-","-","3","2","2","1","1","1","2","1","-","-","-","1","7","","","-","20 Apr 2026 09:29:01","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","1","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","6","6","6","6","7","7","6","-","-","-","3","0","0","1","1","1","1","1","-","-","-","1","5","","","-","-","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10021","Martin Bortlik","CZ100212001","1","I-8","02 Jun 2026","-","-","-","-","-","-","-","-","1","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","-","26 May 2026","-","25 May 2026","Day Not Applicable for Calculation","24 May 2026","Day Not Applicable for Calculation","23 May 2026","Day Not Applicable for Calculation","3","4","4","4","5","5","5","-","-","-","2","0","0","0","0","0","1","1","-","-","-","0","3","","","-","02 Jun 2026 14:44:34","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222002","1","I-0","19 Feb 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","18 Feb 2026","-","17 Feb 2026","-","16 Feb 2026","-","15 Feb 2026","-","14 Feb 2026","-","13 Feb 2026","-","12 Feb 2026","-","11 Feb 2026","Endoscopy;Bowel Preparation for Procedure;Day Not Applicable for Calculation","10 Feb 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","09 Feb 2026","Day Not Applicable for Calculation","3","2","2","3","4","3","2","-","-","-","1","1","1","0","0","0","2","2","-","-","-","1","4","4","6","-","19 Feb 2026 15:37:49","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-0","09 Mar 2026","Yes","11 Feb 2026","10 Feb 2026","11 Feb 2026","-","-","2","-","2","08 Mar 2026","-","07 Mar 2026","-","06 Mar 2026","-","05 Mar 2026","-","04 Mar 2026","-","03 Mar 2026","Missing Diary","02 Mar 2026","Missing Diary","01 Mar 2026","Missing Diary;Day Not Applicable for Calculation","28 Feb 2026","Missing Diary;Day Not Applicable for Calculation","27 Feb 2026","Missing Diary;Day Not Applicable for Calculation","7","7","6","6","7","-","-","-","-","-","3","2","2","2","2","2","-","-","-","-","-","2","7","7","9","-","24 Mar 2026 14:23:10","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-2","27 Mar 2026","-","-","-","-","-","-","-","-","2","26 Mar 2026","-","25 Mar 2026","-","24 Mar 2026","-","23 Mar 2026","-","22 Mar 2026","-","21 Mar 2026","-","20 Mar 2026","-","19 Mar 2026","Day Not Applicable for Calculation","18 Mar 2026","Day Not Applicable for Calculation","17 Mar 2026","Day Not Applicable for Calculation","7","3","3","3","5","5","5","-","-","-","2","0","0","1","1","1","1","2","-","-","-","1","5","","","-","08 Apr 2026 07:36:56","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-4","08 Apr 2026","-","-","-","-","-","-","-","-","2","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","-","31 Mar 2026","Day Not Applicable for Calculation","30 Mar 2026","Day Not Applicable for Calculation","29 Mar 2026","Day Not Applicable for Calculation","3","3","4","4","5","4","3","-","-","-","2","1","0","0","2","1","1","2","-","-","-","1","5","","","-","08 Apr 2026 07:59:35","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-8","04 May 2026","-","-","-","-","-","-","-","-","2","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","-","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","24 Apr 2026","Missing Diary;Day Not Applicable for Calculation","3","5","3","3","3","2","3","-","-","-","1","0","0","0","0","0","0","0","-","-","-","0","3","","","-","04 May 2026 08:08:40","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222003","1","I-12","01 Jun 2026","Yes","20 May 2026","19 May 2026","20 May 2026","-","-","3","-","2","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","-","26 May 2026","-","25 May 2026","-","24 May 2026","Day Not Applicable for Calculation","23 May 2026","Day Not Applicable for Calculation","22 May 2026","Day Not Applicable for Calculation","4","4","6","3","3","3","3","-","-","-","2","1","1","2","1","1","1","2","-","-","-","1","5","6","8","-","01 Jun 2026 14:25:57","Clinical Nonresponder","No","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-0","09 Apr 2026","Yes","08 Apr 2026","31 Mar 2026","01 Apr 2026","-","-","2","-","2","08 Apr 2026","Endoscopy","07 Apr 2026","-","06 Apr 2026","-","05 Apr 2026","-","04 Apr 2026","-","03 Apr 2026","-","02 Apr 2026","-","01 Apr 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","31 Mar 2026","Bowel Preparation for Procedure;Day Not Applicable for Calculation","30 Mar 2026","-","-","3","3","4","3","4","3","-","-","3","1","-","2","2","2","2","2","2","-","-","2","2","5","5","7","-","29 May 2026 11:07:08","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-2","22 Apr 2026","-","-","-","-","-","-","-","-","2","21 Apr 2026","-","20 Apr 2026","-","19 Apr 2026","-","18 Apr 2026","-","17 Apr 2026","-","16 Apr 2026","-","15 Apr 2026","-","14 Apr 2026","Day Not Applicable for Calculation","13 Apr 2026","Day Not Applicable for Calculation","12 Apr 2026","Day Not Applicable for Calculation","3","3","5","3","2","3","2","-","-","-","1","1","2","2","1","1","1","2","-","-","-","1","4","","","-","05 May 2026 07:29:35","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-4","05 May 2026","-","-","-","-","-","-","-","-","2","04 May 2026","-","03 May 2026","-","02 May 2026","-","01 May 2026","-","30 Apr 2026","-","29 Apr 2026","-","28 Apr 2026","-","27 Apr 2026","Day Not Applicable for Calculation","26 Apr 2026","Day Not Applicable for Calculation","25 Apr 2026","Day Not Applicable for Calculation","4","2","2","2","2","2","2","-","-","-","1","1","1","1","1","2","1","1","-","-","-","1","4","","","-","05 May 2026 07:28:55","N/A","N/A","N/A","N/A","N/A","N/A" +"77242113UCO3001","Adult","Czech Republic","DD5-CZ10022","Petr Hrabak","CZ100222005","1","I-8","02 Jun 2026","-","-","-","-","-","-","-","-","2","01 Jun 2026","-","31 May 2026","-","30 May 2026","-","29 May 2026","-","28 May 2026","-","27 May 2026","-","26 May 2026","-","25 May 2026","Day Not Applicable for Calculation","24 May 2026","Day Not Applicable for Calculation","23 May 2026","Day Not Applicable for Calculation","2","2","2","2","2","4","10","-","-","-","1","2","1","2","1","2","2","2","-","-","-","2","5","","","-","02 Jun 2026 08:18:08","N/A","N/A","N/A","N/A","N/A","N/A" diff --git a/EmailsImport/Trash/jnj_tower_ingest_v1.0.md b/EmailsImport/Trash/jnj_tower_ingest_v1.0.md new file mode 100644 index 0000000..2336906 --- /dev/null +++ b/EmailsImport/Trash/jnj_tower_ingest_v1.0.md @@ -0,0 +1,83 @@ +# jnj_tower_ingest v1.0.0 + +**Soubor:** `jnj_tower_ingest_v1.0.py` +**Datum:** 2026-06-10 +**Autor:** vladimir.buzalka +**Běží:** Docker kontejner `python-runner` na Unraid Tower (192.168.1.76), u MongoDB. + +## Co to je + +Sjednocený **Tower-side ingest** JNJ e-mailů — spojuje dvě dříve oddělené poloviny +do jednoho běhu: + +| Fáze | Dříve samostatně | Co dělá | +|---|---|---| +| **1. PARSE** | `parse_emails_tower_v1.3.py` | `.msg` z `/mnt/JNJEMAILS` → bohatý dokument v Mongo `emaily."vbuzalka@its.jnj.com"` (tělo, přílohy, hlavičky, MAPI props). `_id` = Internet Message-ID. | +| **2. SYNC** | `sync_jnj_state_v1.0.py` | nejnovější `/mnt/JNJEMAILS/db/jnjemails_*.db` (SQLite, **jen čtení** `mode=ro`) → zrcadlo do `jnj_messages` + doplnění `jnj_folder`/stavu do `emaily`. | + +**Pořadí: parse BĚŽÍ PŘED sync.** Tím čerstvě naparsované maily dostanou cestu hned ve +stejném běhu (dřív: když sync předběhl parse, nový mail neměl co matchnout — sync +nezakládá stuby). Spojovací klíč všude = **Internet Message-ID = Mongo `_id`**. + +## Inkrementálnost (vhodné pro cron každých 5 min) + +- **PARSE** — parsuje jen `.msg` s `mtime` novějším než watermark + (`jnj_sync_state` / `_id="parse_state"` → `last_parse_mtime`). + - **První běh = seed:** watermark chybí → kandidáti = soubory, jejichž `filename` + ještě není v Mongu (jednorázový `distinct("filename")`); poté se watermark + nastaví na nejnovější soubor. + - **Další běhy = incremental:** jen `mtime > watermark`. Žádný sken Monga. + - `--full` reparsuje vše (upsert, idempotentní). + - **Indexy** se vytvářejí jen při `full`/`seed`/`--reindex` (v incremental už existují). +- **SYNC** — watermark `updated_at` (`jnj_sync_state` / `_id="watermark"`) + zkratka + `last_db` (stejná SQLite jako minule → okamžitý no-op, nesahá na Mongo data). + +Dvě nezávislé události (nová `.msg` / nová `.db`) → skript udělá jen tu fázi, co má +práci; jinak levný no-op. + +## Argumenty + +| Argument | Význam | +|---|---| +| `--dry-run` | nic nezapíše, jen plán obou fází | +| `--full` | parse: reparsuj vše; sync: ignoruj watermark | +| `--limit N` | max N souborů (parse) / řádků (sync) — test | +| `--reindex` | vynutí indexy po parse fázi | +| `--force` | sync: ignoruj zkratku `last_db` | +| `--parse-only` | jen fáze PARSE | +| `--sync-only` | jen fáze SYNC | + +## Spouštění + +```bash +# Test: +docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.0.py --dry-run +# Ostrý inkrementální běh (volá ho cron): +docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.0.py +# Plný reparse + reindex: +docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.0.py --full --reindex +``` + +## Plánování (HOTOVO) + +Unraid User Scripts úloha `jnj_state_sync` (cron `*/5 * * * *`) — wrapper s `flock` +volá `docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.0.py`. +Loguje jen reálnou práci/chyby do `/mnt/user/Scripts/logs/jnj_tower_ingest.log` +(grep `Zapisuji|PARSE hotovo|SYNC hotovo|CHYBA|Traceback`). Cron řádek/rozvrh se při +přepnutí ze `sync_jnj_state` neměnil — jen obsah wrapperu. + +## Revert + +Staré skripty `parse_emails_tower_v1.3.py` a `sync_jnj_state_v1.0.py` zůstávají v +`/scripts/` jako pojistka. Návrat = přepsat wrapper zpět na `sync_jnj_state_v1.0.py`. + +## Závislosti + +`extract-msg==0.55.0`, `olefile`, `pymongo`, `python-dateutil`, `sqlite3` (stdlib). +Python 3.10+. + +## Historie verzí + +- **1.0.0** 2026-06-10 — sjednocení `parse_emails_tower_v1.3` + `sync_jnj_state_v1.0`; + parse zinkrementálněn přes mtime watermark; indexy jen při full/seed/`--reindex`; + pořadí parse→sync. diff --git a/EmailsImport/Trash/jnj_tower_ingest_v1.0.py b/EmailsImport/Trash/jnj_tower_ingest_v1.0.py new file mode 100644 index 0000000..9d32939 --- /dev/null +++ b/EmailsImport/Trash/jnj_tower_ingest_v1.0.py @@ -0,0 +1,1019 @@ +""" +jnj_tower_ingest v1.0 +Nazev: jnj_tower_ingest_v1.0.py +Verze: 1.0.0 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Popis: + Sjednoceny Tower-side ingest JNJ e-mailu. Spojuje dve drive oddelene + poloviny do jednoho behu (oba bezi v kontejneru python-runner u Monga): + + FAZE 1 — PARSE (drive parse_emails_tower_v1.3.py): + .msg soubory z /mnt/JNJEMAILS -> dokument v Mongo + emaily."vbuzalka@its.jnj.com" (bohata extrakce: telo, prilohy, + hlavicky, MAPI props, ...). _id = Internet Message-ID. + INKREMENTALNE: parsuje jen soubory novejsi nez mtime watermark + (jnj_sync_state/_id="parse_state"). Prvni beh = seed dle filename + v Mongu. --full reparsuje vse. + + FAZE 2 — SYNC (drive sync_jnj_state_v1.0.py): + nejnovejsi /mnt/JNJEMAILS/db/jnjemails_*.db (SQLite, JEN CTENI ro) + -> zrcadlo do Mongo kolekce 'jnj_messages' (upsert) + -> doplneni cesty/stavu do emaily."vbuzalka@its.jnj.com": + jnj_folder = COALESCE(jnj_folder, folder) + jnj_is_read, jnj_not_in_mailbox, jnj_left_mailbox_at, + jnj_folder_synced_at (match _id==message_id, fallback + filename; BEZ upsertu — nezakladame stuby). + Inkrementalne pres watermark updated_at (jnj_sync_state/_id= + "watermark") + zkratka last_db (stejna DB -> hned no-op). + + PORADI: parse BEZI PRED sync. Tim cerstve naparsovane maily dostanou + cestu hned ve stejnem behu (drive: pokud sync predbehl parse, novy mail + nemel co matchnout). Dve nezavisle udalosti (nova .msg / nova .db) -> + skript udela jen tu fazi, co ma praci; jinak levny no-op (vhodne pro + cron kazdych 5 minut). + + Spojovaci klic vsude = Internet Message-ID = Mongo _id. + +Prostredi: + Docker container "python-runner" na Unraid Tower. + /mnt/user/JNJEMAILS -> /mnt/JNJEMAILS (.msg v rootu, .db v db/) + MongoDB 192.168.1.76:27017 (externi). + +Argumenty: + --dry-run nic nezapise, jen spocita a vypise plan obou fazi + --full parse: reparsuj vse; sync: ignoruj watermark + --limit N max N souboru (parse) / radku (sync) — test + --reindex vynut vytvoreni indexu na konci parse faze + --force sync: ignoruj zkratku last_db (zpracuj i hotovou DB) + --parse-only spust jen fazi PARSE + --sync-only spust jen fazi SYNC + +Spousteni (v kontejneru python-runner): + # Test: + docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.0.py --dry-run + # Ostry inkrementalni beh (cron): + docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.0.py + # Plny reparse + reindex: + docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.0.py --full --reindex + +Zavislosti (v image python-runner): + extract-msg==0.55.0, olefile, pymongo, python-dateutil, sqlite3 (stdlib). + Python 3.10+. + +Historie verzi: + 1.0.0 2026-06-10 Sjednoceni parse_emails_tower_v1.3 + sync_jnj_state_v1.0 + do jedineho skriptu. Parse zinkrementalnen pres mtime + watermark (drive scan celeho adresare kazdy beh). + Indexy jen pri full/seed/--reindex. Poradi parse->sync. +""" + +import sys +import os +import re +import glob +import logging +import argparse +import base64 +import struct +import sqlite3 +from pathlib import Path +from datetime import datetime, timezone +from typing import Optional + +import extract_msg +from extract_msg.enums import ErrorBehavior +import olefile +from dateutil import parser as dtparser +from pymongo import MongoClient, UpdateOne, ASCENDING, TEXT + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +MSGS_DIR = Path("/mnt/JNJEMAILS") +DB_DIR = "/mnt/JNJEMAILS/db" +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +EMAILS_COL = "vbuzalka@its.jnj.com" +MIRROR_COL = "jnj_messages" +STATE_COL = "jnj_sync_state" +BATCH_SIZE = 200 +LOG_FILE = Path(__file__).parent / "jnj_tower_ingest_errors.log" +SCRIPT_VERSION = "1.0.0" + +# Sloupce zrcadlene ze SQLite messages -> jnj_messages +ROW_COLS = ["message_id", "subject", "sender", "received_at", "folder", + "jnj_folder", "is_read", "not_in_mailbox_anymore", "left_mailbox_at", + "entry_id", "graph_id", "updated_at", "source"] +# ────────────────────────────────────────────────────────────────────────────── + +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + + +# ══════════════════════════════════════════════════════════════════════════════ +# FAZE 1 — PARSE (.msg -> Mongo emaily) [drive parse_emails_tower_v1.3.py] +# ══════════════════════════════════════════════════════════════════════════════ + +def safe(obj, *attrs, default=None): + """Bezpecne cteni atributu — vrati prvni non-None hodnotu.""" + for attr in attrs: + try: + val = getattr(obj, attr, None) + if val is None: + continue + if isinstance(val, str) and not val.strip(): + continue + return val + except Exception: + continue + return default + + +def parse_date(raw) -> Optional[datetime]: + """Libovolny datum -> UTC datetime bez tzinfo (pro MongoDB).""" + if raw is None: + return None + if isinstance(raw, datetime): + if raw.tzinfo: + return raw.astimezone(timezone.utc).replace(tzinfo=None) + return raw + try: + dt = dtparser.parse(str(raw)) + if dt.tzinfo: + return dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt + except Exception: + return None + + +_INT64_MIN, _INT64_MAX = -(2 ** 63), 2 ** 63 - 1 + + +def to_bson(val): + """Konvertuje hodnotu na BSON-serializovatelny typ. + + Pozor: BSON umi jen signed int64. Python ma neomezene integery, takze + velke MAPI hodnoty (PR_CHANGE_KEY, FILETIME, 64-bit handle) mimo rozsah + int64 prevadime na string — jinak cely bulk_write spadne na + 'MongoDB can only handle up to 8-byte ints'. + """ + # bool musi byt PRED int (isinstance(True, int) == True) + if isinstance(val, bool): + return val + if isinstance(val, bytes): + return val.hex() if len(val) <= 128 else f"" + if isinstance(val, datetime): + return parse_date(val) + if isinstance(val, int): + return val if _INT64_MIN <= val <= _INT64_MAX else str(val) + if isinstance(val, (str, float, type(None))): + return val + if isinstance(val, list): + return [to_bson(v) for v in val] + try: + iv = int(val) + return iv if _INT64_MIN <= iv <= _INT64_MAX else str(iv) + except Exception: + pass + return str(val) + + +def extract_headers(msg) -> dict: + headers = {} + try: + hdr = msg.header + if not hdr: + return {} + from email.header import decode_header as _dh + + def _decode(v: str) -> str: + try: + parts = _dh(v) + out = "" + for part, enc in parts: + out += part.decode(enc or "utf-8", errors="replace") if isinstance(part, bytes) else part + return out + except Exception: + return v + + for key in set(hdr.keys()): + k = key.lower().replace("-", "_") + vals = [_decode(v) for v in hdr.get_all(key, [])] + headers[k] = vals if len(vals) > 1 else (vals[0] if vals else "") + except Exception as e: + logging.error("extract_headers: %s", e) + return headers + + +def extract_recipients(msg) -> list: + result = [] + type_map = {1: "to", 2: "cc", 3: "bcc"} + try: + for r in msg.recipients: + rtype = getattr(r, "type", 1) + try: + rtype = int(rtype) + except Exception: + try: + rtype = int(rtype.value) + except Exception: + rtype = 1 + rec = { + "type": type_map.get(rtype, "to"), + "email": safe(r, "email", default=""), + "name": safe(r, "name", default=""), + } + result.append(rec) + except Exception as e: + logging.error("extract_recipients: %s", e) + return result + + +def extract_attachments(msg) -> list: + result = [] + try: + for att in msg.attachments: + fname = safe(att, "longFilename", "shortFilename", default="") + if not fname: + continue + size = 0 + try: + d = att.data + size = len(d) if d else 0 + except Exception: + pass + result.append({ + "filename": fname, + "size_bytes": size, + "mime_type": safe(att, "mimetype", "mimeType", default="application/octet-stream"), + "content_id": safe(att, "cid", default=None), + "is_inline": bool(safe(att, "isInline", default=False)), + }) + except Exception as e: + logging.error("extract_attachments: %s", e) + return result + + +def extract_mapi_props(msg) -> dict: + """Vsechny raw MAPI properties jako {0xXXXX: value}.""" + result = {} + try: + props = msg.props + if not hasattr(props, "items"): + return {} + for key, prop in props.items(): + try: + val = to_bson(prop.value) + prop_id = f"0x{key[:4].upper()}" if len(key) >= 4 else f"0x{key.upper()}" + result[prop_id] = val + except Exception: + pass + except Exception as e: + logging.error("extract_mapi_props: %s", e) + return result + + +# ─── Tolerantni otevirani a raw-OLE fallback ───────────────────────────────── +_CPID_TO_CODEC = { + 1250: "cp1250", 1251: "cp1251", 1252: "cp1252", 1253: "cp1253", + 1254: "cp1254", 1255: "cp1255", 1256: "cp1256", 1257: "cp1257", + 1258: "cp1258", 874: "cp874", 932: "shift_jis", 936: "gb2312", + 949: "euc_kr", 950: "big5", 65001: "utf-8", 28591: "iso-8859-1", + 28592: "iso-8859-2", 20127: "ascii", +} + + +def _read_u32_prop(ole, propid): + """Precte 32-bit hodnotu MAPI property z top-level __properties_version1.0.""" + try: + data = ole.openstream("__properties_version1.0").read() + except Exception: + return None + body = data[32:] # 32-bajtova hlavicka top-level property streamu + for i in range(0, len(body) - 16 + 1, 16): + rec = body[i:i + 16] + tag = struct.unpack("> 16) & 0xFFFF) == propid: + return struct.unpack(" Optional[str]: + """Codec dle PR_INTERNET_CPID / PR_MESSAGE_CODEPAGE (jako napoveda, ne dogma).""" + for pid in (0x3FDE, 0x3FFD): # INTERNET_CPID, MESSAGE_CODEPAGE + codec = _CPID_TO_CODEC.get(_read_u32_prop(ole, pid)) + # utf-8/ascii nejsou dobry hint pro 8-bit stream (casto lzou) + if codec and codec not in ("utf-8", "ascii"): + return codec + return None + + +def _cascade_decode(raw: bytes, is_unicode: bool, cpid_codec: Optional[str]) -> str: + """Dekoduje bajty MAPI stringu. Hlavickam se neveri — zkousime striktne + v poradi priorit a vezmeme prvni, co projde bez chyby.""" + if not raw: + return "" + if is_unicode: # PT_UNICODE = utf-16-le + try: + return raw.decode("utf-16-le") + except Exception: + return raw.decode("utf-16-le", errors="replace") + order = ["utf-8"] # utf-8 strict = silny rozlisovac + if cpid_codec: + order.append(cpid_codec) + order += ["cp1250", "cp1252", "gb2312", "big5"] + for enc in order: + try: + return raw.decode(enc, errors="strict") + except Exception: + continue + return raw.decode("latin-1", errors="replace") # nikdy nespadne + + +def _raw_mapi_strings(msg_path: Path) -> dict: + """Cte klicova textova MAPI pole PRIMO z OLE (mimo extract_msg). + Pouzije se jen kdyz extract_msg vrati degradovane pole.""" + out = {"subject": "", "normalized_subject": "", "sender_name": "", + "sender_email": "", "sender_smtp": "", "body_text": "", "body_html": ""} + try: + ole = olefile.OleFileIO(str(msg_path)) + except Exception: + return out + try: + cpid = _detect_cpid(ole) + wanted = { # MAPI tag -> klic v out + "0037": "subject", "0E1D": "normalized_subject", + "0C1A": "sender_name", "5D01": "sender_smtp", + "0C1F": "sender_email", "1000": "body_text", "1013": "body_html", + } + prefix = "__substg1.0_" + found = {} # key -> (priorita_typu, hodnota) + for entry in ole.listdir(): + if len(entry) != 1: # jen top-level (ne vnorene zpravy) + continue + name = entry[0] + if not name.startswith(prefix): + continue + tag = name[len(prefix):len(prefix) + 4].upper() + key = wanted.get(tag) + if not key: + continue + typ = name[-4:].upper() + prio = {"001F": 3, "001E": 2, "0102": 1}.get(typ, 0) + if prio == 0: + continue + prev = found.get(key) + if prev and prev[0] >= prio: # preferuj unicode > ansi > binarni + continue + try: + raw = ole.openstream(entry).read() + val = _cascade_decode(raw, typ == "001F", cpid) + except Exception: + continue + found[key] = (prio, val) + for key, (_, val) in found.items(): + out[key] = val + finally: + ole.close() + return out + + +def _degraded(s) -> bool: + """Pole je degradovane: prazdne nebo obsahuje U+FFFD (nahradni znak).""" + return (not s) or ("�" in s) + + +def open_message(msg_path: Path): + """Kaskadove otevreni .msg -> (msg, mode) nebo (None, None).""" + try: + return extract_msg.Message(str(msg_path)), "normal" + except Exception: + pass + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL), "suppress_all" + except Exception: + pass + encs = [] + try: + ole = olefile.OleFileIO(str(msg_path)) + c = _detect_cpid(ole) + ole.close() + if c: + encs.append(c) + except Exception: + pass + for e in encs + ["cp1250", "cp1252"]: + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL, + overrideEncoding=e), f"override:{e}" + except Exception: + continue + return None, None + + +def extract_message(msg_path: Path) -> Optional[dict]: + """Parsuje jeden .msg soubor -> MongoDB dokument.""" + msg, parse_mode = open_message(msg_path) + if msg is None: + logging.error("open failed [%s]: vsechny pokusy o otevreni selhaly", msg_path.name) + return None + + try: + # ── Message-ID ──────────────────────────────────────────────── + mid = None + for attr in ("messageId", "message_id", "internetMessageId"): + mid = safe(msg, attr) + if mid: + break + if not mid: + mid = f"filename:{msg_path.stem}" + mid = str(mid).strip() + + # ── Predmet ─────────────────────────────────────────────────── + try: + subject = msg.subject or "" + except Exception: + subject = "" + + normalized_subject = safe(msg, "normalizedSubject", "normalized_subject", default="") + + # ── Telo ────────────────────────────────────────────────────── + try: + body_text = msg.body or "" + except Exception: + body_text = "" + + body_html = None + try: + bh = msg.htmlBody + if isinstance(bh, bytes): + bh = bh.decode("utf-8", errors="replace") + if bh: + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + except Exception: + pass + + # ── Odesilatel ──────────────────────────────────────────────── + try: + sender_email = msg.sender or "" + except Exception: + sender_email = "" + + sender_name = safe(msg, "senderName", "sender_name", default="") + sender_smtp = safe(msg, "senderSmtpAddress", "sent_representing_smtp_address", default="") + + # ── Prijemci ────────────────────────────────────────────────── + recipients = extract_recipients(msg) + + try: + to_raw = msg.to or "" + except Exception: + to_raw = "" + try: + cc_raw = msg.cc or "" + except Exception: + cc_raw = "" + try: + bcc_raw = getattr(msg, "bcc", None) or "" + except Exception: + bcc_raw = "" + + display_to = safe(msg, "displayTo", "display_to", default="") + display_cc = safe(msg, "displayCc", "display_cc", default="") + + # ── Casy ────────────────────────────────────────────────────── + try: + received_at = parse_date(msg.date) + except Exception: + received_at = None + + sent_at = None + for attr in ("clientSubmitTime", "client_submit_time", "sentOn"): + v = safe(msg, attr) + if v: + sent_at = parse_date(v) + break + + # ── MAPI vlastnosti ─────────────────────────────────────────── + importance = 1 + try: + v = msg.importance + if v is not None: + importance = int(v) + except Exception: + pass + + sensitivity = 0 + try: + v = getattr(msg, "sensitivity", None) + if v is not None: + sensitivity = int(v) + except Exception: + pass + + flag_status = 0 + try: + v = safe(msg, "flagStatus", "flag_status") + if v is not None: + flag_status = int(v) + except Exception: + pass + + conversation_topic = safe(msg, "conversationTopic", "conversation_topic", default="") + + conversation_index = "" + try: + ci = safe(msg, "conversationIndex", "conversation_index") + if isinstance(ci, bytes): + conversation_index = base64.b64encode(ci).decode() + elif ci: + conversation_index = str(ci) + except Exception: + pass + + in_reply_to = safe(msg, "inReplyTo", "in_reply_to", default="") + + internet_refs = [] + try: + refs = safe(msg, "internetReferences", "internet_references") + if isinstance(refs, list): + internet_refs = refs + elif isinstance(refs, str) and refs: + internet_refs = [r.strip() for r in refs.split() if r.strip()] + except Exception: + pass + + categories = [] + try: + cats = safe(msg, "categories") + if isinstance(cats, list): + categories = [str(c) for c in cats if c] + elif isinstance(cats, str) and cats: + categories = [c.strip() for c in re.split(r"[;,]", cats) if c.strip()] + except Exception: + pass + + read_receipt = bool(safe(msg, "readReceiptRequested", "read_receipt_requested", default=False)) + delivery_receipt = bool(safe(msg, "deliveryReceiptRequested", "delivery_receipt_requested", default=False)) + + # ── Internet headers ────────────────────────────────────────── + headers = extract_headers(msg) + + if not in_reply_to: + in_reply_to = headers.get("in_reply_to", "") + if not internet_refs: + refs_str = headers.get("references", "") + if isinstance(refs_str, str) and refs_str: + internet_refs = [r.strip() for r in refs_str.split() if r.strip()] + + # ── Prilohy ─────────────────────────────────────────────────── + attachments = extract_attachments(msg) + + # ── Raw MAPI ────────────────────────────────────────────────── + mapi_raw = extract_mapi_props(msg) + + msg.close() + + # ── Raw-OLE fallback pro degradovana textova pole ───────────── + parse_degraded = parse_mode != "normal" + forced = parse_mode != "normal" + if (forced or _degraded(subject) or _degraded(body_text) + or _degraded(sender_email) or (body_html and "�" in body_html)): + raw = _raw_mapi_strings(msg_path) + if raw["subject"] and (forced or _degraded(subject)): + subject = raw["subject"] + if raw["normalized_subject"] and (forced or _degraded(normalized_subject)): + normalized_subject = raw["normalized_subject"] + if raw["body_text"] and (forced or _degraded(body_text)): + body_text = raw["body_text"] + if raw["body_html"] and (forced or not body_html or "�" in body_html): + bh = raw["body_html"] + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + if (raw["sender_smtp"] or raw["sender_email"]) and (forced or _degraded(sender_email)): + sender_email = raw["sender_smtp"] or raw["sender_email"] + if raw["sender_name"] and (forced or _degraded(sender_name)): + sender_name = raw["sender_name"] + if raw["sender_smtp"] and not sender_smtp: + sender_smtp = raw["sender_smtp"] + + # ── Dokument ────────────────────────────────────────────────── + return { + "_id": mid, + "filename": msg_path.name, + + "subject": subject, + "normalized_subject": normalized_subject, + "importance": importance, + "sensitivity": sensitivity, + "flag_status": flag_status, + "read_receipt_requested": read_receipt, + "delivery_receipt_requested": delivery_receipt, + "has_attachments": len(attachments) > 0, + "attachment_count": len(attachments), + "message_size_bytes": msg_path.stat().st_size, + + "conversation_topic": conversation_topic, + "conversation_index": conversation_index, + "in_reply_to": in_reply_to, + "internet_references": internet_refs, + "categories": categories, + + "received_at": received_at, + "sent_at": sent_at, + + "sender": { + "email": sender_email, + "name": sender_name, + "smtp": sender_smtp, + }, + "to": to_raw, + "cc": cc_raw, + "bcc": bcc_raw, + "display_to": display_to, + "display_cc": display_cc, + "recipients": recipients, + + "body_text": body_text, + "body_html": body_html, + + "attachments": attachments, + "headers": headers, + "mapi": mapi_raw, + + "parse_mode": parse_mode, + "parse_degraded": parse_degraded, + + "parsed_at": datetime.now(timezone.utc).replace(tzinfo=None), + } + + except Exception as e: + logging.error("extract_message failed [%s]: %s", msg_path.name, e) + return None + + +def create_indexes(col): + print(" Vytvarim indexy...") + col.create_index([("received_at", ASCENDING)]) + col.create_index([("sent_at", ASCENDING)]) + col.create_index([("sender.email", ASCENDING)]) + col.create_index([("filename", ASCENDING)], unique=True, sparse=True) + col.create_index([("conversation_topic", ASCENDING)]) + col.create_index([("has_attachments", ASCENDING)]) + col.create_index([("categories", ASCENDING)]) + col.create_index([("importance", ASCENDING)]) + col.create_index([("flag_status", ASCENDING)]) + col.create_index([ + ("subject", TEXT), + ("body_text", TEXT), + ("to", TEXT), + ("cc", TEXT), + ], name="text_search", default_language="none") + print(" Indexy hotovy.") + + +def run_parse(col, state_col, args, now) -> dict: + """FAZE 1: inkrementalni parse .msg -> emaily. Vraci statistiku.""" + stats = {"mode": None, "total_files": 0, "candidates": 0, "ok": 0, "err": 0} + print("\n=== FAZE 1: PARSE (.msg -> emaily) ===") + + all_files = sorted(MSGS_DIR.glob("*.msg")) + stats["total_files"] = len(all_files) + if not all_files: + print(" Zadne .msg ve zdroji -> preskakuji.") + return stats + max_mtime = max(f.stat().st_mtime for f in all_files) + + ps = state_col.find_one({"_id": "parse_state"}) or {} + last_mtime = ps.get("last_parse_mtime") + + if args.full: + candidates = all_files + mode = "full" + elif last_mtime is None: + print(" Prvni beh (zadny mtime watermark) -> seed dle filename v Mongu...") + existing = set(col.distinct("filename")) + candidates = [f for f in all_files if f.name not in existing] + mode = "seed" + print(f" V Mongu jiz {len(existing)} filename; nove k naparsovani: {len(candidates)}") + else: + candidates = [f for f in all_files if f.stat().st_mtime > last_mtime] + mode = "incremental" + if args.limit: + candidates = candidates[:args.limit] + + stats["mode"] = mode + stats["candidates"] = len(candidates) + wm_str = datetime.fromtimestamp(last_mtime).strftime("%Y-%m-%d %H:%M:%S") if last_mtime else "(zadny)" + print(f" Rezim: {mode} | .msg celkem {len(all_files)} | watermark {wm_str} | ke zpracovani {len(candidates)}") + + if not candidates: + print(" Nic noveho k parsovani.") + # I tak posun watermark na nejnovejsi soubor (krome --full a dry-run) + if not args.dry_run and mode != "full": + state_col.update_one({"_id": "parse_state"}, + {"$set": {"last_parse_mtime": max_mtime, "last_parse_at": now}}, upsert=True) + return stats + + if args.dry_run: + print(f" DRY-RUN: naparsoval bych {len(candidates)} souboru (Mongo se nemeni). Ukazka:") + for f in candidates[:10]: + mt = datetime.fromtimestamp(f.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") + print(f" + {f.name} (mtime {mt})") + if len(candidates) > 10: + print(f" ... a dalsich {len(candidates) - 10}") + return stats + + batch = [] + verbose = len(candidates) <= 30 + + def flush(): + if not batch: + return + try: + col.bulk_write(batch, ordered=False) + except Exception as e: + logging.error("bulk_write spadl (%s) -- prepinam na per-dokument", e) + print(f" CHYBA bulk_write: {e} -- zkousim per-dokument") + for op in batch: + try: + col.bulk_write([op], ordered=False) + except Exception as e2: + try: + bad_id = getattr(op, "_filter", {}).get("_id", "?") + except Exception: + bad_id = "?" + logging.error("per-dokument selhal [_id=%s]: %s", bad_id, e2) + print(f" ZAHOZEN _id={bad_id}: {e2}") + stats["ok"] -= 1 + stats["err"] += 1 + batch.clear() + + for i, msg_path in enumerate(candidates, 1): + doc = extract_message(msg_path) + if doc is None: + stats["err"] += 1 + else: + batch.append(UpdateOne({"_id": doc["_id"]}, {"$set": doc}, upsert=True)) + stats["ok"] += 1 + if len(batch) >= BATCH_SIZE: + flush() + if verbose: + status = "ERR " if doc is None else "OK " + subj = (doc.get("subject") or "")[:60] if doc else "?" + print(f" {i:>5}/{len(candidates)} {status} {subj}") + elif i % 500 == 0: + print(f" prubeh {i}/{len(candidates)} ok={stats['ok']} err={stats['err']}") + flush() + + # Indexy jen pri full/seed/--reindex (v inkrementalnim behu uz existuji) + if mode in ("full", "seed") or args.reindex: + create_indexes(col) + + # Posun watermark na nejnovejsi soubor + state_col.update_one({"_id": "parse_state"}, + {"$set": {"last_parse_mtime": max_mtime, "last_parse_at": now, + "last_parsed_count": stats["ok"], "last_parse_mode": mode}}, + upsert=True) + print(f" PARSE hotovo: ok={stats['ok']} err={stats['err']} " + f"watermark={datetime.fromtimestamp(max_mtime):%Y-%m-%d %H:%M:%S}") + return stats + + +# ══════════════════════════════════════════════════════════════════════════════ +# FAZE 2 — SYNC (SQLite -> Mongo jnj_messages + emaily cesta) +# [drive sync_jnj_state_v1.0.py] +# ══════════════════════════════════════════════════════════════════════════════ + +def norm_mid(s: str) -> str: + return (s or "").strip().strip("<>").strip() + + +def coalesce_path(jnjf, fld) -> str: + return jnjf if (jnjf and jnjf.strip()) else (fld or "") + + +def newest_db(): + cands = glob.glob(os.path.join(DB_DIR, "jnjemails_*.db")) or glob.glob(os.path.join(DB_DIR, "*.db")) + return max(cands, key=os.path.getmtime) if cands else None + + +def run_sync(db, args, now) -> dict: + """FAZE 2: SQLite -> jnj_messages (zrcadlo) + emaily (cesta/stav).""" + stats = {"total": 0, "matched": 0, "skipped": False} + print("\n=== FAZE 2: SYNC (SQLite -> jnj_messages + emaily cesta) ===") + + emails = db[EMAILS_COL] + state_col = db[STATE_COL] + + db_path = newest_db() + if not db_path: + print(f" Zadna .db v {DB_DIR} -> preskakuji.") + stats["skipped"] = True + return stats + db_name = os.path.basename(db_path) + print(f" SQLite: {db_name}") + + st = state_col.find_one({"_id": "watermark"}) or {} + + # ── Zkratka: tuto DB uz jsme zpracovali? (jen inkrementalni rezim) ───── + if not args.full and not args.force and st.get("last_db") == db_name: + print(f" DB {db_name} uz byla zpracovana (last_db) -> nic na praci.") + stats["skipped"] = True + return stats + + wm = None if args.full else st.get("last_updated_at") + print(f" Watermark: {wm or '(zadny -> vse)'}") + + # ── SQLite (read-only) ──────────────────────────────────────────────── + con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + con.row_factory = sqlite3.Row + available = {row[1] for row in con.execute("PRAGMA table_info(messages)")} + sel_cols = [c for c in ROW_COLS if c in available] + missing = [c for c in ROW_COLS if c not in available] + if missing: + print(f" (DB nema sloupce: {', '.join(missing)} -> default None/0)") + has_updated = "updated_at" in available + q = f"SELECT {', '.join(sel_cols)} FROM messages" + params = () + if wm and has_updated: + q += " WHERE updated_at > ?" + params = (wm,) + elif wm and not has_updated: + print(" (DB nema updated_at -> watermark ignorovan, beru vse)") + wm = None + rows = [dict(row) for row in con.execute(q, params).fetchall()] + con.close() + if args.limit: + rows = rows[:args.limit] + total = len(rows) + stats["total"] = total + print(f" Radku ke zpracovani: {total}") + if total == 0: + print(" Neni co synchronizovat (zadne nove radky).") + if not args.dry_run: + state_col.update_one({"_id": "watermark"}, + {"$set": {"last_db": db_name, "synced_at": now}}, upsert=True) + return stats + + # ── Indexy z Monga ──────────────────────────────────────────────────── + print(" Nacitam _id + filename + jnj_folder z Mongo...") + ids_exact = set() + ids_norm = {} + fnames = {} + has_path = set() + for d in emails.find({}, {"_id": 1, "filename": 1, "jnj_folder": 1}): + _id = d["_id"] + ids_exact.add(_id) + ids_norm.setdefault(norm_mid(_id), _id) + fn = d.get("filename") + if fn: + fnames[fn] = _id + if d.get("jnj_folder"): + has_path.add(_id) + print(f" Mongo dokumentu v {EMAILS_COL}: {len(ids_exact)} (z toho s jnj_folder: {len(has_path)})") + + # ── Plan ────────────────────────────────────────────────────────────── + m_exact = m_norm = m_fname = unmatched = 0 + examples = [] + mirror_ops = [] + emaily_ops = [] + max_wm = wm or "" + + for r in rows: + mid = r.get("message_id") + uv = r.get("updated_at") + if uv and uv > max_wm: + max_wm = uv + + # Krok A — zrcadlo (vzdy) + doc = {k: r.get(k) for k in ROW_COLS} + doc["mirrored_at"] = now + mirror_ops.append(UpdateOne({"_id": mid}, {"$set": doc}, upsert=True)) + + # Krok B — match do emaily + target = None + if mid in ids_exact: + target = mid; m_exact += 1 + elif norm_mid(mid) in ids_norm: + target = ids_norm[norm_mid(mid)]; m_norm += 1 + else: + eid = r.get("entry_id") + fn = (eid[-20:] + ".msg") if eid else None + if fn and fn in fnames: + target = fnames[fn]; m_fname += 1 + else: + unmatched += 1 + if len(examples) < 6: + examples.append(mid) + + if target is not None: + setdoc = { + "jnj_folder": coalesce_path(r.get("jnj_folder"), r.get("folder")), + "jnj_is_read": bool(r.get("is_read")), + "jnj_not_in_mailbox": bool(r.get("not_in_mailbox_anymore")), + "jnj_left_mailbox_at": r.get("left_mailbox_at"), + "jnj_folder_synced_at": now, + } + emaily_ops.append(UpdateOne({"_id": target}, {"$set": setdoc})) + + matched = m_exact + m_norm + m_fname + stats["matched"] = matched + print(" --- PLAN ---") + print(f" Zrcadlo -> {MIRROR_COL}: {len(mirror_ops)} upsert") + print(f" Emaily match exact (_id): {m_exact}") + print(f" Emaily match norm (<>): {m_norm}") + print(f" Emaily match filename: {m_fname}") + print(f" Emaily match CELKEM: {matched}/{total} ({100.0*matched/total:.1f}%)") + print(f" NEnamatchovano: {unmatched}") + if examples: + print(" Priklady nenamatchovanych message_id:") + for e in examples: + print(f" {str(e)[:72]}") + + # ── Zapis ───────────────────────────────────────────────────────────── + if args.dry_run: + print(" DRY-RUN: Mongo se NEMENI.") + return stats + + print(" Zapisuji...") + if mirror_ops: + db[MIRROR_COL].bulk_write(mirror_ops, ordered=False) + if emaily_ops: + emails.bulk_write(emaily_ops, ordered=False) + state_col.update_one( + {"_id": "watermark"}, + {"$set": {"last_updated_at": max_wm, "synced_at": now, "last_db": db_name, + "last_total": total, "last_matched": matched}}, + upsert=True, + ) + print(f" SYNC hotovo: zrcadlo={len(mirror_ops)} emaily={len(emaily_ops)} watermark={max_wm}") + return stats + + +# ══════════════════════════════════════════════════════════════════════════════ +# MAIN +# ══════════════════════════════════════════════════════════════════════════════ + +def main(): + ap = argparse.ArgumentParser(description=f"jnj_tower_ingest v{SCRIPT_VERSION}") + ap.add_argument("--dry-run", action="store_true", help="nic nezapise, jen plan") + ap.add_argument("--full", action="store_true", + help="parse: reparsuj vse; sync: ignoruj watermark") + ap.add_argument("--limit", type=int, default=0, help="max N souboru/radku (test)") + ap.add_argument("--reindex", action="store_true", help="vynut indexy po parse") + ap.add_argument("--force", action="store_true", + help="sync: ignoruj last_db zkratku") + ap.add_argument("--parse-only", action="store_true", help="jen faze PARSE") + ap.add_argument("--sync-only", action="store_true", help="jen faze SYNC") + args = ap.parse_args() + + now = datetime.now(timezone.utc).replace(tzinfo=None) + + print(f"=== jnj_tower_ingest v{SCRIPT_VERSION} {'[DRY-RUN]' if args.dry_run else ''} ===") + print(f"Start: {datetime.now():%Y-%m-%d %H:%M:%S}") + print(f"MongoDB: {MONGO_URI} -> {MONGO_DB}") + + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + try: + client.admin.command("ping") + print(" MongoDB OK") + except Exception as e: + print(f"CHYBA: MongoDB nedostupna -- {e}") + sys.exit(1) + + db = client[MONGO_DB] + col = db[EMAILS_COL] + state_col = db[STATE_COL] + + p_stats = s_stats = None + if not args.sync_only: + p_stats = run_parse(col, state_col, args, now) + if not args.parse_only: + s_stats = run_sync(db, args, now) + + # ── Souhrn ──────────────────────────────────────────────────────────── + print("\n=== SOUHRN ===") + if p_stats is not None: + print(f" PARSE: rezim={p_stats['mode']} kandidatu={p_stats['candidates']} " + f"ok={p_stats['ok']} err={p_stats['err']}") + if s_stats is not None: + if s_stats.get("skipped"): + print(" SYNC: preskoceno (zadna nova DB / uz zpracovana)") + else: + print(f" SYNC: radku={s_stats['total']} match={s_stats['matched']}") + print(f"Konec: {datetime.now():%Y-%m-%d %H:%M:%S}") + client.close() + + +if __name__ == "__main__": + main() diff --git a/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.0.py b/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.0.py new file mode 100644 index 0000000..b9a8ae7 --- /dev/null +++ b/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.0.py @@ -0,0 +1,514 @@ +""" +============================================================================== +Skript: 1b_parse_emails_graph_delta_v1.0.py +Verze: 1.0 +Datum: 2026-06-04 +Autor: vladimir.buzalka + +Popis: + Inkrementalni sync emailu pres Microsoft Graph DELTA QUERY. + Sourozenec `1_parse_emails_graph_v1.4.py` — kazdy resi jiny use case: + + 1_parse_emails_graph_v1.4.py = prvni plny import schranky + 1b_parse_emails_graph_delta_v1.0.py = pravidelny sync (zmeny od minula) + + Delta query je server-side change tracking — Graph si pamatuje "zalozku" + (deltaLink) a vraci jen to, co se od ni zmenilo: + - nove zpravy + - zmeny existujicich (isRead, flag, presun do jine slozky, kategorie) + - SMAZANE zpravy (@removed) — definitivne smazane, nikoli v kosi + + Pro mail v "Deleted Items" delta nic specialniho nedela — je to porad + normalni zprava, jen s folder_path="Deleted Items". @removed prijde az + kdyz uzivatel vysype kos / Shift+Del. + +State: + Kolekce `emaily.sync_state`, _id = "|". + { + mailbox, folder_id, folder_path, + delta_link, # plny URL s $deltatoken na pristi beh + last_run_at, + cumulative_new, cumulative_sync, cumulative_removed + } + +Permanentne smazane zpravy: + Skript je NEMAZE z Mongo. Pouze nastavi: + permanently_deleted: True + permanently_deleted_at: + Dohledani: col.find({"permanently_deleted": True}) + +Reuse: + Funkce extract_message / extract_sync_fields se nactou primo z modulu + 1_parse_emails_graph_v1.4.py (importlib, file-based), aby se logika + extrahce nikdy nerozesla. + +Spousteni: + python 1b_parse_emails_graph_delta_v1.0.py # VSECHNY schranky (mimo SKIP_MAILBOXES) + python 1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz # jedna schranka + python 1b_parse_emails_graph_delta_v1.0.py --mailbox ordinace@buzalkova.cz --folder Inbox + python 1b_parse_emails_graph_delta_v1.0.py --reset # zahodit deltaLinky a najet znova + python 1b_parse_emails_graph_delta_v1.0.py --dry-run # nic neulozit + +SKIP_MAILBOXES (hardcoded): + vbuzalka@its.jnj.com — JNJ tenant, nemame Graph API pristup. Pro tuto + schranku je nutny samostatny skript (lokalni .msg). + +Zavislosti: + msal, requests, pymongo, python-dateutil + Python 3.10+ +============================================================================== +""" + +from __future__ import annotations + +import argparse +import importlib.util +import logging +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import msal +import requests +from pymongo import MongoClient, ASCENDING + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +GRAPH_TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9" +GRAPH_CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f" +GRAPH_CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk" +GRAPH_URL = "https://graph.microsoft.com/v1.0" + +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +SYNC_STATE_COL = "sync_state" +PAGE_SIZE = 100 # delta endpoint typicky vraci max 100/stranka +LOG_FILE = Path(__file__).parent / "delta_errors.log" +SCRIPT_VERSION = "1.0" + +# Kolekce v `emaily` ktere NEJSOU mailboxy: +NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state"} + +# Schranky, kde NEMAME Graph API pristup — pri bezneho behu se preskoci. +# Pro tyto je nutny separatni skript (napr. lokalni .msg parser). +SKIP_MAILBOXES = { + "vbuzalka@its.jnj.com", # JNJ tenant — nemame Graph credentials +} + +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + +# Co tahnout z delta endpointu (stejne jako MSG_SELECT v v1.4, mimo internetMessageHeaders +# ktere delta neumi vratit pro vsechny polozky — pro nove zpravy si je dotahneme +# samostatnym fetchem). +DELTA_SELECT = ( + "id,internetMessageId,subject,bodyPreview,body," + "importance,isRead,isDraft,hasAttachments," + "receivedDateTime,sentDateTime,createdDateTime,lastModifiedDateTime," + "sender,from,toRecipients,ccRecipients,bccRecipients,replyTo," + "conversationId,conversationIndex,parentFolderId," + "categories,flag,inferenceClassification" +) + +# Pro plne nacteni nove zpravy (vcetne hlavicek + priloh) pouzijeme stejny +# select+expand jako v1.4 +FULL_FETCH_SELECT = ( + "id,internetMessageId,subject,bodyPreview,body," + "importance,isRead,isDraft,hasAttachments," + "receivedDateTime,sentDateTime,createdDateTime,lastModifiedDateTime," + "sender,from,toRecipients,ccRecipients,bccRecipients,replyTo," + "conversationId,conversationIndex,parentFolderId," + "categories,flag,inferenceClassification,internetMessageHeaders" +) +FULL_FETCH_EXPAND = "attachments($select=id,name,contentType,size,isInline)" + +# ─── Reuse extract logiky z v1.4 ────────────────────────────────────────────── + +_HERE = Path(__file__).parent +_V14_PATH = _HERE / "1_parse_emails_graph_v1.4.py" +if not _V14_PATH.exists(): + print(f"CHYBA: chybi sourozenec {_V14_PATH.name} — extract logiku nelze nacist", file=sys.stderr) + sys.exit(1) + +_spec = importlib.util.spec_from_file_location("v14_parse", _V14_PATH) +_v14 = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_v14) +extract_message = _v14.extract_message +extract_sync_fields = _v14.extract_sync_fields + +# GRAPH_MAILBOX modul-level v v1.4 — pro extract neni potreba, ale pro +# konzistenci nastavujeme ho v main() + +# ─── Graph API ──────────────────────────────────────────────────────────────── + +_graph_token: Optional[str] = None + + +def get_token() -> str: + global _graph_token + app = msal.ConfidentialClientApplication( + GRAPH_CLIENT_ID, + authority=f"https://login.microsoftonline.com/{GRAPH_TENANT_ID}", + client_credential=GRAPH_CLIENT_SECRET, + ) + result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"]) + if "access_token" not in result: + raise RuntimeError(f"Graph auth failed: {result}") + _graph_token = result["access_token"] + return _graph_token + + +class DeltaExpired(Exception): + """deltaLink expiroval (HTTP 410) — je nutne zacit od plne delta znovu.""" + + +def graph_get(url: str, params: dict = None, allow_410: bool = False) -> dict: + """GET na Graph s retry pri 401. Pri 410 a allow_410=True vyhodi DeltaExpired.""" + global _graph_token + if not _graph_token: + get_token() + for attempt in range(3): + r = requests.get( + url, + headers={"Authorization": f"Bearer {_graph_token}"}, + params=params, + timeout=60, + ) + if r.status_code == 401: + get_token() + continue + if r.status_code == 410 and allow_410: + raise DeltaExpired(url) + if r.status_code == 429: + # rate limit — respect Retry-After + wait = int(r.headers.get("Retry-After", "5")) + print(f" [429] cekam {wait}s ...") + time.sleep(wait) + continue + r.raise_for_status() + return r.json() + raise RuntimeError(f"Graph GET failed after retries: {url}") + + +def get_all_folders(mailbox: str, parent_id: str = None, parent_path: str = "") -> list[dict]: + if parent_id is None: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders" + else: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders/{parent_id}/childFolders" + + folders = [] + params = {"$top": 100, "$select": "id,displayName,childFolderCount"} + while url: + data = graph_get(url, params) + for f in data.get("value", []): + path = f"{parent_path}/{f['displayName']}".lstrip("/") + folders.append({"id": f["id"], "path": path}) + if f.get("childFolderCount", 0) > 0: + folders.extend(get_all_folders(mailbox, f["id"], path)) + url = data.get("@odata.nextLink") + params = None + return folders + + +def fetch_full_message(mailbox: str, msg_id: str) -> Optional[dict]: + """Stahne celou zpravu vcetne hlavicek a priloh — pro nove zpravy zachycene v delte.""" + url = f"{GRAPH_URL}/users/{mailbox}/messages/{msg_id}" + params = {"$select": FULL_FETCH_SELECT, "$expand": FULL_FETCH_EXPAND} + try: + return graph_get(url, params) + except requests.HTTPError as e: + logging.error("fetch_full_message %s: %s", msg_id, e) + return None + + +# ─── Delta iterace ──────────────────────────────────────────────────────────── + +def iter_folder_delta(mailbox: str, folder_id: str, delta_link: Optional[str], limit: int = 0): + """ + Generator: vraci (item, final_delta_link). + item je dict s polozkou (bud zmena nebo {'@removed': ...}). + Posledni vyhozeny tuple ma final_delta_link != None (zbytek None). + + Pri HTTP 410 (expirovany deltaLink) vyhodi DeltaExpired — caller ma + pustit znova s delta_link=None (= fresh full delta). + """ + if delta_link: + url = delta_link + params = None + else: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders/{folder_id}/messages/delta" + params = {"$select": DELTA_SELECT, "$top": PAGE_SIZE} + + n = 0 + while url: + data = graph_get(url, params, allow_410=True) + params = None + for item in data.get("value", []): + yield item, None + n += 1 + if limit and n >= limit: + # ulozime aspon stavajici nextLink jako "delta" — neni to ciste, + # ale pri --limit jde o test, takze pristi beh proste pocnize znovu + return + next_link = data.get("@odata.nextLink") + final_link = data.get("@odata.deltaLink") + if final_link: + # konec — predame final delta + yield None, final_link + return + url = next_link + + +# ─── Per-folder sync ────────────────────────────────────────────────────────── + +def sync_folder(col, sync_col, mailbox: str, folder: dict, dry_run: bool, limit: int) -> dict: + """Vrati statistiky.""" + fid = folder["id"] + fpath = folder["path"] + state_id = f"{mailbox}|{fid}" + state = sync_col.find_one({"_id": state_id}) + delta_link = state.get("delta_link") if state else None + + is_first_run = delta_link is None + label = "FRESH" if is_first_run else "DELTA" + print(f"\n[{label}] {fpath}") + + stats = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + final_delta = None + + try: + gen = iter_folder_delta(mailbox, fid, delta_link, limit=limit) + for item, fin in gen: + if fin: + final_delta = fin + break + try: + process_item(col, mailbox, fpath, item, stats, dry_run) + except Exception as e: + stats["errors"] += 1 + logging.error("process_item %s: %s", item.get("id", "?"), e) + except DeltaExpired: + print(f" [410] deltaLink expiroval — restart od fresh delta") + # rekurzivni restart s vymazanym statem + sync_col.delete_one({"_id": state_id}) + return sync_folder(col, sync_col, mailbox, folder, dry_run, limit) + + print(f" new={stats['new']} sync={stats['sync']} removed={stats['removed']} err={stats['errors']}") + + # Ulozit sync_state pokud mame final_delta a neni dry run + if final_delta and not dry_run: + sync_col.update_one( + {"_id": state_id}, + { + "$set": { + "mailbox": mailbox, + "folder_id": fid, + "folder_path": fpath, + "delta_link": final_delta, + "last_run_at": datetime.now(timezone.utc).replace(tzinfo=None), + }, + "$inc": { + "cumulative_new": stats["new"], + "cumulative_sync": stats["sync"], + "cumulative_removed": stats["removed"], + "run_count": 1, + }, + }, + upsert=True, + ) + elif not final_delta: + # neprisel deltaLink (napr. limit nebo chyba) — nemenime state, pristi beh + # bude pokracovat normalne podle stareho deltaLinku nebo zacne od fresh + if not is_first_run: + print(f" [pozn] delta neukoncena — pristi beh pojede od ulozeneho deltaLinku") + + return stats + + +def process_item(col, mailbox: str, folder_path: str, item: dict, stats: dict, dry_run: bool): + """Zpracuje jednu polozku z delta odpovedi.""" + # 1) Smazana zprava (@removed) + if "@removed" in item or item.get("@removed.reason"): + graph_id = item.get("id") + if not graph_id: + return + if dry_run: + print(f" REMOVED graph_id={graph_id[:30]}...") + else: + col.update_one( + {"graph_id": graph_id}, + {"$set": { + "permanently_deleted": True, + "permanently_deleted_at": datetime.now(timezone.utc).replace(tzinfo=None), + }}, + ) + stats["removed"] += 1 + return + + # 2) Nova nebo zmenena zprava — rozhodneme podle existence graph_id v Mongo + graph_id = item.get("id") + if not graph_id: + return + + existing = col.find_one({"graph_id": graph_id}, {"_id": 1}) + + if existing: + # Existujici zprava — update jen sync poli (delta payload je obsahuje) + fields = extract_sync_fields(item, folder_path) + if dry_run: + print(f" SYNC {item.get('subject','')[:60]}") + else: + col.update_one({"_id": existing["_id"]}, {"$set": fields}) + stats["sync"] += 1 + else: + # Nova zprava — pro telo+attachments+headers fetchneme plnou verzi + full = fetch_full_message(mailbox, graph_id) + if full is None: + stats["errors"] += 1 + return + doc = extract_message(full, folder_path) + if doc is None: + stats["errors"] += 1 + return + if dry_run: + print(f" NEW {doc.get('subject','')[:60]}") + else: + col.update_one({"_id": doc["_id"]}, {"$set": doc}, upsert=True) + stats["new"] += 1 + + +# ─── Indexy pro sync_state ──────────────────────────────────────────────────── + +def ensure_sync_state_indexes(sync_col): + sync_col.create_index([("mailbox", ASCENDING), ("folder_id", ASCENDING)]) + sync_col.create_index([("last_run_at", ASCENDING)]) + + +def ensure_perm_deleted_index(col): + col.create_index([("permanently_deleted", ASCENDING)], sparse=True) + + +# ─── Main ───────────────────────────────────────────────────────────────────── + +def discover_mailboxes(db) -> list[str]: + """Vrati seznam mailboxu = vsechny kolekce v `emaily` mimo NON_MAILBOX_COLLECTIONS + a SKIP_MAILBOXES.""" + out = [] + for name in sorted(db.list_collection_names()): + if name in NON_MAILBOX_COLLECTIONS: + continue + if name in SKIP_MAILBOXES: + print(f" [skip] {name} — v SKIP_MAILBOXES (neni Graph pristup)") + continue + out.append(name) + return out + + +def sync_mailbox(client, mailbox: str, args) -> dict: + """Sync jedne schranky. Vraci totals dict.""" + _v14.GRAPH_MAILBOX = mailbox + + print(f"\n========== {mailbox} ==========") + + col = client[MONGO_DB][mailbox] + sync_col = client[MONGO_DB][SYNC_STATE_COL] + + if not args.dry_run: + ensure_sync_state_indexes(sync_col) + ensure_perm_deleted_index(col) + + if args.reset: + n = sync_col.delete_many({"mailbox": mailbox}).deleted_count + print(f" --reset: smazano {n} deltaLinku pro {mailbox}") + + print("Nacitam seznam slozek...") + try: + folders = get_all_folders(mailbox) + except requests.HTTPError as e: + print(f" CHYBA: nelze nacist slozky pro {mailbox}: {e}") + logging.error("get_all_folders %s: %s", mailbox, e) + return {"new": 0, "sync": 0, "removed": 0, "errors": 1} + + if args.folder: + folders = [f for f in folders if args.folder.lower() in f["path"].lower()] + print(f" Slozek ke zpracovani: {len(folders)}") + + totals = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + for folder in folders: + s = sync_folder(col, sync_col, mailbox, folder, args.dry_run, args.limit) + for k in totals: + totals[k] += s[k] + print(f" -> mailbox total: new={totals['new']} sync={totals['sync']} removed={totals['removed']} err={totals['errors']}") + return totals + + +def main(): + ap = argparse.ArgumentParser(description=f"parse_emails_graph delta sync v{SCRIPT_VERSION}") + ap.add_argument("--mailbox", default="", + help="E-mail schranky (= kolekce v Mongo). " + "Bez argumentu projede vsechny schranky z `emaily` (mimo SKIP_MAILBOXES).") + ap.add_argument("--folder", default="", help="Filtruje slozky obsahujici tento retezec (default: vsechny)") + ap.add_argument("--limit", type=int, default=0, help="Max polozek na slozku (test)") + ap.add_argument("--reset", action="store_true", + help="Smaze deltaLinky pro vybrane schranky — pristi beh zacne od fresh delta") + ap.add_argument("--dry-run", action="store_true", help="Nic neulozi do Mongo, jen vypise co by se stalo") + args = ap.parse_args() + + print(f"=== Delta sync v{SCRIPT_VERSION} ===") + if args.dry_run: + print(" DRY-RUN — zadne zmeny v Mongo") + + print("Pripojuji se k MongoDB...") + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + client.admin.command("ping") + db = client[MONGO_DB] + + if args.mailbox: + if args.mailbox in SKIP_MAILBOXES: + print(f" CHYBA: {args.mailbox} je v SKIP_MAILBOXES — neni Graph pristup.") + sys.exit(2) + mailboxes = [args.mailbox] + else: + mailboxes = discover_mailboxes(db) + print(f" Schranky ke zpracovani: {len(mailboxes)}") + for m in mailboxes: + print(f" {m}") + + print("Token Graph API...") + get_token() + print(" OK") + + t0 = time.time() + grand = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + per_mailbox = [] + for mb in mailboxes: + try: + s = sync_mailbox(client, mb, args) + except Exception as e: + print(f" FATAL pri sync {mb}: {e}") + logging.error("sync_mailbox %s: %s", mb, e) + s = {"new": 0, "sync": 0, "removed": 0, "errors": 1} + per_mailbox.append((mb, s)) + for k in grand: + grand[k] += s[k] + + dt = time.time() - t0 + print(f"\n=== SHRNUTI ===") + for mb, s in per_mailbox: + print(f" {mb:40} new={s['new']:>5} sync={s['sync']:>5} removed={s['removed']:>4} err={s['errors']:>3}") + print(f" {'TOTAL':40} new={grand['new']:>5} sync={grand['sync']:>5} removed={grand['removed']:>4} err={grand['errors']:>3}") + print(f" trvalo: {dt:.1f} s") + return 1 if grand["errors"] > 0 else 0 + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.1.py b/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.1.py new file mode 100644 index 0000000..511901b --- /dev/null +++ b/EmailsImport/_tower_study/1b_parse_emails_graph_delta_v1.1.py @@ -0,0 +1,523 @@ +""" +============================================================================== +Skript: 1b_parse_emails_graph_delta_v1.1.py +Verze: 1.1 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Zmeny v1.1 (2026-06-10): + - Bugfix: NON_MAILBOX_COLLECTIONS rozsireno o "jnj_messages" a + "jnj_sync_state" (pomocne kolekce JNJ folder trackingu). Predtim je + discover_mailboxes bral jako schranky -> Graph 404 na + /users/jnj_messages/mailFolders -> cely krok 1b FAIL(1) pri kazdem behu. + +Popis: + Inkrementalni sync emailu pres Microsoft Graph DELTA QUERY. + Sourozenec `1_parse_emails_graph_v1.4.py` — kazdy resi jiny use case: + + 1_parse_emails_graph_v1.4.py = prvni plny import schranky + 1b_parse_emails_graph_delta_v1.1.py = pravidelny sync (zmeny od minula) + + Delta query je server-side change tracking — Graph si pamatuje "zalozku" + (deltaLink) a vraci jen to, co se od ni zmenilo: + - nove zpravy + - zmeny existujicich (isRead, flag, presun do jine slozky, kategorie) + - SMAZANE zpravy (@removed) — definitivne smazane, nikoli v kosi + + Pro mail v "Deleted Items" delta nic specialniho nedela — je to porad + normalni zprava, jen s folder_path="Deleted Items". @removed prijde az + kdyz uzivatel vysype kos / Shift+Del. + +State: + Kolekce `emaily.sync_state`, _id = "|". + { + mailbox, folder_id, folder_path, + delta_link, # plny URL s $deltatoken na pristi beh + last_run_at, + cumulative_new, cumulative_sync, cumulative_removed + } + +Permanentne smazane zpravy: + Skript je NEMAZE z Mongo. Pouze nastavi: + permanently_deleted: True + permanently_deleted_at: + Dohledani: col.find({"permanently_deleted": True}) + +Reuse: + Funkce extract_message / extract_sync_fields se nactou primo z modulu + 1_parse_emails_graph_v1.4.py (importlib, file-based), aby se logika + extrahce nikdy nerozesla. + +Spousteni: + python 1b_parse_emails_graph_delta_v1.1.py # VSECHNY schranky (mimo SKIP_MAILBOXES) + python 1b_parse_emails_graph_delta_v1.1.py --mailbox ordinace@buzalkova.cz # jedna schranka + python 1b_parse_emails_graph_delta_v1.1.py --mailbox ordinace@buzalkova.cz --folder Inbox + python 1b_parse_emails_graph_delta_v1.1.py --reset # zahodit deltaLinky a najet znova + python 1b_parse_emails_graph_delta_v1.1.py --dry-run # nic neulozit + +SKIP_MAILBOXES (hardcoded): + vbuzalka@its.jnj.com — JNJ tenant, nemame Graph API pristup. Pro tuto + schranku je nutny samostatny skript (lokalni .msg). + +Zavislosti: + msal, requests, pymongo, python-dateutil + Python 3.10+ +============================================================================== +""" + +from __future__ import annotations + +import argparse +import importlib.util +import logging +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import msal +import requests +from pymongo import MongoClient, ASCENDING + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +GRAPH_TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9" +GRAPH_CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f" +GRAPH_CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk" +GRAPH_URL = "https://graph.microsoft.com/v1.0" + +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +SYNC_STATE_COL = "sync_state" +PAGE_SIZE = 100 # delta endpoint typicky vraci max 100/stranka +LOG_FILE = Path(__file__).parent / "delta_errors.log" +SCRIPT_VERSION = "1.1" + +# Kolekce v `emaily` ktere NEJSOU mailboxy: +# (jnj_messages + jnj_sync_state = pomocne kolekce JNJ folder trackingu, +# bez exclude je discover_mailboxes bere jako schranky -> Graph 404 -> FAIL) +NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state", + "jnj_messages", "jnj_sync_state"} + +# Schranky, kde NEMAME Graph API pristup — pri bezneho behu se preskoci. +# Pro tyto je nutny separatni skript (napr. lokalni .msg parser). +SKIP_MAILBOXES = { + "vbuzalka@its.jnj.com", # JNJ tenant — nemame Graph credentials +} + +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + +# Co tahnout z delta endpointu (stejne jako MSG_SELECT v v1.4, mimo internetMessageHeaders +# ktere delta neumi vratit pro vsechny polozky — pro nove zpravy si je dotahneme +# samostatnym fetchem). +DELTA_SELECT = ( + "id,internetMessageId,subject,bodyPreview,body," + "importance,isRead,isDraft,hasAttachments," + "receivedDateTime,sentDateTime,createdDateTime,lastModifiedDateTime," + "sender,from,toRecipients,ccRecipients,bccRecipients,replyTo," + "conversationId,conversationIndex,parentFolderId," + "categories,flag,inferenceClassification" +) + +# Pro plne nacteni nove zpravy (vcetne hlavicek + priloh) pouzijeme stejny +# select+expand jako v1.4 +FULL_FETCH_SELECT = ( + "id,internetMessageId,subject,bodyPreview,body," + "importance,isRead,isDraft,hasAttachments," + "receivedDateTime,sentDateTime,createdDateTime,lastModifiedDateTime," + "sender,from,toRecipients,ccRecipients,bccRecipients,replyTo," + "conversationId,conversationIndex,parentFolderId," + "categories,flag,inferenceClassification,internetMessageHeaders" +) +FULL_FETCH_EXPAND = "attachments($select=id,name,contentType,size,isInline)" + +# ─── Reuse extract logiky z v1.4 ────────────────────────────────────────────── + +_HERE = Path(__file__).parent +_V14_PATH = _HERE / "1_parse_emails_graph_v1.4.py" +if not _V14_PATH.exists(): + print(f"CHYBA: chybi sourozenec {_V14_PATH.name} — extract logiku nelze nacist", file=sys.stderr) + sys.exit(1) + +_spec = importlib.util.spec_from_file_location("v14_parse", _V14_PATH) +_v14 = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_v14) +extract_message = _v14.extract_message +extract_sync_fields = _v14.extract_sync_fields + +# GRAPH_MAILBOX modul-level v v1.4 — pro extract neni potreba, ale pro +# konzistenci nastavujeme ho v main() + +# ─── Graph API ──────────────────────────────────────────────────────────────── + +_graph_token: Optional[str] = None + + +def get_token() -> str: + global _graph_token + app = msal.ConfidentialClientApplication( + GRAPH_CLIENT_ID, + authority=f"https://login.microsoftonline.com/{GRAPH_TENANT_ID}", + client_credential=GRAPH_CLIENT_SECRET, + ) + result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"]) + if "access_token" not in result: + raise RuntimeError(f"Graph auth failed: {result}") + _graph_token = result["access_token"] + return _graph_token + + +class DeltaExpired(Exception): + """deltaLink expiroval (HTTP 410) — je nutne zacit od plne delta znovu.""" + + +def graph_get(url: str, params: dict = None, allow_410: bool = False) -> dict: + """GET na Graph s retry pri 401. Pri 410 a allow_410=True vyhodi DeltaExpired.""" + global _graph_token + if not _graph_token: + get_token() + for attempt in range(3): + r = requests.get( + url, + headers={"Authorization": f"Bearer {_graph_token}"}, + params=params, + timeout=60, + ) + if r.status_code == 401: + get_token() + continue + if r.status_code == 410 and allow_410: + raise DeltaExpired(url) + if r.status_code == 429: + # rate limit — respect Retry-After + wait = int(r.headers.get("Retry-After", "5")) + print(f" [429] cekam {wait}s ...") + time.sleep(wait) + continue + r.raise_for_status() + return r.json() + raise RuntimeError(f"Graph GET failed after retries: {url}") + + +def get_all_folders(mailbox: str, parent_id: str = None, parent_path: str = "") -> list[dict]: + if parent_id is None: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders" + else: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders/{parent_id}/childFolders" + + folders = [] + params = {"$top": 100, "$select": "id,displayName,childFolderCount"} + while url: + data = graph_get(url, params) + for f in data.get("value", []): + path = f"{parent_path}/{f['displayName']}".lstrip("/") + folders.append({"id": f["id"], "path": path}) + if f.get("childFolderCount", 0) > 0: + folders.extend(get_all_folders(mailbox, f["id"], path)) + url = data.get("@odata.nextLink") + params = None + return folders + + +def fetch_full_message(mailbox: str, msg_id: str) -> Optional[dict]: + """Stahne celou zpravu vcetne hlavicek a priloh — pro nove zpravy zachycene v delte.""" + url = f"{GRAPH_URL}/users/{mailbox}/messages/{msg_id}" + params = {"$select": FULL_FETCH_SELECT, "$expand": FULL_FETCH_EXPAND} + try: + return graph_get(url, params) + except requests.HTTPError as e: + logging.error("fetch_full_message %s: %s", msg_id, e) + return None + + +# ─── Delta iterace ──────────────────────────────────────────────────────────── + +def iter_folder_delta(mailbox: str, folder_id: str, delta_link: Optional[str], limit: int = 0): + """ + Generator: vraci (item, final_delta_link). + item je dict s polozkou (bud zmena nebo {'@removed': ...}). + Posledni vyhozeny tuple ma final_delta_link != None (zbytek None). + + Pri HTTP 410 (expirovany deltaLink) vyhodi DeltaExpired — caller ma + pustit znova s delta_link=None (= fresh full delta). + """ + if delta_link: + url = delta_link + params = None + else: + url = f"{GRAPH_URL}/users/{mailbox}/mailFolders/{folder_id}/messages/delta" + params = {"$select": DELTA_SELECT, "$top": PAGE_SIZE} + + n = 0 + while url: + data = graph_get(url, params, allow_410=True) + params = None + for item in data.get("value", []): + yield item, None + n += 1 + if limit and n >= limit: + # ulozime aspon stavajici nextLink jako "delta" — neni to ciste, + # ale pri --limit jde o test, takze pristi beh proste pocnize znovu + return + next_link = data.get("@odata.nextLink") + final_link = data.get("@odata.deltaLink") + if final_link: + # konec — predame final delta + yield None, final_link + return + url = next_link + + +# ─── Per-folder sync ────────────────────────────────────────────────────────── + +def sync_folder(col, sync_col, mailbox: str, folder: dict, dry_run: bool, limit: int) -> dict: + """Vrati statistiky.""" + fid = folder["id"] + fpath = folder["path"] + state_id = f"{mailbox}|{fid}" + state = sync_col.find_one({"_id": state_id}) + delta_link = state.get("delta_link") if state else None + + is_first_run = delta_link is None + label = "FRESH" if is_first_run else "DELTA" + print(f"\n[{label}] {fpath}") + + stats = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + final_delta = None + + try: + gen = iter_folder_delta(mailbox, fid, delta_link, limit=limit) + for item, fin in gen: + if fin: + final_delta = fin + break + try: + process_item(col, mailbox, fpath, item, stats, dry_run) + except Exception as e: + stats["errors"] += 1 + logging.error("process_item %s: %s", item.get("id", "?"), e) + except DeltaExpired: + print(f" [410] deltaLink expiroval — restart od fresh delta") + # rekurzivni restart s vymazanym statem + sync_col.delete_one({"_id": state_id}) + return sync_folder(col, sync_col, mailbox, folder, dry_run, limit) + + print(f" new={stats['new']} sync={stats['sync']} removed={stats['removed']} err={stats['errors']}") + + # Ulozit sync_state pokud mame final_delta a neni dry run + if final_delta and not dry_run: + sync_col.update_one( + {"_id": state_id}, + { + "$set": { + "mailbox": mailbox, + "folder_id": fid, + "folder_path": fpath, + "delta_link": final_delta, + "last_run_at": datetime.now(timezone.utc).replace(tzinfo=None), + }, + "$inc": { + "cumulative_new": stats["new"], + "cumulative_sync": stats["sync"], + "cumulative_removed": stats["removed"], + "run_count": 1, + }, + }, + upsert=True, + ) + elif not final_delta: + # neprisel deltaLink (napr. limit nebo chyba) — nemenime state, pristi beh + # bude pokracovat normalne podle stareho deltaLinku nebo zacne od fresh + if not is_first_run: + print(f" [pozn] delta neukoncena — pristi beh pojede od ulozeneho deltaLinku") + + return stats + + +def process_item(col, mailbox: str, folder_path: str, item: dict, stats: dict, dry_run: bool): + """Zpracuje jednu polozku z delta odpovedi.""" + # 1) Smazana zprava (@removed) + if "@removed" in item or item.get("@removed.reason"): + graph_id = item.get("id") + if not graph_id: + return + if dry_run: + print(f" REMOVED graph_id={graph_id[:30]}...") + else: + col.update_one( + {"graph_id": graph_id}, + {"$set": { + "permanently_deleted": True, + "permanently_deleted_at": datetime.now(timezone.utc).replace(tzinfo=None), + }}, + ) + stats["removed"] += 1 + return + + # 2) Nova nebo zmenena zprava — rozhodneme podle existence graph_id v Mongo + graph_id = item.get("id") + if not graph_id: + return + + existing = col.find_one({"graph_id": graph_id}, {"_id": 1}) + + if existing: + # Existujici zprava — update jen sync poli (delta payload je obsahuje) + fields = extract_sync_fields(item, folder_path) + if dry_run: + print(f" SYNC {item.get('subject','')[:60]}") + else: + col.update_one({"_id": existing["_id"]}, {"$set": fields}) + stats["sync"] += 1 + else: + # Nova zprava — pro telo+attachments+headers fetchneme plnou verzi + full = fetch_full_message(mailbox, graph_id) + if full is None: + stats["errors"] += 1 + return + doc = extract_message(full, folder_path) + if doc is None: + stats["errors"] += 1 + return + if dry_run: + print(f" NEW {doc.get('subject','')[:60]}") + else: + col.update_one({"_id": doc["_id"]}, {"$set": doc}, upsert=True) + stats["new"] += 1 + + +# ─── Indexy pro sync_state ──────────────────────────────────────────────────── + +def ensure_sync_state_indexes(sync_col): + sync_col.create_index([("mailbox", ASCENDING), ("folder_id", ASCENDING)]) + sync_col.create_index([("last_run_at", ASCENDING)]) + + +def ensure_perm_deleted_index(col): + col.create_index([("permanently_deleted", ASCENDING)], sparse=True) + + +# ─── Main ───────────────────────────────────────────────────────────────────── + +def discover_mailboxes(db) -> list[str]: + """Vrati seznam mailboxu = vsechny kolekce v `emaily` mimo NON_MAILBOX_COLLECTIONS + a SKIP_MAILBOXES.""" + out = [] + for name in sorted(db.list_collection_names()): + if name in NON_MAILBOX_COLLECTIONS: + continue + if name in SKIP_MAILBOXES: + print(f" [skip] {name} — v SKIP_MAILBOXES (neni Graph pristup)") + continue + out.append(name) + return out + + +def sync_mailbox(client, mailbox: str, args) -> dict: + """Sync jedne schranky. Vraci totals dict.""" + _v14.GRAPH_MAILBOX = mailbox + + print(f"\n========== {mailbox} ==========") + + col = client[MONGO_DB][mailbox] + sync_col = client[MONGO_DB][SYNC_STATE_COL] + + if not args.dry_run: + ensure_sync_state_indexes(sync_col) + ensure_perm_deleted_index(col) + + if args.reset: + n = sync_col.delete_many({"mailbox": mailbox}).deleted_count + print(f" --reset: smazano {n} deltaLinku pro {mailbox}") + + print("Nacitam seznam slozek...") + try: + folders = get_all_folders(mailbox) + except requests.HTTPError as e: + print(f" CHYBA: nelze nacist slozky pro {mailbox}: {e}") + logging.error("get_all_folders %s: %s", mailbox, e) + return {"new": 0, "sync": 0, "removed": 0, "errors": 1} + + if args.folder: + folders = [f for f in folders if args.folder.lower() in f["path"].lower()] + print(f" Slozek ke zpracovani: {len(folders)}") + + totals = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + for folder in folders: + s = sync_folder(col, sync_col, mailbox, folder, args.dry_run, args.limit) + for k in totals: + totals[k] += s[k] + print(f" -> mailbox total: new={totals['new']} sync={totals['sync']} removed={totals['removed']} err={totals['errors']}") + return totals + + +def main(): + ap = argparse.ArgumentParser(description=f"parse_emails_graph delta sync v{SCRIPT_VERSION}") + ap.add_argument("--mailbox", default="", + help="E-mail schranky (= kolekce v Mongo). " + "Bez argumentu projede vsechny schranky z `emaily` (mimo SKIP_MAILBOXES).") + ap.add_argument("--folder", default="", help="Filtruje slozky obsahujici tento retezec (default: vsechny)") + ap.add_argument("--limit", type=int, default=0, help="Max polozek na slozku (test)") + ap.add_argument("--reset", action="store_true", + help="Smaze deltaLinky pro vybrane schranky — pristi beh zacne od fresh delta") + ap.add_argument("--dry-run", action="store_true", help="Nic neulozi do Mongo, jen vypise co by se stalo") + args = ap.parse_args() + + print(f"=== Delta sync v{SCRIPT_VERSION} ===") + if args.dry_run: + print(" DRY-RUN — zadne zmeny v Mongo") + + print("Pripojuji se k MongoDB...") + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + client.admin.command("ping") + db = client[MONGO_DB] + + if args.mailbox: + if args.mailbox in SKIP_MAILBOXES: + print(f" CHYBA: {args.mailbox} je v SKIP_MAILBOXES — neni Graph pristup.") + sys.exit(2) + mailboxes = [args.mailbox] + else: + mailboxes = discover_mailboxes(db) + print(f" Schranky ke zpracovani: {len(mailboxes)}") + for m in mailboxes: + print(f" {m}") + + print("Token Graph API...") + get_token() + print(" OK") + + t0 = time.time() + grand = {"new": 0, "sync": 0, "removed": 0, "errors": 0} + per_mailbox = [] + for mb in mailboxes: + try: + s = sync_mailbox(client, mb, args) + except Exception as e: + print(f" FATAL pri sync {mb}: {e}") + logging.error("sync_mailbox %s: %s", mb, e) + s = {"new": 0, "sync": 0, "removed": 0, "errors": 1} + per_mailbox.append((mb, s)) + for k in grand: + grand[k] += s[k] + + dt = time.time() - t0 + print(f"\n=== SHRNUTI ===") + for mb, s in per_mailbox: + print(f" {mb:40} new={s['new']:>5} sync={s['sync']:>5} removed={s['removed']:>4} err={s['errors']:>3}") + print(f" {'TOTAL':40} new={grand['new']:>5} sync={grand['sync']:>5} removed={grand['removed']:>4} err={grand['errors']:>3}") + print(f" trvalo: {dt:.1f} s") + return 1 if grand["errors"] > 0 else 0 + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.3.py b/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.3.py new file mode 100644 index 0000000..7304d29 --- /dev/null +++ b/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.3.py @@ -0,0 +1,579 @@ +""" +============================================================================== +Skript: enrich_fulltext_emails_v1.3.py +Verze: 1.3 +Datum: 2026-06-04 +Autor: vladimir.buzalka + +Popis: + Vytahne plny text z emailu ulozenych v MongoDB (db: emaily) a ulozi ho do + PostgreSQL (db: MongoEmaily, tabulka: emails) s GIN tsvector indexem. + + Emaily se NESTAHUJI znovu - tela uz jsou v Mongo z parse_emails_graph_v1.4 + (a refetch_text_bodies_v1.0 pro stare plain-text emaily). + Tento skript jen vybere prvni dostupne telo a posle text do PG na fulltext. + +Zmeny v1.3.1 (2026-06-09): + - Bugfix: _clean_for_pg nahrazuje osamocene surrogate (\\ud800-\\udfff) za U+FFFD. + Drive jeden mail se surrogaty (napr. JNJ .msg) shodil celou davku a krok 5 + skoncil FAIL. EXTRACTOR_VERSION zustava 1.2 (neni zmena fallback logiky). + +Zmeny v1.3 vs v1.2: + - Bugfix: NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state"} + (sync_state pribyla v delta syncu, predtim ji v1.2 brala jako mailbox). + - --index-reset: pred zpracovanim schranky vymaze vsechny jeji emaily z PG + (force re-extract; pouzij kdyz povysis EXTRACTOR_VERSION nebo chces ciste). + - Vylepseny header per-mailbox: ukaze pocet v Mongu, v PG a k zpracovani. + +Zmeny v1.2 vs v1.1: + - S/MIME emaily: pokud unwrap_smime_v1.0 ulozil smime_body_text/smime_body_html, + pouzije se PREFEROVANE pred bezvyznamnym wrapper telem. + - body_source: nova hodnota "smime". + - EXTRACTOR_VERSION=1.2 -> vsechny existujici emaily v PG se preparsuji. + +Zmeny v1.1 vs v1.0: + - Fallback poradi rozsireno o body_text. + - body_source umi novou hodnotu "text" (plne plain-text telo, max 2 MB). + +Zdroj: + MongoDB 192.168.1.76 db=emaily kolekce= + (krome NON_MAILBOX_COLLECTIONS) + +Cil: + PostgreSQL 192.168.1.76 db=MongoEmaily tabulka=emails + tsvector config 'soubory' (sdileny - simple + unaccent) + +Inkrementalita: + Pokud (mailbox, message_id) jiz existuje a extractor_version je aktualni + a modified_at v Mongo neni novejsi -> skip. Pri zmene verze extractoru + se vse preparsuje. --index-reset to obejde a smaze PG pred behom. + +Spusteni: + python enrich_fulltext_emails_v1.3.py # vsechny schranky + python enrich_fulltext_emails_v1.3.py --mailbox ordinace@buzalkova.cz + python enrich_fulltext_emails_v1.3.py --limit 500 # test + python enrich_fulltext_emails_v1.3.py --mailbox X --index-reset # smaze PG schranky a re-extrahuje vsechno + python enrich_fulltext_emails_v1.3.py --index-reset # smaze CELY index a postavi znovu (POMALE!) +============================================================================== +""" + +from __future__ import annotations + +import argparse +import re +import sys +import time +import traceback +from datetime import datetime, timezone +from typing import Optional + +import psycopg +from bs4 import BeautifulSoup +from pymongo import MongoClient + +# --- konfigurace ------------------------------------------------------------ +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" + +PG_DSN = ("host=192.168.1.76 port=5432 dbname=MongoEmaily " + "user=vladimir.buzalka password=Vlado7309208104++") + +EXTRACTOR_VERSION = "1.2" # NEMENIT pokud nemenis fallback logiku! + +MAX_TEXT_BYTES = 5 * 1024 * 1024 # plain text max 5 MB + +# Kolekce v `emaily` ktere NEJSOU mailboxy (nezpracovavame) +NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state"} + +BATCH_SIZE = 100 + + +# --- SCHEMA ----------------------------------------------------------------- + +SCHEMA_SQL = """ +CREATE EXTENSION IF NOT EXISTS unaccent; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = 'soubory') THEN + CREATE TEXT SEARCH CONFIGURATION soubory ( COPY = simple ); + ALTER TEXT SEARCH CONFIGURATION soubory + ALTER MAPPING FOR hword, hword_part, word + WITH unaccent, simple; + END IF; +END$$; + +CREATE TABLE IF NOT EXISTS emails ( + id BIGSERIAL PRIMARY KEY, + mailbox TEXT NOT NULL, + message_id TEXT NOT NULL, + graph_id TEXT, + conversation_id TEXT, + folder_path TEXT, + subject TEXT, + sender_email TEXT, + sender_name TEXT, + to_addrs TEXT, + cc_addrs TEXT, + bcc_addrs TEXT, + sent_at TIMESTAMPTZ, + received_at TIMESTAMPTZ, + modified_at TIMESTAMPTZ, + is_read BOOLEAN, + is_draft BOOLEAN, + has_attachments BOOLEAN, + attachment_count INT, + attachments_summary TEXT, + body TEXT, + body_length INT, + body_source TEXT, -- 'html' | 'preview' | 'empty' + tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('soubory'::regconfig, + left( + coalesce(subject, '') || ' ' || + coalesce(sender_email, '') || ' ' || + coalesce(sender_name, '') || ' ' || + coalesce(to_addrs, '') || ' ' || + coalesce(cc_addrs, '') || ' ' || + coalesce(attachments_summary, '') || ' ' || + coalesce(body, ''), + 800000) + ) + ) STORED, + extracted_at TIMESTAMPTZ DEFAULT now(), + extractor_version TEXT, + ok BOOLEAN, + error TEXT, + UNIQUE (mailbox, message_id) +); + +CREATE INDEX IF NOT EXISTS emails_tsv_gin ON emails USING gin(tsv); +CREATE INDEX IF NOT EXISTS emails_subject_trgm ON emails USING gin(subject gin_trgm_ops); +CREATE INDEX IF NOT EXISTS emails_sender_email_idx ON emails(sender_email); +CREATE INDEX IF NOT EXISTS emails_mailbox_idx ON emails(mailbox); +CREATE INDEX IF NOT EXISTS emails_received_idx ON emails(received_at DESC); +CREATE INDEX IF NOT EXISTS emails_conv_idx ON emails(conversation_id); +""" + + +# --- HELPERY ---------------------------------------------------------------- + +_CTRL_RX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") +_WS_RX = re.compile(r"[ \t]+") +_NL_RX = re.compile(r"\n{3,}") +# Osamocene surrogate (\ud800-\udfff) jsou neplatne v UTF-8 -> psycopg pri zapisu +# vyhodi UnicodeEncodeError ("surrogates not allowed") a shodi celou davku. +# Vznikaji ze spatne dekodovanych tel (napr. nektere JNJ .msg). Nahradime je U+FFFD. +_SURROGATE_RX = re.compile(r"[\ud800-\udfff]") + + +def _clean_for_pg(s: str) -> str: + if not s: + return "" + s = _CTRL_RX.sub("", s) + if _SURROGATE_RX.search(s): + s = _SURROGATE_RX.sub("�", s) + return s + + +def _truncate(s: str) -> str: + s = _clean_for_pg(s or "") + if not s: + return "" + b = s.encode("utf-8", errors="replace") + if len(b) <= MAX_TEXT_BYTES: + return s + return b[:MAX_TEXT_BYTES].decode("utf-8", errors="ignore") + + +def html_to_text(html: str) -> str: + if not html: + return "" + try: + soup = BeautifulSoup(html, "lxml") + except Exception: + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "head"]): + tag.decompose() + text = soup.get_text(separator="\n") + lines = [_WS_RX.sub(" ", ln).strip() for ln in text.split("\n")] + text = "\n".join(ln for ln in lines if ln) + text = _NL_RX.sub("\n\n", text) + return text + + +def fmt_recipients(recipients: list, kind: str) -> str: + if not recipients: + return "" + out = [] + for r in recipients: + if not isinstance(r, dict): + continue + if r.get("type") != kind: + continue + name = (r.get("name") or "").strip() + email = (r.get("email") or "").strip() + if name and email: + out.append(f"{name} <{email}>") + elif email: + out.append(email) + elif name: + out.append(name) + return "; ".join(out) + + +def fmt_attachments(attachments: list) -> str: + if not attachments: + return "" + out = [] + for a in attachments[:20]: + if not isinstance(a, dict): + continue + name = a.get("name") or a.get("filename") or "" + if name: + out.append(name) + return " | ".join(out) + + +def _short(s, n=60): + if not s: + return "" + s = str(s).replace("\n", " ").strip() + return s if len(s) <= n else s[:n] + "..." + + +def _now() -> datetime: + return datetime.now(tz=timezone.utc) + + +def _aware_utc(dt: Optional[datetime]) -> Optional[datetime]: + """Sjednoceni: PG TIMESTAMPTZ -> tz-aware UTC; Mongo datetime -> naive (UTC). + Vrati tz-aware UTC datetime nebo None.""" + if dt is None: + return None + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +# --- HLAVNI SMYCKA ---------------------------------------------------------- + +def process_mailbox(pg: psycopg.Connection, mongo_coll, mailbox: str, + limit: Optional[int] = None, + index_reset: bool = False) -> dict: + # --index-reset: smaz vse pro tuto schranku v PG + if index_reset: + with pg.cursor() as cur: + cur.execute("DELETE FROM emails WHERE mailbox = %s", (mailbox,)) + deleted = cur.rowcount + pg.commit() + print(f"[{mailbox}] --index-reset: smazano {deleted} radku v PG") + + # existujici zaznamy v PG (rychly inkrementalni lookup) + # tuple = (extractor_version, ok, body_source) + with pg.cursor() as cur: + cur.execute( + "SELECT message_id, extractor_version, ok, body_source " + "FROM emails WHERE mailbox = %s", + (mailbox,), + ) + existing = {row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()} + + mongo_total = mongo_coll.estimated_document_count() + pg_total = len(existing) + pg_uptodate = sum(1 for v in existing.values() + if v[0] == EXTRACTOR_VERSION and v[1]) + to_process_estimate = mongo_total - pg_uptodate + print(f"\n========== {mailbox} ==========") + print(f" v Mongu: {mongo_total}") + print(f" v PG: {pg_total} (z toho ext_v={EXTRACTOR_VERSION} & ok=true: {pg_uptodate})") + print(f" k zpracovani: ~{to_process_estimate}{' (limit=' + str(limit) + ')' if limit else ''}") + + if to_process_estimate <= 0 and not index_reset and not limit: + print(" Nic noveho ke zpracovani.") + return {"mailbox": mailbox, "processed": 0, "ok": 0, "errors": 0, + "skipped": pg_uptodate, "empty_body": 0} + + proj = { + "_id": 1, "graph_id": 1, "conversation_id": 1, "folder_path": 1, + "subject": 1, "sender": 1, "recipients": 1, + "sent_at": 1, "received_at": 1, "modified_at": 1, + "is_read": 1, "is_draft": 1, + "has_attachments": 1, "attachment_count": 1, "attachments": 1, + "body_html": 1, "body_text": 1, "body_preview": 1, + "smime_unwrapped": 1, "smime_body_text": 1, "smime_body_html": 1, + "smime_subject": 1, "smime_inner_attachments": 1, + } + cursor = mongo_coll.find({}, proj, no_cursor_timeout=True) + if limit: + cursor = cursor.limit(limit) + + processed = ok = errors = skipped = empty_body = 0 + queue: list[dict] = [] + n = 0 + + try: + for doc in cursor: + n += 1 + msg_id = doc.get("_id") or "" + prev = existing.get(msg_id) # (extractor_version, ok, body_source) + mongo_mtime = doc.get("modified_at") + + # Skip kdyz PG ma stejnou EV a ok=true. + # Vyjimka: smime_unwrapped v Mongu, ale PG body_source != 'smime' + # -> unwrap_smime pridal rozbaleny text az po enrichu -> re-enrich. + if prev and prev[0] == EXTRACTOR_VERSION and prev[1]: + needs_smime_reindex = ( + bool(doc.get("smime_unwrapped")) + and prev[2] != "smime" + ) + if not needs_smime_reindex: + skipped += 1 + continue + + sender = doc.get("sender") or {} + recipients = doc.get("recipients") or [] + attachments = doc.get("attachments") or [] + inner = doc.get("smime_inner_attachments") or [] + if inner: + attachments = list(attachments) + [ + {"filename": (a.get("filename") or "") + " [smime]"} + for a in inner if a.get("filename") + ] + + row = { + "mailbox": mailbox, + "message_id": msg_id, + "graph_id": doc.get("graph_id"), + "conversation_id": doc.get("conversation_id"), + "folder_path": doc.get("folder_path"), + "subject": doc.get("subject") or "", + "sender_email": sender.get("email"), + "sender_name": sender.get("name"), + "to_addrs": fmt_recipients(recipients, "to"), + "cc_addrs": fmt_recipients(recipients, "cc"), + "bcc_addrs": fmt_recipients(recipients, "bcc"), + # Vsechny timestampy z Monga jsou naive ale interpretovany jako UTC. + # Tagneme je tz-aware aby PG TIMESTAMPTZ ulozil spravnou UTC hodnotu + # a nepocital posun podle session timezone. + "sent_at": _aware_utc(doc.get("sent_at")), + "received_at": _aware_utc(doc.get("received_at")), + "modified_at": _aware_utc(mongo_mtime), + "is_read": doc.get("is_read"), + "is_draft": doc.get("is_draft"), + "has_attachments": doc.get("has_attachments"), + "attachment_count": doc.get("attachment_count"), + "attachments_summary": fmt_attachments(attachments), + "body": None, + "body_length": 0, + "body_source": "empty", + "extracted_at": _now(), + "extractor_version": EXTRACTOR_VERSION, + "ok": False, + "error": None, + } + + status = "OK "; detail = "" + try: + text = "" + if doc.get("smime_unwrapped"): + s_text = doc.get("smime_body_text") or "" + s_html = doc.get("smime_body_html") or "" + s_html_text = html_to_text(s_html) if s_html else "" + combined = "\n\n".join(p for p in (s_text, s_html_text) if p) + s_subject = doc.get("smime_subject") or "" + if s_subject: + combined = f"Subject: {s_subject}\n\n{combined}" + if combined: + text = combined + row["body_source"] = "smime" + if not text: + html = doc.get("body_html") or "" + h_text = html_to_text(html) if html else "" + if h_text: + text = h_text + row["body_source"] = "html" + if not text: + plain = doc.get("body_text") or "" + if plain: + text = plain + row["body_source"] = "text" + if not text: + preview = doc.get("body_preview") or "" + if preview: + text = preview + row["body_source"] = "preview" + if not text: + row["body_source"] = "empty" + empty_body += 1 + body = _truncate(text) + row["body"] = body if body else None + row["body_length"] = len(body) + row["ok"] = True + ok += 1 + detail = f"{len(body)} znaku {_short(body, 60)!r}" + except Exception as e: + row["error"] = f"{type(e).__name__}: {e}"[:500] + status = "ERR"; detail = row["error"][:80]; errors += 1 + + queue.append(row) + processed += 1 + + if processed % 200 == 0 or processed == 1: + subj = _short(row["subject"], 50) + print(f" [{n:>6}|p={processed:>5}] {status} {row['body_source']:<7} " + f"{row['body_length']:>7}ch | {subj}", flush=True) + + if len(queue) >= BATCH_SIZE: + _flush(pg, queue); queue.clear() + finally: + cursor.close() + + if queue: + _flush(pg, queue) + + return {"mailbox": mailbox, "processed": processed, "ok": ok, + "errors": errors, "skipped": skipped, "empty_body": empty_body} + + +UPSERT_SQL = """ +INSERT INTO emails + (mailbox, message_id, graph_id, conversation_id, folder_path, + subject, sender_email, sender_name, to_addrs, cc_addrs, bcc_addrs, + sent_at, received_at, modified_at, is_read, is_draft, + has_attachments, attachment_count, attachments_summary, + body, body_length, body_source, + extracted_at, extractor_version, ok, error) +VALUES + (%(mailbox)s, %(message_id)s, %(graph_id)s, %(conversation_id)s, %(folder_path)s, + %(subject)s, %(sender_email)s, %(sender_name)s, %(to_addrs)s, %(cc_addrs)s, %(bcc_addrs)s, + %(sent_at)s, %(received_at)s, %(modified_at)s, %(is_read)s, %(is_draft)s, + %(has_attachments)s, %(attachment_count)s, %(attachments_summary)s, + %(body)s, %(body_length)s, %(body_source)s, + %(extracted_at)s, %(extractor_version)s, %(ok)s, %(error)s) +ON CONFLICT (mailbox, message_id) DO UPDATE SET + graph_id = EXCLUDED.graph_id, + conversation_id = EXCLUDED.conversation_id, + folder_path = EXCLUDED.folder_path, + subject = EXCLUDED.subject, + sender_email = EXCLUDED.sender_email, + sender_name = EXCLUDED.sender_name, + to_addrs = EXCLUDED.to_addrs, + cc_addrs = EXCLUDED.cc_addrs, + bcc_addrs = EXCLUDED.bcc_addrs, + sent_at = EXCLUDED.sent_at, + received_at = EXCLUDED.received_at, + modified_at = EXCLUDED.modified_at, + is_read = EXCLUDED.is_read, + is_draft = EXCLUDED.is_draft, + has_attachments = EXCLUDED.has_attachments, + attachment_count = EXCLUDED.attachment_count, + attachments_summary = EXCLUDED.attachments_summary, + body = EXCLUDED.body, + body_length = EXCLUDED.body_length, + body_source = EXCLUDED.body_source, + extracted_at = EXCLUDED.extracted_at, + extractor_version = EXCLUDED.extractor_version, + ok = EXCLUDED.ok, + error = EXCLUDED.error +""" + + +def _flush(pg: psycopg.Connection, rows: list[dict]) -> None: + for r in rows: + for k in ("subject", "sender_email", "sender_name", "to_addrs", "cc_addrs", + "bcc_addrs", "attachments_summary", "body", "error", "folder_path"): + if r.get(k): + r[k] = _clean_for_pg(r[k]) + with pg.cursor() as cur: + cur.executemany(UPSERT_SQL, rows) + pg.commit() + + +def discover_mailboxes(db) -> list[str]: + out = [] + for name in sorted(db.list_collection_names()): + if name in NON_MAILBOX_COLLECTIONS: + continue + out.append(name) + return out + + +def main() -> int: + ap = argparse.ArgumentParser(description="enrich_fulltext_emails v1.3") + ap.add_argument("--mailbox", default="", + help="Jedna konkretni schranka. Bez argumentu projede vsechny.") + ap.add_argument("--limit", type=int, + help="Limit emailu na schranku (test)") + ap.add_argument("--index-reset", action="store_true", + help="Pred zpracovanim schranky vymaze vsechny jeji emaily z PG " + "(force re-extract). Bez --mailbox SMAZE CELY index.") + args = ap.parse_args() + + t0 = time.time() + print(f"=== enrich_fulltext_emails v1.3 ===") + print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + print("\nPripojuji se k PostgreSQL...") + pg = psycopg.connect(PG_DSN, connect_timeout=10) + with pg.cursor() as cur: + cur.execute(SCHEMA_SQL) + pg.commit() + print(" Schema OK.") + + print("Pripojuji se k MongoDB...") + mongo = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + mongo.admin.command("ping") + db = mongo[MONGO_DB] + print(" MongoDB OK.") + + if args.mailbox: + mailboxes = [args.mailbox] + else: + mailboxes = discover_mailboxes(db) + print(f"\nSchranky ke zpracovani ({len(mailboxes)}):") + for mb in mailboxes: + print(f" - {mb}") + + if args.index_reset and not args.mailbox: + print(f"\n!!! --index-reset bez --mailbox => SMAZE CELY INDEX ({len(mailboxes)} schranek) !!!") + + results = [] + for mb in mailboxes: + try: + results.append(process_mailbox(pg, db[mb], mb, + limit=args.limit, + index_reset=args.index_reset)) + except Exception as e: + traceback.print_exc() + print(f" FATAL pri zpracovani {mb}: {e}") + results.append({"mailbox": mb, "processed": 0, "ok": 0, + "errors": 1, "skipped": 0, "empty_body": 0}) + + pg.close() + + print("\n" + "="*60) + print("=== SHRNUTI ===") + grand = {"processed": 0, "ok": 0, "errors": 0, "skipped": 0, "empty_body": 0} + for r in results: + print(f" {r['mailbox']:40} processed={r['processed']:>5} ok={r['ok']:>5} " + f"errors={r['errors']:>3} skipped={r['skipped']:>6} empty={r['empty_body']:>4}") + for k in grand: + grand[k] += r.get(k, 0) + print(f" {'TOTAL':40} processed={grand['processed']:>5} ok={grand['ok']:>5} " + f"errors={grand['errors']:>3} skipped={grand['skipped']:>6} empty={grand['empty_body']:>4}") + print(f"\nCelkem trvalo: {time.time() - t0:.1f} s") + print(f"Konec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # exit code: 0 jen kdyz vsechny schranky probehly bez chyby + return 1 if grand["errors"] > 0 else 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print("\nPreruseno uzivatelem") + except Exception: + traceback.print_exc() + sys.exit(1) diff --git a/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.4.py b/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.4.py new file mode 100644 index 0000000..0ae7b05 --- /dev/null +++ b/EmailsImport/_tower_study/5_enrich_fulltext_emails_v1.4.py @@ -0,0 +1,587 @@ +""" +============================================================================== +Skript: enrich_fulltext_emails_v1.4.py +Verze: 1.4 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Zmeny v1.4 (2026-06-10): + - Bugfix: NON_MAILBOX_COLLECTIONS rozsireno o "jnj_messages" a + "jnj_sync_state" (pomocne kolekce JNJ folder trackingu). Predtim je + discover_mailboxes bral jako schranky (jiny schema dokumentu) -> + errors=1 -> cely krok 5 FAIL(1) pri kazdem behu pipeline. + +Popis: + Vytahne plny text z emailu ulozenych v MongoDB (db: emaily) a ulozi ho do + PostgreSQL (db: MongoEmaily, tabulka: emails) s GIN tsvector indexem. + + Emaily se NESTAHUJI znovu - tela uz jsou v Mongo z parse_emails_graph_v1.4 + (a refetch_text_bodies_v1.0 pro stare plain-text emaily). + Tento skript jen vybere prvni dostupne telo a posle text do PG na fulltext. + +Zmeny v1.3.1 (2026-06-09): + - Bugfix: _clean_for_pg nahrazuje osamocene surrogate (\\ud800-\\udfff) za U+FFFD. + Drive jeden mail se surrogaty (napr. JNJ .msg) shodil celou davku a krok 5 + skoncil FAIL. EXTRACTOR_VERSION zustava 1.2 (neni zmena fallback logiky). + +Zmeny v1.3 vs v1.2: + - Bugfix: NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state"} + (sync_state pribyla v delta syncu, predtim ji v1.2 brala jako mailbox). + - --index-reset: pred zpracovanim schranky vymaze vsechny jeji emaily z PG + (force re-extract; pouzij kdyz povysis EXTRACTOR_VERSION nebo chces ciste). + - Vylepseny header per-mailbox: ukaze pocet v Mongu, v PG a k zpracovani. + +Zmeny v1.2 vs v1.1: + - S/MIME emaily: pokud unwrap_smime_v1.0 ulozil smime_body_text/smime_body_html, + pouzije se PREFEROVANE pred bezvyznamnym wrapper telem. + - body_source: nova hodnota "smime". + - EXTRACTOR_VERSION=1.2 -> vsechny existujici emaily v PG se preparsuji. + +Zmeny v1.1 vs v1.0: + - Fallback poradi rozsireno o body_text. + - body_source umi novou hodnotu "text" (plne plain-text telo, max 2 MB). + +Zdroj: + MongoDB 192.168.1.76 db=emaily kolekce= + (krome NON_MAILBOX_COLLECTIONS) + +Cil: + PostgreSQL 192.168.1.76 db=MongoEmaily tabulka=emails + tsvector config 'soubory' (sdileny - simple + unaccent) + +Inkrementalita: + Pokud (mailbox, message_id) jiz existuje a extractor_version je aktualni + a modified_at v Mongo neni novejsi -> skip. Pri zmene verze extractoru + se vse preparsuje. --index-reset to obejde a smaze PG pred behom. + +Spusteni: + python enrich_fulltext_emails_v1.4.py # vsechny schranky + python enrich_fulltext_emails_v1.4.py --mailbox ordinace@buzalkova.cz + python enrich_fulltext_emails_v1.4.py --limit 500 # test + python enrich_fulltext_emails_v1.4.py --mailbox X --index-reset # smaze PG schranky a re-extrahuje vsechno + python enrich_fulltext_emails_v1.4.py --index-reset # smaze CELY index a postavi znovu (POMALE!) +============================================================================== +""" + +from __future__ import annotations + +import argparse +import re +import sys +import time +import traceback +from datetime import datetime, timezone +from typing import Optional + +import psycopg +from bs4 import BeautifulSoup +from pymongo import MongoClient + +# --- konfigurace ------------------------------------------------------------ +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" + +PG_DSN = ("host=192.168.1.76 port=5432 dbname=MongoEmaily " + "user=vladimir.buzalka password=Vlado7309208104++") + +EXTRACTOR_VERSION = "1.2" # NEMENIT pokud nemenis fallback logiku! + +MAX_TEXT_BYTES = 5 * 1024 * 1024 # plain text max 5 MB + +# Kolekce v `emaily` ktere NEJSOU mailboxy (nezpracovavame) +# (jnj_messages + jnj_sync_state = pomocne kolekce JNJ folder trackingu) +NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state", + "jnj_messages", "jnj_sync_state"} + +BATCH_SIZE = 100 + + +# --- SCHEMA ----------------------------------------------------------------- + +SCHEMA_SQL = """ +CREATE EXTENSION IF NOT EXISTS unaccent; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = 'soubory') THEN + CREATE TEXT SEARCH CONFIGURATION soubory ( COPY = simple ); + ALTER TEXT SEARCH CONFIGURATION soubory + ALTER MAPPING FOR hword, hword_part, word + WITH unaccent, simple; + END IF; +END$$; + +CREATE TABLE IF NOT EXISTS emails ( + id BIGSERIAL PRIMARY KEY, + mailbox TEXT NOT NULL, + message_id TEXT NOT NULL, + graph_id TEXT, + conversation_id TEXT, + folder_path TEXT, + subject TEXT, + sender_email TEXT, + sender_name TEXT, + to_addrs TEXT, + cc_addrs TEXT, + bcc_addrs TEXT, + sent_at TIMESTAMPTZ, + received_at TIMESTAMPTZ, + modified_at TIMESTAMPTZ, + is_read BOOLEAN, + is_draft BOOLEAN, + has_attachments BOOLEAN, + attachment_count INT, + attachments_summary TEXT, + body TEXT, + body_length INT, + body_source TEXT, -- 'html' | 'preview' | 'empty' + tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('soubory'::regconfig, + left( + coalesce(subject, '') || ' ' || + coalesce(sender_email, '') || ' ' || + coalesce(sender_name, '') || ' ' || + coalesce(to_addrs, '') || ' ' || + coalesce(cc_addrs, '') || ' ' || + coalesce(attachments_summary, '') || ' ' || + coalesce(body, ''), + 800000) + ) + ) STORED, + extracted_at TIMESTAMPTZ DEFAULT now(), + extractor_version TEXT, + ok BOOLEAN, + error TEXT, + UNIQUE (mailbox, message_id) +); + +CREATE INDEX IF NOT EXISTS emails_tsv_gin ON emails USING gin(tsv); +CREATE INDEX IF NOT EXISTS emails_subject_trgm ON emails USING gin(subject gin_trgm_ops); +CREATE INDEX IF NOT EXISTS emails_sender_email_idx ON emails(sender_email); +CREATE INDEX IF NOT EXISTS emails_mailbox_idx ON emails(mailbox); +CREATE INDEX IF NOT EXISTS emails_received_idx ON emails(received_at DESC); +CREATE INDEX IF NOT EXISTS emails_conv_idx ON emails(conversation_id); +""" + + +# --- HELPERY ---------------------------------------------------------------- + +_CTRL_RX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") +_WS_RX = re.compile(r"[ \t]+") +_NL_RX = re.compile(r"\n{3,}") +# Osamocene surrogate (\ud800-\udfff) jsou neplatne v UTF-8 -> psycopg pri zapisu +# vyhodi UnicodeEncodeError ("surrogates not allowed") a shodi celou davku. +# Vznikaji ze spatne dekodovanych tel (napr. nektere JNJ .msg). Nahradime je U+FFFD. +_SURROGATE_RX = re.compile(r"[\ud800-\udfff]") + + +def _clean_for_pg(s: str) -> str: + if not s: + return "" + s = _CTRL_RX.sub("", s) + if _SURROGATE_RX.search(s): + s = _SURROGATE_RX.sub("�", s) + return s + + +def _truncate(s: str) -> str: + s = _clean_for_pg(s or "") + if not s: + return "" + b = s.encode("utf-8", errors="replace") + if len(b) <= MAX_TEXT_BYTES: + return s + return b[:MAX_TEXT_BYTES].decode("utf-8", errors="ignore") + + +def html_to_text(html: str) -> str: + if not html: + return "" + try: + soup = BeautifulSoup(html, "lxml") + except Exception: + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "head"]): + tag.decompose() + text = soup.get_text(separator="\n") + lines = [_WS_RX.sub(" ", ln).strip() for ln in text.split("\n")] + text = "\n".join(ln for ln in lines if ln) + text = _NL_RX.sub("\n\n", text) + return text + + +def fmt_recipients(recipients: list, kind: str) -> str: + if not recipients: + return "" + out = [] + for r in recipients: + if not isinstance(r, dict): + continue + if r.get("type") != kind: + continue + name = (r.get("name") or "").strip() + email = (r.get("email") or "").strip() + if name and email: + out.append(f"{name} <{email}>") + elif email: + out.append(email) + elif name: + out.append(name) + return "; ".join(out) + + +def fmt_attachments(attachments: list) -> str: + if not attachments: + return "" + out = [] + for a in attachments[:20]: + if not isinstance(a, dict): + continue + name = a.get("name") or a.get("filename") or "" + if name: + out.append(name) + return " | ".join(out) + + +def _short(s, n=60): + if not s: + return "" + s = str(s).replace("\n", " ").strip() + return s if len(s) <= n else s[:n] + "..." + + +def _now() -> datetime: + return datetime.now(tz=timezone.utc) + + +def _aware_utc(dt: Optional[datetime]) -> Optional[datetime]: + """Sjednoceni: PG TIMESTAMPTZ -> tz-aware UTC; Mongo datetime -> naive (UTC). + Vrati tz-aware UTC datetime nebo None.""" + if dt is None: + return None + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +# --- HLAVNI SMYCKA ---------------------------------------------------------- + +def process_mailbox(pg: psycopg.Connection, mongo_coll, mailbox: str, + limit: Optional[int] = None, + index_reset: bool = False) -> dict: + # --index-reset: smaz vse pro tuto schranku v PG + if index_reset: + with pg.cursor() as cur: + cur.execute("DELETE FROM emails WHERE mailbox = %s", (mailbox,)) + deleted = cur.rowcount + pg.commit() + print(f"[{mailbox}] --index-reset: smazano {deleted} radku v PG") + + # existujici zaznamy v PG (rychly inkrementalni lookup) + # tuple = (extractor_version, ok, body_source) + with pg.cursor() as cur: + cur.execute( + "SELECT message_id, extractor_version, ok, body_source " + "FROM emails WHERE mailbox = %s", + (mailbox,), + ) + existing = {row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()} + + mongo_total = mongo_coll.estimated_document_count() + pg_total = len(existing) + pg_uptodate = sum(1 for v in existing.values() + if v[0] == EXTRACTOR_VERSION and v[1]) + to_process_estimate = mongo_total - pg_uptodate + print(f"\n========== {mailbox} ==========") + print(f" v Mongu: {mongo_total}") + print(f" v PG: {pg_total} (z toho ext_v={EXTRACTOR_VERSION} & ok=true: {pg_uptodate})") + print(f" k zpracovani: ~{to_process_estimate}{' (limit=' + str(limit) + ')' if limit else ''}") + + if to_process_estimate <= 0 and not index_reset and not limit: + print(" Nic noveho ke zpracovani.") + return {"mailbox": mailbox, "processed": 0, "ok": 0, "errors": 0, + "skipped": pg_uptodate, "empty_body": 0} + + proj = { + "_id": 1, "graph_id": 1, "conversation_id": 1, "folder_path": 1, + "subject": 1, "sender": 1, "recipients": 1, + "sent_at": 1, "received_at": 1, "modified_at": 1, + "is_read": 1, "is_draft": 1, + "has_attachments": 1, "attachment_count": 1, "attachments": 1, + "body_html": 1, "body_text": 1, "body_preview": 1, + "smime_unwrapped": 1, "smime_body_text": 1, "smime_body_html": 1, + "smime_subject": 1, "smime_inner_attachments": 1, + } + cursor = mongo_coll.find({}, proj, no_cursor_timeout=True) + if limit: + cursor = cursor.limit(limit) + + processed = ok = errors = skipped = empty_body = 0 + queue: list[dict] = [] + n = 0 + + try: + for doc in cursor: + n += 1 + msg_id = doc.get("_id") or "" + prev = existing.get(msg_id) # (extractor_version, ok, body_source) + mongo_mtime = doc.get("modified_at") + + # Skip kdyz PG ma stejnou EV a ok=true. + # Vyjimka: smime_unwrapped v Mongu, ale PG body_source != 'smime' + # -> unwrap_smime pridal rozbaleny text az po enrichu -> re-enrich. + if prev and prev[0] == EXTRACTOR_VERSION and prev[1]: + needs_smime_reindex = ( + bool(doc.get("smime_unwrapped")) + and prev[2] != "smime" + ) + if not needs_smime_reindex: + skipped += 1 + continue + + sender = doc.get("sender") or {} + recipients = doc.get("recipients") or [] + attachments = doc.get("attachments") or [] + inner = doc.get("smime_inner_attachments") or [] + if inner: + attachments = list(attachments) + [ + {"filename": (a.get("filename") or "") + " [smime]"} + for a in inner if a.get("filename") + ] + + row = { + "mailbox": mailbox, + "message_id": msg_id, + "graph_id": doc.get("graph_id"), + "conversation_id": doc.get("conversation_id"), + "folder_path": doc.get("folder_path"), + "subject": doc.get("subject") or "", + "sender_email": sender.get("email"), + "sender_name": sender.get("name"), + "to_addrs": fmt_recipients(recipients, "to"), + "cc_addrs": fmt_recipients(recipients, "cc"), + "bcc_addrs": fmt_recipients(recipients, "bcc"), + # Vsechny timestampy z Monga jsou naive ale interpretovany jako UTC. + # Tagneme je tz-aware aby PG TIMESTAMPTZ ulozil spravnou UTC hodnotu + # a nepocital posun podle session timezone. + "sent_at": _aware_utc(doc.get("sent_at")), + "received_at": _aware_utc(doc.get("received_at")), + "modified_at": _aware_utc(mongo_mtime), + "is_read": doc.get("is_read"), + "is_draft": doc.get("is_draft"), + "has_attachments": doc.get("has_attachments"), + "attachment_count": doc.get("attachment_count"), + "attachments_summary": fmt_attachments(attachments), + "body": None, + "body_length": 0, + "body_source": "empty", + "extracted_at": _now(), + "extractor_version": EXTRACTOR_VERSION, + "ok": False, + "error": None, + } + + status = "OK "; detail = "" + try: + text = "" + if doc.get("smime_unwrapped"): + s_text = doc.get("smime_body_text") or "" + s_html = doc.get("smime_body_html") or "" + s_html_text = html_to_text(s_html) if s_html else "" + combined = "\n\n".join(p for p in (s_text, s_html_text) if p) + s_subject = doc.get("smime_subject") or "" + if s_subject: + combined = f"Subject: {s_subject}\n\n{combined}" + if combined: + text = combined + row["body_source"] = "smime" + if not text: + html = doc.get("body_html") or "" + h_text = html_to_text(html) if html else "" + if h_text: + text = h_text + row["body_source"] = "html" + if not text: + plain = doc.get("body_text") or "" + if plain: + text = plain + row["body_source"] = "text" + if not text: + preview = doc.get("body_preview") or "" + if preview: + text = preview + row["body_source"] = "preview" + if not text: + row["body_source"] = "empty" + empty_body += 1 + body = _truncate(text) + row["body"] = body if body else None + row["body_length"] = len(body) + row["ok"] = True + ok += 1 + detail = f"{len(body)} znaku {_short(body, 60)!r}" + except Exception as e: + row["error"] = f"{type(e).__name__}: {e}"[:500] + status = "ERR"; detail = row["error"][:80]; errors += 1 + + queue.append(row) + processed += 1 + + if processed % 200 == 0 or processed == 1: + subj = _short(row["subject"], 50) + print(f" [{n:>6}|p={processed:>5}] {status} {row['body_source']:<7} " + f"{row['body_length']:>7}ch | {subj}", flush=True) + + if len(queue) >= BATCH_SIZE: + _flush(pg, queue); queue.clear() + finally: + cursor.close() + + if queue: + _flush(pg, queue) + + return {"mailbox": mailbox, "processed": processed, "ok": ok, + "errors": errors, "skipped": skipped, "empty_body": empty_body} + + +UPSERT_SQL = """ +INSERT INTO emails + (mailbox, message_id, graph_id, conversation_id, folder_path, + subject, sender_email, sender_name, to_addrs, cc_addrs, bcc_addrs, + sent_at, received_at, modified_at, is_read, is_draft, + has_attachments, attachment_count, attachments_summary, + body, body_length, body_source, + extracted_at, extractor_version, ok, error) +VALUES + (%(mailbox)s, %(message_id)s, %(graph_id)s, %(conversation_id)s, %(folder_path)s, + %(subject)s, %(sender_email)s, %(sender_name)s, %(to_addrs)s, %(cc_addrs)s, %(bcc_addrs)s, + %(sent_at)s, %(received_at)s, %(modified_at)s, %(is_read)s, %(is_draft)s, + %(has_attachments)s, %(attachment_count)s, %(attachments_summary)s, + %(body)s, %(body_length)s, %(body_source)s, + %(extracted_at)s, %(extractor_version)s, %(ok)s, %(error)s) +ON CONFLICT (mailbox, message_id) DO UPDATE SET + graph_id = EXCLUDED.graph_id, + conversation_id = EXCLUDED.conversation_id, + folder_path = EXCLUDED.folder_path, + subject = EXCLUDED.subject, + sender_email = EXCLUDED.sender_email, + sender_name = EXCLUDED.sender_name, + to_addrs = EXCLUDED.to_addrs, + cc_addrs = EXCLUDED.cc_addrs, + bcc_addrs = EXCLUDED.bcc_addrs, + sent_at = EXCLUDED.sent_at, + received_at = EXCLUDED.received_at, + modified_at = EXCLUDED.modified_at, + is_read = EXCLUDED.is_read, + is_draft = EXCLUDED.is_draft, + has_attachments = EXCLUDED.has_attachments, + attachment_count = EXCLUDED.attachment_count, + attachments_summary = EXCLUDED.attachments_summary, + body = EXCLUDED.body, + body_length = EXCLUDED.body_length, + body_source = EXCLUDED.body_source, + extracted_at = EXCLUDED.extracted_at, + extractor_version = EXCLUDED.extractor_version, + ok = EXCLUDED.ok, + error = EXCLUDED.error +""" + + +def _flush(pg: psycopg.Connection, rows: list[dict]) -> None: + for r in rows: + for k in ("subject", "sender_email", "sender_name", "to_addrs", "cc_addrs", + "bcc_addrs", "attachments_summary", "body", "error", "folder_path"): + if r.get(k): + r[k] = _clean_for_pg(r[k]) + with pg.cursor() as cur: + cur.executemany(UPSERT_SQL, rows) + pg.commit() + + +def discover_mailboxes(db) -> list[str]: + out = [] + for name in sorted(db.list_collection_names()): + if name in NON_MAILBOX_COLLECTIONS: + continue + out.append(name) + return out + + +def main() -> int: + ap = argparse.ArgumentParser(description="enrich_fulltext_emails v1.4") + ap.add_argument("--mailbox", default="", + help="Jedna konkretni schranka. Bez argumentu projede vsechny.") + ap.add_argument("--limit", type=int, + help="Limit emailu na schranku (test)") + ap.add_argument("--index-reset", action="store_true", + help="Pred zpracovanim schranky vymaze vsechny jeji emaily z PG " + "(force re-extract). Bez --mailbox SMAZE CELY index.") + args = ap.parse_args() + + t0 = time.time() + print(f"=== enrich_fulltext_emails v1.4 ===") + print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + print("\nPripojuji se k PostgreSQL...") + pg = psycopg.connect(PG_DSN, connect_timeout=10) + with pg.cursor() as cur: + cur.execute(SCHEMA_SQL) + pg.commit() + print(" Schema OK.") + + print("Pripojuji se k MongoDB...") + mongo = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + mongo.admin.command("ping") + db = mongo[MONGO_DB] + print(" MongoDB OK.") + + if args.mailbox: + mailboxes = [args.mailbox] + else: + mailboxes = discover_mailboxes(db) + print(f"\nSchranky ke zpracovani ({len(mailboxes)}):") + for mb in mailboxes: + print(f" - {mb}") + + if args.index_reset and not args.mailbox: + print(f"\n!!! --index-reset bez --mailbox => SMAZE CELY INDEX ({len(mailboxes)} schranek) !!!") + + results = [] + for mb in mailboxes: + try: + results.append(process_mailbox(pg, db[mb], mb, + limit=args.limit, + index_reset=args.index_reset)) + except Exception as e: + traceback.print_exc() + print(f" FATAL pri zpracovani {mb}: {e}") + results.append({"mailbox": mb, "processed": 0, "ok": 0, + "errors": 1, "skipped": 0, "empty_body": 0}) + + pg.close() + + print("\n" + "="*60) + print("=== SHRNUTI ===") + grand = {"processed": 0, "ok": 0, "errors": 0, "skipped": 0, "empty_body": 0} + for r in results: + print(f" {r['mailbox']:40} processed={r['processed']:>5} ok={r['ok']:>5} " + f"errors={r['errors']:>3} skipped={r['skipped']:>6} empty={r['empty_body']:>4}") + for k in grand: + grand[k] += r.get(k, 0) + print(f" {'TOTAL':40} processed={grand['processed']:>5} ok={grand['ok']:>5} " + f"errors={grand['errors']:>3} skipped={grand['skipped']:>6} empty={grand['empty_body']:>4}") + print(f"\nCelkem trvalo: {time.time() - t0:.1f} s") + print(f"Konec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # exit code: 0 jen kdyz vsechny schranky probehly bez chyby + return 1 if grand["errors"] > 0 else 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print("\nPreruseno uzivatelem") + except Exception: + traceback.print_exc() + sys.exit(1) diff --git a/EmailsImport/_tower_study/parse_emails_tower_v1.3.md b/EmailsImport/_tower_study/parse_emails_tower_v1.3.md new file mode 100644 index 0000000..c0da9db --- /dev/null +++ b/EmailsImport/_tower_study/parse_emails_tower_v1.3.md @@ -0,0 +1,289 @@ +# parse_emails_tower_v1.3 + +## Spuštění + +**První spuštění:** +```bash +docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py > /scripts/parse_emails_tower.log 2>&1" +``` + +**Pokračování po přerušení (přeskočí už importované):** +```bash +docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py --skip-existing > /scripts/parse_emails_tower.log 2>&1" +``` + +--- + +## Stav importu + +**Sledování průběhu (live log):** +```bash +docker exec -it python-runner tail -f /scripts/parse_emails_tower.log +``` + +**Počet emailů v MongoDB:** +```bash +docker exec -it python-runner python -c \ + "from pymongo import MongoClient; c=MongoClient('mongodb://192.168.1.76:27017'); print(c['emaily']['vbuzalka@its.jnj.com'].count_documents({}))" +``` + +--- + +**Název:** parse_emails_tower_v1.3.py +**Verze:** 1.3 +**Datum:** 2026-06-08 +**Autor:** vladimir.buzalka + +--- + +## Účel + +Import všech `.msg` souborů do MongoDB. Z každého souboru extrahuje **všechny dostupné vlastnosti** — podobně jako EXIF u fotek. + +- **DB:** `emaily` +- **Kolekce:** `vbuzalka@its.jnj.com` +- `_id` = Internet Message-ID (nebo `filename:` jako fallback) +- Bezpečné přerušit a opakovat — upsert podle `_id` + +--- + +## Prostředí + +Běží v Docker containeru **python-runner** na **Unraid Tower**. + +| Komponenta | Umístění | +|---|---| +| Container | `python-runner` (Docker na Unraid Tower) | +| .msg soubory | `/mnt/user/JNJEMAILS` → `/mnt/JNJEMAILS` uvnitř containeru | +| Skripty | `/mnt/user/Scripts` → `/scripts` uvnitř containeru | +| MongoDB | `192.168.1.76:27017` (externí, mimo container) | + +--- + +## Spouštění (z Unraid terminálu) + +**Test na 50 emailech:** +```bash +docker exec -it python-runner python /scripts/parse_emails_tower_v1.3.py --limit 50 --no-indexes +``` + +**Kompletní import na pozadí (log do souboru):** +```bash +docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py > /scripts/parse_emails_tower.log 2>&1" +``` + +**Pokračování po přerušení:** +```bash +docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py --skip-existing > /scripts/parse_emails_tower.log 2>&1" +``` + +**Sledování průběhu (Ctrl+C ukončí sledování, import běží dál):** +```bash +docker exec -it python-runner tail -f /scripts/parse_emails_tower.log +``` + +### Všechny parametry + +| Parametr | Popis | +|---|---| +| `--skip-existing` | Načte seznam hotových souborů z MongoDB a přeskočí je. Použij pro pokračování po přerušení. | +| `--limit N` | Zpracuje jen prvních N souborů. Vhodné pro test. | +| `--no-indexes` | Nevytváří indexy na konci. Použij pokud přerušíš uprostřed — indexy vytvoř ručně až je vše hotové. | +| `--msgs-dir PATH` | Přepíše výchozí cestu k .msg souborům (výchozí: `/mnt/JNJEMAILS`). | + +--- + +## Průběh na konzoli + +Každý email na jednom řádku: +``` + 1/69371 OK RE: Protocol deviation CZ10022 jan.novak@its.jnj.com + 2/69371 OK UCO3001: Draft FUL pro DD5-CZ10022 monitor@4gclinical.com + 3/69371 ERR ? ? +``` + +Každých 500 emailů oddělovač s průběhem: +``` + ──────────────────────────────────────────────────────────────────────────────── + Průběh: ok=498 err=2 0.4 msg/s ETA 47h12m + ──────────────────────────────────────────────────────────────────────────────── +``` + +Na konci souhrn: +``` +==================================================== +Vysledek: ok=69300 | skip=0 | err=71 +Celkovy cas: 47h 23m 10s +Dokumentu v kolekci: 69300 +``` + +--- + +## Zdroje dat z každého .msg + +| Pole | Popis | +|---|---| +| Předmět, normalized subject | | +| Odesílatel | email, jméno, SMTP adresa | +| Příjemci To/CC/BCC | strukturovaně `[{type, email, name}]` | +| Čas doručení a odeslání | UTC | +| Tělo | plaintext + HTML (max 2 MB) | +| Přílohy | metadata: jméno, velikost, MIME typ, inline flag | +| Internet headers | X-Originating-IP, Received, DKIM, X-Mailer, ... | +| MAPI | důležitost, citlivost, příznak, konverzační vlákno, kategorie | +| In-Reply-To, References | pro rekonstrukci vlákna | +| Raw MAPI properties | `{0xXXXX: value}` | + +--- + +## Hodnotové kódy + +| Pole | Hodnota | Význam | +|---|---|---| +| `importance` | 0 | Nízká | +| | 1 | Normální | +| | 2 | Vysoká | +| `sensitivity` | 0 | Normální | +| | 1 | Osobní | +| | 2 | Soukromé | +| | 3 | Důvěrné | +| `flag_status` | 0 | Bez příznaku | +| | 1 | Označeno (follow up) | +| | 2 | Dokončeno | + +--- + +## MongoDB indexy + +Automaticky vytvořeny na konci importu (`--no-indexes` přeskočí): + +| Index | Pole | +|---|---| +| Chronologický | `received_at`, `sent_at` | +| Odesílatel | `sender.email` | +| Soubor | `filename` (unique) | +| Konverzace | `conversation_topic` | +| Filtry | `has_attachments`, `categories`, `importance`, `flag_status` | +| Full-text | `subject` + `body_text` + `to` + `cc` (text index `text_search`) | + +--- + +## Ukázkové dotazy (MongoDB shell / MCP) + +**Emaily o UCO3001 s přílohou:** +```javascript +db["vbuzalka@its.jnj.com"].find({ + $text: { $search: "UCO3001" }, + has_attachments: true +}).sort({ received_at: -1 }) +``` + +**Emaily od konkrétního odesílatele:** +```javascript +db["vbuzalka@its.jnj.com"].find({ + "sender.email": /covance/i +}).sort({ received_at: -1 }) +``` + +**Celé konverzační vlákno:** +```javascript +db["vbuzalka@its.jnj.com"].find({ + conversation_topic: "Protocol deviation CZ10022" +}).sort({ received_at: 1 }) +``` + +**Statistiky podle odesílatele (top 20):** +```javascript +db["vbuzalka@its.jnj.com"].aggregate([ + { $group: { _id: "$sender.email", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 20 } +]) +``` + +--- + +## Chybový log + +Soubory které selhaly jsou zalogovány do **samostatného** `parse_emails_tower_errors.log` vedle skriptu (tj. `/scripts/parse_emails_tower_errors.log` → `\\tower\Scripts\parse_emails_tower_errors.log`). Tento log je oddělený od Graph importu, aby v něm nebyl bordel: +``` +2026-06-08 12:40:33 | open failed [7A3F...0000.msg]: +2026-06-08 12:41:02 | per-dokument selhal [_id=<...>]: +``` + +Stdout (průběh) jde do `parse_emails_tower.log` — rovněž samostatný. + +--- + +## Záchrana problémových .msg (v1.3) + +Některé `.msg` defaultní `extract_msg` neumí otevřít a celý soubor zahodí, **i když email je naprosto v pořádku** (jde otevřít v Outlooku). Tři příčiny a jejich řešení: + +| Příčina | Příklad | Řešení | +|---|---|---| +| Vadná příloha bez `PR_ATTACH_METHOD` | „Attachment method missing" | `errorBehavior=SUPPRESS_ALL` — vadnou přílohu přeskočí, zbytek (tělo, ostatní přílohy) načte | +| Tělo deklaruje codepage 1200 (UTF-16), ale bajty jsou cp1250/gb2312 | české `�` místo diakritiky | raw-OLE čtení + kaskádové dekódování | +| Vnořený email (Outlook item) | „not an MSG file", `extract_msg` vrátí prázdno | raw-OLE čtení klíčových MAPI streamů | + +**Jak to funguje:** + +1. `open_message()` — kaskádové otevření: `normal` → `SUPPRESS_ALL` → `+overrideEncoding` (dle codepage property). +2. **raw-OLE fallback** — když extract_msg vrátí prázdno/`�` nebo musel hádat kódování, klíčová pole (subject, sender, body, html) se dočtou **přímo z OLE streamů** (`__substg1.0_0037`/`0C1A`/`5D01`/`1000`/`1013`) s kaskádovým dekódováním: + ``` + utf-8 (strict) → kódování dle CPID → cp1250 → cp1252 → gb2312 → latin-1 + ``` + Hlavičkám o kódování se **nevěří** (často si protiřečí); bere se první kódování, které projde striktně bez chyby. `utf-8 strict` je silný rozlišovač. + +**Nová pole v dokumentu:** + +| Pole | Význam | +|---|---| +| `parse_mode` | `normal` / `suppress_all` / `override:` — jak byl soubor otevřen | +| `parse_degraded` | `true` = byl potřeba fallback (vadná příloha nebo hádané kódování) | + +**Ověřeno:** všech 126 dříve selhaných souborů z běhu 8.6. se obnoví čistě (74× `suppress_all`, 52× `override:cp1250`), 0 prázdných, 0 s `�`. + +Dohledání degradovaných: +```javascript +db["vbuzalka@its.jnj.com"].find({ parse_degraded: true }) +``` + +--- + +## Výkon + +| Parametr | Hodnota | +|---|---| +| Počet souborů | ~69 000 | +| Rychlost | ~0.4 msg/s (htmlBody dekódování) | +| Odhadovaný čas | 48 hodin | +| Batch size | 200 dokumentů / bulk_write | +| Odhadovaná velikost DB | 2–5 GB | + +--- + +## Závislosti (v Docker image python-runner) + +``` +extract-msg==0.55.0 +olefile +pymongo +python-dateutil +``` + +Image sestaven z `Dockerfile` v `/mnt/user/Scripts/python-runner/`. + +--- + +## Historie verzí + +| Verze | Datum | Změna | +|---|---|---| +| 1.0 | 2026-06-01 | Iniciální verze | +| 1.1 | 2026-06-02 | Nasazení na Unraid Tower v Docker containeru python-runner; MSGS_DIR změněno z SMB share (`\\tower\JNJEMAILS`) na lokální mount (`/mnt/JNJEMAILS`); aktualizován popis spouštění pro `docker exec` | +| 1.2 | 2026-06-08 | **Oprava `to_bson`:** int mimo rozsah int64 (BSON umí jen 8-byte ints) se převede na string — dřív celý `bulk_write` spadl na `MongoDB can only handle up to 8-byte ints` a zahodil celou dávku 200 dokumentů (běh v1.1 z 8.6. neuložil **nic**). `flush()` má fallback per-dokument (vadný záznam zahodí sám, ne celou dávku). `bool()` testován před `int()`. Samostatné logy `parse_emails_tower.log` + `parse_emails_tower_errors.log`. | +| 1.3 | 2026-06-08 | **Záchrana dříve selhaných .msg** (cca 126 z běhu 8.6.): `open_message()` kaskádové otevření (`normal`→`SUPPRESS_ALL`→`+overrideEncoding`) řeší vadné přílohy i „not an MSG file"; **raw-OLE fallback** dočítá subject/sender/body/html přímo z OLE streamů s kaskádovým dekódováním (utf-8 strict→CPID→cp1250…), když extract_msg vrátí prázdno/`�`. Nová pole `parse_mode`, `parse_degraded`. Nová závislost `olefile`. Ověřeno: 126/126 obnoveno čistě. | diff --git a/EmailsImport/_tower_study/parse_emails_tower_v1.3.py b/EmailsImport/_tower_study/parse_emails_tower_v1.3.py new file mode 100644 index 0000000..eb00abc --- /dev/null +++ b/EmailsImport/_tower_study/parse_emails_tower_v1.3.py @@ -0,0 +1,896 @@ +""" +parse_emails_tower_v1.3.py +Nazev: parse_emails_tower_v1.3.py +Verze: 1.3 +Datum: 2026-06-08 +Autor: vladimir.buzalka + +Popis: + Parsuje vsechny .msg soubory z MSGS_DIR a importuje je jako dokumenty + do MongoDB. Z kazdeho souboru extrahuje VSECHNY dostupne vlastnosti — + podobne jako EXIF u fotek: + + - predmet, odesilatel, prijemci (To/CC/BCC s typy) + - cas doruceni a odeslani (UTC) + - telo plaintext + HTML (max 2 MB) + - prilohy (metadata: jmeno, velikost, MIME typ, inline flag) + - internet headers (X-Originating-IP, Received, DKIM, ...) + - MAPI vlastnosti: dulezitost, citlivost, priznak, konverzacni vlakno, + kategorie, In-Reply-To, References, ... + - vsechny raw MAPI properties jako {0xXXXX: value} + + DB: emaily + Kolekce: vbuzalka@its.jnj.com + _id: Internet Message-ID (nebo "filename:" jako fallback) + + Bezpecne prerusit a opakovat: + - upsert podle _id — duplicity se automaticky prepisi + - --skip-existing nacte seznam hotovych souboru z MongoDB a + preskoci je => pokracovani po preruseni bez ztraty prace + +Prostredi: + Bezi v Docker containeru "python-runner" na Unraid Tower. + .msg soubory jsou dostupne jako lokalni disk (volume mount): + /mnt/user/JNJEMAILS -> /mnt/JNJEMAILS (uvnitr containeru) + MongoDB na 192.168.1.76:27017 (externi, bezi mimo container). + +Spousteni (z Unraid terminalu): + # Test na 50 emailech: + docker exec -it python-runner python /scripts/parse_emails_tower_v1.3.py --limit 50 --no-indexes + + # Kompletni import na pozadi (samostatny log, ne sdileny s Graph importem): + docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py > /scripts/parse_emails_tower.log 2>&1" + + # Pokracovani po preruseni: + docker exec -d python-runner bash -c \ + "python /scripts/parse_emails_tower_v1.3.py --skip-existing > /scripts/parse_emails_tower.log 2>&1" + + # Sledovani prubehu: + docker exec -it python-runner tail -f /scripts/parse_emails_tower.log + +Vystup na konzoli: + Kazdy email na jednom radku: + / OK/ERR + Kazych 500 emailu: oddelovac s prubehem, rychlosti a ETA. + Na konci: souhrn ok/skip/err, celkovy cas, pocet dokumentu v kolekci. + +Zavislosti (nainstalovane v Docker image python-runner): + extract-msg==0.55.0, olefile, pymongo, python-dateutil + Python 3.12, Linux (Docker container na Unraid Tower) + (olefile je tranzitivni zavislost extract-msg, raw-OLE fallback ji pouziva primo) + +Struktura dokumentu v MongoDB: + _id Internet Message-ID (nebo filename: fallback) + filename jmeno .msg souboru (20znakovy hex + .msg) + subject predmet zpravy + normalized_subject predmet bez RE:/FW: prefixu + importance 0=nizka 1=normalni 2=vysoka + sensitivity 0=normalni 1=osobni 2=soukrome 3=duverne + flag_status 0=bez priznaku 1=oznaceno 2=dokonceno + read_receipt_requested bool + delivery_receipt_requested bool + has_attachments bool + attachment_count int + message_size_bytes velikost .msg souboru na disku + conversation_topic tema vlakna (PR_CONVERSATION_TOPIC) + conversation_index base64 PR_CONVERSATION_INDEX + in_reply_to Message-ID predchozi zpravy + internet_references [Message-ID] — cela historia vlakna + categories [str] — MAPI kategorie / stitky + read_receipt_requested bool + delivery_receipt_requested bool + received_at datetime UTC — cas doruceni + sent_at datetime UTC — cas odeslani + sender.email emailova adresa odesilatele + sender.name zobrazovane jmeno odesilatele + sender.smtp SMTP adresa (pro interni EX adresy) + to retezec To (tak jak v Outlooku) + cc retezec CC + bcc retezec BCC + display_to PR_DISPLAY_TO (zkraceny seznam) + display_cc PR_DISPLAY_CC + recipients [{type, email, name}] — to/cc/bcc s typy + body_text plain text telo + body_html HTML telo (max 2 MB, None pokud neni) + attachments [{filename, size_bytes, mime_type, + content_id, is_inline}] + headers dict internet headers (lowercase_s_podtrzitky) + mapi dict vsech raw MAPI properties {0xXXXX: value} + parsed_at datetime UTC — cas parsovani + +Indexy (vytvoreny automaticky na konci): + received_at, sent_at, sender.email, filename (unique), + conversation_topic, has_attachments, categories, importance, + flag_status, text_search (subject + body_text + to + cc) + +Chyby: + Soubory ktere selhaly jsou zalogovany do parse_emails_tower_errors.log + v adresari skriptu (SAMOSTATNY log, oddeleny od Graph importu). + Radek: timestamp | open/extract failed | duvod. + +Historie verzi: + 1.0 2026-06-01 Inicialni verze + 1.1 2026-06-02 Nasazeni na Unraid Tower v Docker containeru python-runner; + MSGS_DIR zmeneno z SMB share na lokalni mount /mnt/JNJEMAILS; + aktualizovany popis spousteni pro docker exec + 1.2 2026-06-08 OPRAVA: to_bson prevadi int mimo rozsah int64 na string + (BSON umi jen 8-byte ints) — drive cely bulk_write spadl na + 'MongoDB can only handle up to 8-byte ints' a zahodil celou + davku 200 dokumentu (v1.1 beh 8.6. neulozil NIC). + flush() ma fallback per-dokument: vadny zaznam zahodi sam, + ne celou davku. bool() testovan pred int(). + Samostatny error log parse_emails_tower_errors.log a + stdout log parse_emails_tower.log (drive sdilene s Graph + importem — bordel v logu). + 1.3 2026-06-08 ZACHRANA drive selhavajicich .msg (cca 126 z behu 8.6.): + - open_message(): kaskadove otevreni + normal -> SUPPRESS_ALL (vadne prilohy) -> +overrideEncoding + Resi 'Attachment method missing' i 'not an MSG file'. + - raw-OLE fallback: kdyz extract_msg vrati prazdno/� (vnoreny + email, codepage 1200 lze byt cp1250/gb2312), klicova pole + (subject/sender/body/html) se doctou PRIMO z OLE streamu + s kaskadovym dekodovanim (utf-8 strict -> CPID -> cp1250 ...). + Hlavickam o kodovani se neveri (casto si protireci). + - nova pole: parse_mode (normal/suppress_all/override:ENC), + parse_degraded (bool). +""" + +import sys +import re +import logging +import argparse +import base64 +import struct +from pathlib import Path +from datetime import datetime, timezone +from typing import Optional + +import extract_msg +from extract_msg.enums import ErrorBehavior +import olefile +from dateutil import parser as dtparser +from pymongo import MongoClient, UpdateOne, ASCENDING, TEXT + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +MSGS_DIR = Path("/mnt/JNJEMAILS") +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +MONGO_COL = "vbuzalka@its.jnj.com" +BATCH_SIZE = 200 +LOG_FILE = Path(__file__).parent / "parse_emails_tower_errors.log" +SCRIPT_VERSION = "1.2" +# ────────────────────────────────────────────────────────────────────────────── + +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + + +# ─── Pomocné funkce ─────────────────────────────────────────────────────────── + +def safe(obj, *attrs, default=None): + """Bezpecne cteni atributu — vrati prvni non-None hodnotu.""" + for attr in attrs: + try: + val = getattr(obj, attr, None) + if val is None: + continue + if isinstance(val, str) and not val.strip(): + continue + return val + except Exception: + continue + return default + + +def parse_date(raw) -> Optional[datetime]: + """Libovolny datum -> UTC datetime bez tzinfo (pro MongoDB).""" + if raw is None: + return None + if isinstance(raw, datetime): + if raw.tzinfo: + return raw.astimezone(timezone.utc).replace(tzinfo=None) + return raw + try: + dt = dtparser.parse(str(raw)) + if dt.tzinfo: + return dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt + except Exception: + return None + + +_INT64_MIN, _INT64_MAX = -(2 ** 63), 2 ** 63 - 1 + + +def to_bson(val): + """Konvertuje hodnotu na BSON-serializovatelny typ. + + Pozor: BSON umi jen signed int64. Python ma neomezene integery, takze + velke MAPI hodnoty (PR_CHANGE_KEY, FILETIME, 64-bit handle) mimo rozsah + int64 prevadime na string — jinak cely bulk_write spadne na + 'MongoDB can only handle up to 8-byte ints'. + """ + # bool musi byt PRED int (isinstance(True, int) == True) + if isinstance(val, bool): + return val + if isinstance(val, bytes): + return val.hex() if len(val) <= 128 else f"" + if isinstance(val, datetime): + return parse_date(val) + if isinstance(val, int): + return val if _INT64_MIN <= val <= _INT64_MAX else str(val) + if isinstance(val, (str, float, type(None))): + return val + if isinstance(val, list): + return [to_bson(v) for v in val] + try: + iv = int(val) + return iv if _INT64_MIN <= iv <= _INT64_MAX else str(iv) + except Exception: + pass + return str(val) + + +# ─── Extrakce částí zprávy ──────────────────────────────────────────────────── + +def extract_headers(msg) -> dict: + headers = {} + try: + hdr = msg.header + if not hdr: + return {} + from email.header import decode_header as _dh + + def _decode(v: str) -> str: + try: + parts = _dh(v) + out = "" + for part, enc in parts: + out += part.decode(enc or "utf-8", errors="replace") if isinstance(part, bytes) else part + return out + except Exception: + return v + + for key in set(hdr.keys()): + k = key.lower().replace("-", "_") + vals = [_decode(v) for v in hdr.get_all(key, [])] + headers[k] = vals if len(vals) > 1 else (vals[0] if vals else "") + except Exception as e: + logging.error("extract_headers: %s", e) + return headers + + +def extract_recipients(msg) -> list: + result = [] + type_map = {1: "to", 2: "cc", 3: "bcc"} + try: + for r in msg.recipients: + rtype = getattr(r, "type", 1) + try: + rtype = int(rtype) + except Exception: + try: + rtype = int(rtype.value) + except Exception: + rtype = 1 + rec = { + "type": type_map.get(rtype, "to"), + "email": safe(r, "email", default=""), + "name": safe(r, "name", default=""), + } + result.append(rec) + except Exception as e: + logging.error("extract_recipients: %s", e) + return result + + +def extract_attachments(msg) -> list: + result = [] + try: + for att in msg.attachments: + fname = safe(att, "longFilename", "shortFilename", default="") + if not fname: + continue + size = 0 + try: + d = att.data + size = len(d) if d else 0 + except Exception: + pass + result.append({ + "filename": fname, + "size_bytes": size, + "mime_type": safe(att, "mimetype", "mimeType", default="application/octet-stream"), + "content_id": safe(att, "cid", default=None), + "is_inline": bool(safe(att, "isInline", default=False)), + }) + except Exception as e: + logging.error("extract_attachments: %s", e) + return result + + +def extract_mapi_props(msg) -> dict: + """Vsechny raw MAPI properties jako {0xXXXX: value}.""" + result = {} + try: + props = msg.props + if not hasattr(props, "items"): + return {} + for key, prop in props.items(): + try: + val = to_bson(prop.value) + prop_id = f"0x{key[:4].upper()}" if len(key) >= 4 else f"0x{key.upper()}" + result[prop_id] = val + except Exception: + pass + except Exception as e: + logging.error("extract_mapi_props: %s", e) + return result + + +# ─── Tolerantní otevírání a raw-OLE fallback ───────────────────────────────── +# +# Nektere .msg extract_msg neumi: (a) vadna priloha bez PR_ATTACH_METHOD, +# (b) telo deklaruje codepage 1200 (UTF-16) ale bajty jsou cp1250/gb2312, +# (c) vnoreny email ("not an MSG file") — extract_msg vrati prazdne pole. +# Data v souboru ale jsou. Otevreme tolerantne a degradovana textova pole +# docteme PRIMO z OLE streamu s kaskadovym dekodovanim (hlavickam se neveri). + +# Windows codepage -> python codec (PR_INTERNET_CPID / PR_MESSAGE_CODEPAGE) +_CPID_TO_CODEC = { + 1250: "cp1250", 1251: "cp1251", 1252: "cp1252", 1253: "cp1253", + 1254: "cp1254", 1255: "cp1255", 1256: "cp1256", 1257: "cp1257", + 1258: "cp1258", 874: "cp874", 932: "shift_jis", 936: "gb2312", + 949: "euc_kr", 950: "big5", 65001: "utf-8", 28591: "iso-8859-1", + 28592: "iso-8859-2", 20127: "ascii", +} + + +def _read_u32_prop(ole, propid): + """Precte 32-bit hodnotu MAPI property z top-level __properties_version1.0.""" + try: + data = ole.openstream("__properties_version1.0").read() + except Exception: + return None + body = data[32:] # 32-bajtova hlavicka top-level property streamu + for i in range(0, len(body) - 16 + 1, 16): + rec = body[i:i + 16] + tag = struct.unpack("> 16) & 0xFFFF) == propid: + return struct.unpack(" Optional[str]: + """Codec dle PR_INTERNET_CPID / PR_MESSAGE_CODEPAGE (jako napoveda, ne dogma).""" + for pid in (0x3FDE, 0x3FFD): # INTERNET_CPID, MESSAGE_CODEPAGE + codec = _CPID_TO_CODEC.get(_read_u32_prop(ole, pid)) + # utf-8/ascii nejsou dobry hint pro 8-bit stream (casto lzou) + if codec and codec not in ("utf-8", "ascii"): + return codec + return None + + +def _cascade_decode(raw: bytes, is_unicode: bool, cpid_codec: Optional[str]) -> str: + """Dekoduje bajty MAPI stringu. Hlavickam se neveri — zkousime striktne + v poradi priorit a vezmeme prvni, co projde bez chyby.""" + if not raw: + return "" + if is_unicode: # PT_UNICODE = utf-16-le + try: + return raw.decode("utf-16-le") + except Exception: + return raw.decode("utf-16-le", errors="replace") + order = ["utf-8"] # utf-8 strict = silny rozlisovac + if cpid_codec: + order.append(cpid_codec) + order += ["cp1250", "cp1252", "gb2312", "big5"] + for enc in order: + try: + return raw.decode(enc, errors="strict") + except Exception: + continue + return raw.decode("latin-1", errors="replace") # nikdy nespadne + + +def _raw_mapi_strings(msg_path: Path) -> dict: + """Cte klicova textova MAPI pole PRIMO z OLE (mimo extract_msg). + Pouzije se jen kdyz extract_msg vrati degradovane pole.""" + out = {"subject": "", "normalized_subject": "", "sender_name": "", + "sender_email": "", "sender_smtp": "", "body_text": "", "body_html": ""} + try: + ole = olefile.OleFileIO(str(msg_path)) + except Exception: + return out + try: + cpid = _detect_cpid(ole) + wanted = { # MAPI tag -> klic v out + "0037": "subject", "0E1D": "normalized_subject", + "0C1A": "sender_name", "5D01": "sender_smtp", + "0C1F": "sender_email", "1000": "body_text", "1013": "body_html", + } + prefix = "__substg1.0_" + found = {} # key -> (priorita_typu, hodnota) + for entry in ole.listdir(): + if len(entry) != 1: # jen top-level (ne vnorene zpravy) + continue + name = entry[0] + if not name.startswith(prefix): + continue + tag = name[len(prefix):len(prefix) + 4].upper() + key = wanted.get(tag) + if not key: + continue + typ = name[-4:].upper() + prio = {"001F": 3, "001E": 2, "0102": 1}.get(typ, 0) + if prio == 0: + continue + prev = found.get(key) + if prev and prev[0] >= prio: # preferuj unicode > ansi > binarni + continue + try: + raw = ole.openstream(entry).read() + val = _cascade_decode(raw, typ == "001F", cpid) + except Exception: + continue + found[key] = (prio, val) + for key, (_, val) in found.items(): + out[key] = val + finally: + ole.close() + return out + + +def _degraded(s) -> bool: + """Pole je degradovane: prazdne nebo obsahuje U+FFFD (nahradni znak).""" + return (not s) or ("�" in s) + + +def open_message(msg_path: Path): + """Kaskadove otevreni .msg -> (msg, mode) nebo (None, None). + normal bezna cesta + suppress_all tolerantni k vadnym prilohum + override:ENC tolerantni + vnuceny encoding dle codepage property + """ + try: + return extract_msg.Message(str(msg_path)), "normal" + except Exception: + pass + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL), "suppress_all" + except Exception: + pass + encs = [] + try: + ole = olefile.OleFileIO(str(msg_path)) + c = _detect_cpid(ole) + ole.close() + if c: + encs.append(c) + except Exception: + pass + for e in encs + ["cp1250", "cp1252"]: + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL, + overrideEncoding=e), f"override:{e}" + except Exception: + continue + return None, None + + +# ─── Hlavní extrakce ───────────────────────────────────────────────────────── + +def extract_message(msg_path: Path) -> Optional[dict]: + """Parsuje jeden .msg soubor -> MongoDB dokument.""" + msg, parse_mode = open_message(msg_path) + if msg is None: + logging.error("open failed [%s]: vsechny pokusy o otevreni selhaly", msg_path.name) + return None + + try: + # ── Message-ID ──────────────────────────────────────────────── + mid = None + for attr in ("messageId", "message_id", "internetMessageId"): + mid = safe(msg, attr) + if mid: + break + if not mid: + mid = f"filename:{msg_path.stem}" + mid = str(mid).strip() + + # ── Předmět ─────────────────────────────────────────────────── + try: + subject = msg.subject or "" + except Exception: + subject = "" + + normalized_subject = safe(msg, "normalizedSubject", "normalized_subject", default="") + + # ── Tělo ────────────────────────────────────────────────────── + try: + body_text = msg.body or "" + except Exception: + body_text = "" + + body_html = None + try: + bh = msg.htmlBody + if isinstance(bh, bytes): + bh = bh.decode("utf-8", errors="replace") + if bh: + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + except Exception: + pass + + # ── Odesílatel ──────────────────────────────────────────────── + try: + sender_email = msg.sender or "" + except Exception: + sender_email = "" + + sender_name = safe(msg, "senderName", "sender_name", default="") + sender_smtp = safe(msg, "senderSmtpAddress", "sent_representing_smtp_address", default="") + + # ── Příjemci ────────────────────────────────────────────────── + recipients = extract_recipients(msg) + + try: + to_raw = msg.to or "" + except Exception: + to_raw = "" + try: + cc_raw = msg.cc or "" + except Exception: + cc_raw = "" + try: + bcc_raw = getattr(msg, "bcc", None) or "" + except Exception: + bcc_raw = "" + + display_to = safe(msg, "displayTo", "display_to", default="") + display_cc = safe(msg, "displayCc", "display_cc", default="") + + # ── Časy ────────────────────────────────────────────────────── + try: + received_at = parse_date(msg.date) + except Exception: + received_at = None + + sent_at = None + for attr in ("clientSubmitTime", "client_submit_time", "sentOn"): + v = safe(msg, attr) + if v: + sent_at = parse_date(v) + break + + # ── MAPI vlastnosti ─────────────────────────────────────────── + importance = 1 + try: + v = msg.importance + if v is not None: + importance = int(v) + except Exception: + pass + + sensitivity = 0 + try: + v = getattr(msg, "sensitivity", None) + if v is not None: + sensitivity = int(v) + except Exception: + pass + + flag_status = 0 + try: + v = safe(msg, "flagStatus", "flag_status") + if v is not None: + flag_status = int(v) + except Exception: + pass + + conversation_topic = safe(msg, "conversationTopic", "conversation_topic", default="") + + conversation_index = "" + try: + ci = safe(msg, "conversationIndex", "conversation_index") + if isinstance(ci, bytes): + conversation_index = base64.b64encode(ci).decode() + elif ci: + conversation_index = str(ci) + except Exception: + pass + + in_reply_to = safe(msg, "inReplyTo", "in_reply_to", default="") + + internet_refs = [] + try: + refs = safe(msg, "internetReferences", "internet_references") + if isinstance(refs, list): + internet_refs = refs + elif isinstance(refs, str) and refs: + internet_refs = [r.strip() for r in refs.split() if r.strip()] + except Exception: + pass + + categories = [] + try: + cats = safe(msg, "categories") + if isinstance(cats, list): + categories = [str(c) for c in cats if c] + elif isinstance(cats, str) and cats: + categories = [c.strip() for c in re.split(r"[;,]", cats) if c.strip()] + except Exception: + pass + + read_receipt = bool(safe(msg, "readReceiptRequested", "read_receipt_requested", default=False)) + delivery_receipt = bool(safe(msg, "deliveryReceiptRequested", "delivery_receipt_requested", default=False)) + + # ── Internet headers ────────────────────────────────────────── + headers = extract_headers(msg) + + if not in_reply_to: + in_reply_to = headers.get("in_reply_to", "") + if not internet_refs: + refs_str = headers.get("references", "") + if isinstance(refs_str, str) and refs_str: + internet_refs = [r.strip() for r in refs_str.split() if r.strip()] + + # ── Přílohy ─────────────────────────────────────────────────── + attachments = extract_attachments(msg) + + # ── Raw MAPI ────────────────────────────────────────────────── + mapi_raw = extract_mapi_props(msg) + + msg.close() + + # ── Raw-OLE fallback pro degradovana textova pole ───────────── + # Kdyz extract_msg vratil prazdno/� nebo musel hadat encoding + # (override/suppress), docteme klicova pole primo z OLE streamu + # kaskadovym dekodovanim — spolehlivejsi nez jeden vnuceny encoding. + parse_degraded = parse_mode != "normal" + # v non-normal modu byl encoding hadany -> raw kaskade se veri vic + forced = parse_mode != "normal" + if (forced or _degraded(subject) or _degraded(body_text) + or _degraded(sender_email) or (body_html and "�" in body_html)): + raw = _raw_mapi_strings(msg_path) + if raw["subject"] and (forced or _degraded(subject)): + subject = raw["subject"] + if raw["normalized_subject"] and (forced or _degraded(normalized_subject)): + normalized_subject = raw["normalized_subject"] + if raw["body_text"] and (forced or _degraded(body_text)): + body_text = raw["body_text"] + if raw["body_html"] and (forced or not body_html or "�" in body_html): + bh = raw["body_html"] + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + if (raw["sender_smtp"] or raw["sender_email"]) and (forced or _degraded(sender_email)): + sender_email = raw["sender_smtp"] or raw["sender_email"] + if raw["sender_name"] and (forced or _degraded(sender_name)): + sender_name = raw["sender_name"] + if raw["sender_smtp"] and not sender_smtp: + sender_smtp = raw["sender_smtp"] + + # ── Dokument ────────────────────────────────────────────────── + return { + "_id": mid, + "filename": msg_path.name, + + "subject": subject, + "normalized_subject": normalized_subject, + "importance": importance, + "sensitivity": sensitivity, + "flag_status": flag_status, + "read_receipt_requested": read_receipt, + "delivery_receipt_requested": delivery_receipt, + "has_attachments": len(attachments) > 0, + "attachment_count": len(attachments), + "message_size_bytes": msg_path.stat().st_size, + + "conversation_topic": conversation_topic, + "conversation_index": conversation_index, + "in_reply_to": in_reply_to, + "internet_references": internet_refs, + "categories": categories, + + "received_at": received_at, + "sent_at": sent_at, + + "sender": { + "email": sender_email, + "name": sender_name, + "smtp": sender_smtp, + }, + "to": to_raw, + "cc": cc_raw, + "bcc": bcc_raw, + "display_to": display_to, + "display_cc": display_cc, + "recipients": recipients, + + "body_text": body_text, + "body_html": body_html, + + "attachments": attachments, + "headers": headers, + "mapi": mapi_raw, + + "parse_mode": parse_mode, # normal / suppress_all / override:ENC + "parse_degraded": parse_degraded, # True = pouzit fallback (vadna priloha/encoding) + + "parsed_at": datetime.now(timezone.utc).replace(tzinfo=None), + } + + except Exception as e: + logging.error("extract_message failed [%s]: %s", msg_path.name, e) + return None + + +# ─── MongoDB indexy ─────────────────────────────────────────────────────────── + +def create_indexes(col): + print(" Vytvarim indexy...") + col.create_index([("received_at", ASCENDING)]) + col.create_index([("sent_at", ASCENDING)]) + col.create_index([("sender.email", ASCENDING)]) + col.create_index([("filename", ASCENDING)], unique=True, sparse=True) + col.create_index([("conversation_topic", ASCENDING)]) + col.create_index([("has_attachments", ASCENDING)]) + col.create_index([("categories", ASCENDING)]) + col.create_index([("importance", ASCENDING)]) + col.create_index([("flag_status", ASCENDING)]) + col.create_index([ + ("subject", TEXT), + ("body_text", TEXT), + ("to", TEXT), + ("cc", TEXT), + ], name="text_search", default_language="none") + print(" Indexy hotovy.") + + +# ─── MAIN ───────────────────────────────────────────────────────────────────── + +def main(): + ap = argparse.ArgumentParser(description=f"parse_emails v{SCRIPT_VERSION}") + ap.add_argument("--msgs-dir", default=str(MSGS_DIR), + help="Cesta k .msg souborum") + ap.add_argument("--limit", type=int, default=0, + help="Zpracovat max N souboru (0 = vse)") + ap.add_argument("--skip-existing", action="store_true", + help="Preskocit soubory ktere jiz jsou v MongoDB (pokracovani)") + ap.add_argument("--no-indexes", action="store_true", + help="Nevytvorit indexy na konci") + args = ap.parse_args() + + msgs_dir = Path(args.msgs_dir) + start = datetime.now() + + print(f"=== parse_emails v{SCRIPT_VERSION} ===") + print(f"Start: {start.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"Zdroj: {msgs_dir}") + print(f"MongoDB: {MONGO_URI} -> {MONGO_DB}.{MONGO_COL}") + + # MongoDB + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + try: + client.admin.command("ping") + print(" MongoDB OK") + except Exception as e: + print(f" CHYBA: MongoDB neni dostupna -- {e}") + sys.exit(1) + + col = client[MONGO_DB][MONGO_COL] + + # Skip existing — nacti seznam uz importovanych souboru + existing: set = set() + if args.skip_existing: + print(" Nacitam existujici zaznamy z MongoDB...") + existing = set(col.distinct("filename")) + print(f" {len(existing)} jiz importovano") + + # Scan + print(f"\nSkenuji {msgs_dir} ...") + all_files = sorted(msgs_dir.glob("*.msg")) + if args.limit: + all_files = all_files[:args.limit] + + to_process = [f for f in all_files if f.name not in existing] + skipped = len(all_files) - len(to_process) + total = len(to_process) + + print(f" Celkem .msg: {len(all_files)}") + print(f" Preskoceno: {skipped}") + print(f" Ke zpracovani: {total}\n") + + if total == 0: + print("Neni co importovat.") + client.close() + return + + batch = [] + ok_count = 0 + err_count = 0 + + def flush(): + nonlocal ok_count, err_count + if not batch: + return + try: + col.bulk_write(batch, ordered=False) + except Exception as e: + # Cely batch spadl (typicky jeden vadny dokument). Zkusime + # ho zapsat dokument po dokumentu, aby chyba zahodila jen + # skutecne vadny zaznam, ne celych BATCH_SIZE. + logging.error("bulk_write spadl (%s) -- prepinam na per-dokument", e) + print(f" CHYBA bulk_write: {e} -- zkousim per-dokument") + for op in batch: + try: + col.bulk_write([op], ordered=False) + except Exception as e2: + try: + bad_id = getattr(op, "_filter", {}).get("_id", "?") + except Exception: + bad_id = "?" + logging.error("per-dokument selhal [_id=%s]: %s", bad_id, e2) + print(f" ZAHOZEN _id={bad_id}: {e2}") + ok_count -= 1 + err_count += 1 + batch.clear() + + for i, msg_path in enumerate(to_process, 1): + doc = extract_message(msg_path) + + if doc is None: + err_count += 1 + else: + batch.append(UpdateOne({"_id": doc["_id"]}, {"$set": doc}, upsert=True)) + ok_count += 1 + + if len(batch) >= BATCH_SIZE: + flush() + + # Výpis každého emailu + status = "ERR " if doc is None else "OK " + subject_str = (doc.get("subject") or "")[:60] if doc else "?" + sender_str = (doc.get("sender", {}).get("email") or "")[:40] if doc else "?" + print(f" {i:>6}/{total} {status} {subject_str:<60} {sender_str}") + + if i % 500 == 0: + elapsed = (datetime.now() - start).total_seconds() + rate = i / elapsed if elapsed > 0 else 0 + eta_s = int((total - i) / rate) if rate > 0 else 0 + print(f" {'─'*80}") + print(f" Průběh: ok={ok_count} err={err_count} " + f"{rate:.1f} msg/s ETA {eta_s//3600}h{(eta_s%3600)//60}m") + print(f" {'─'*80}") + + flush() + + elapsed_total = (datetime.now() - start).total_seconds() + print(f"\n{'='*52}") + print(f"Vysledek: ok={ok_count} | skip={skipped} | err={err_count}") + print(f"Celkovy cas: {int(elapsed_total//3600)}h {int((elapsed_total%3600)//60)}m {int(elapsed_total%60)}s") + print(f"Dokumentu v kolekci: {col.count_documents({})}") + + if not args.no_indexes: + print() + create_indexes(col) + + print(f"\nKonec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + if err_count: + print(f"Chyby logovany do: {LOG_FILE}") + + client.close() + + +if __name__ == "__main__": + main() diff --git a/EmailsImport/jnj_tower_ingest_v1.1.md b/EmailsImport/jnj_tower_ingest_v1.1.md new file mode 100644 index 0000000..8f2c7d9 --- /dev/null +++ b/EmailsImport/jnj_tower_ingest_v1.1.md @@ -0,0 +1,80 @@ +# jnj_tower_ingest v1.1.0 + +**Soubor:** `jnj_tower_ingest_v1.1.py` +**Datum:** 2026-06-10 +**Autor:** vladimir.buzalka +**Běží:** Docker kontejner `python-runner` na Unraid Tower (192.168.1.76), u MongoDB. + +## Co to je + +Sjednocený **Tower-side ingest** JNJ e-mailů — tři dříve oddělené části v jednom běhu: + +| Fáze | Dříve samostatně | Co dělá | +|---|---|---| +| **1. PARSE** | `parse_emails_tower_v1.3.py` | `.msg` z `/mnt/JNJEMAILS` → dokument v Mongo `emaily."vbuzalka@its.jnj.com"` (tělo, přílohy, hlavičky, MAPI). Inkrementálně přes **mtime watermark** (`jnj_sync_state`/`_id="parse_state"`). | +| **2. SYNC** | `sync_jnj_state_v1.0.py` | nejnovější SQLite (read-only) → zrcadlo `jnj_messages` + doplnění `jnj_folder`/stavu do `emaily`. Watermark `updated_at` + zkratka `last_db`. | +| **3. ENRICH** | `jnj_emails_to_fulltext_v1.0.py` | doindexuje JNJ schránku do **PG fulltextu** zavoláním **sdíleného** `5_enrich_fulltext_emails_vX.Y.py --mailbox vbuzalka@its.jnj.com` (stejný extractor jako Graph pipeline → konzistentní schéma). | + +**Pořadí: parse → sync → enrich.** Čerstvě naparsovaný mail dostane v jednom běhu tělo +(parse) + cestu (sync) + fulltext (enrich). Klíč všude = Internet Message-ID = Mongo `_id`. + +## Inkrementálnost (cron každých 5 min) + +- **PARSE** — jen `.msg` s `mtime > parse_state.last_parse_mtime`. 1. běh = seed dle + filename v Mongu, pak čistě mtime. `--full` reparsuje vše. Indexy jen při full/seed/`--reindex`. +- **SYNC** — watermark `updated_at` + zkratka `last_db` (stejná SQLite → no-op). +- **ENRICH** — spustí se **jen když parse přidal nové dokumenty** (jinak přeskočí — JNJ + stejně enrichuje hlavní Graph pipeline v 6:00/18:00). Verze enrich se **auto-detekuje** + (nejnovější `/scripts/5_enrich_fulltext_emails_v*.py`). `--no-enrich` vypne, + `--enrich-always` vynutí. + +Tři nezávislé události (nová `.msg` / nová `.db` / nové doc pro PG) → skript udělá jen to, +co má práci; jinak levný no-op. + +## Vztah ke Graph pipeline + +Hlavní `0_run_pipeline` (Graph API) zpracovává schránky buzalka.cz a **JNJ přeskakuje** +(`SKIP_MAILBOXES`, žádné API). JNJ řeší tenhle skript přes `.msg`. Obě cesty ústí do téhož +Monga `emaily` a přes **sdílený `5_enrich`** do téhož PG `MongoEmaily.emails`. Servisní +kolekce `jnj_messages` + `jnj_sync_state` jsou v enrich `NON_MAILBOX_COLLECTIONS` +(nejsou schránky → nejdou do PG). + +## Argumenty + +| Argument | Význam | +|---|---| +| `--dry-run` | nic nezapíše, jen plán všech fází | +| `--full` | parse: reparsuj vše; sync: ignoruj watermark; enrich: vynuť | +| `--limit N` | max N souborů (parse) / řádků (sync) | +| `--reindex` | vynutí indexy po parse | +| `--force` | sync: ignoruj `last_db` | +| `--parse-only` / `--sync-only` / `--enrich-only` | jen daná fáze | +| `--no-enrich` | přeskoč enrich | +| `--enrich-always` | spusť enrich i bez nových dokumentů | + +## Spouštění + +```bash +docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.1.py --dry-run +docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.1.py # cron +docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.1.py --enrich-only +``` + +## Plánování (HOTOVO) + +Unraid User Scripts úloha `jnj_state_sync` (cron `*/5 * * * *`) — wrapper s `flock` volá +`docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.1.py`. Loguje jen reálnou +práci/chyby do `/mnt/user/Scripts/logs/jnj_tower_ingest.log` +(grep `Zapisuji|PARSE hotovo|SYNC hotovo|ENRICH hotovo|CHYBA|Traceback`). + +## Revert + +`jnj_tower_ingest_v1.0.py` (bez enrich) + `parse_emails_tower_v1.3.py` + +`sync_jnj_state_v1.0.py` zůstávají v `/scripts/` jako pojistka. Návrat = přepsat wrapper +zpět. `jnj_emails_to_fulltext` přesunut do Trash (nahrazen fází 3). + +## Historie verzí + +- **1.0.0** 2026-06-10 — sjednocení parse + sync (mtime watermark, pořadí parse→sync). +- **1.1.0** 2026-06-10 — + fáze ENRICH (sdílený `5_enrich --mailbox`, auto-detekce verze, + jen při nových dokumentech). Nahrazuje `jnj_emails_to_fulltext_v1.0`. diff --git a/EmailsImport/jnj_tower_ingest_v1.1.py b/EmailsImport/jnj_tower_ingest_v1.1.py new file mode 100644 index 0000000..4b77a44 --- /dev/null +++ b/EmailsImport/jnj_tower_ingest_v1.1.py @@ -0,0 +1,1108 @@ +""" +jnj_tower_ingest v1.1 +Nazev: jnj_tower_ingest_v1.1.py +Verze: 1.1.0 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Popis: + Sjednoceny Tower-side ingest JNJ e-mailu. Spojuje tri drive oddelene + casti do jednoho behu (vse bezi v kontejneru python-runner u Monga): + + FAZE 1 — PARSE (drive parse_emails_tower_v1.3.py): + .msg soubory z /mnt/JNJEMAILS -> dokument v Mongo + emaily."vbuzalka@its.jnj.com" (bohata extrakce: telo, prilohy, + hlavicky, MAPI props, ...). _id = Internet Message-ID. + INKREMENTALNE: parsuje jen soubory novejsi nez mtime watermark + (jnj_sync_state/_id="parse_state"). Prvni beh = seed dle filename + v Mongu. --full reparsuje vse. + + FAZE 2 — SYNC (drive sync_jnj_state_v1.0.py): + nejnovejsi /mnt/JNJEMAILS/db/jnjemails_*.db (SQLite, JEN CTENI ro) + -> zrcadlo do Mongo kolekce 'jnj_messages' (upsert) + -> doplneni cesty/stavu do emaily."vbuzalka@its.jnj.com": + jnj_folder = COALESCE(jnj_folder, folder) + jnj_is_read, jnj_not_in_mailbox, jnj_left_mailbox_at, + jnj_folder_synced_at (match _id==message_id, fallback + filename; BEZ upsertu — nezakladame stuby). + Inkrementalne pres watermark updated_at (jnj_sync_state/_id= + "watermark") + zkratka last_db (stejna DB -> hned no-op). + + FAZE 3 — ENRICH (drive jnj_emails_to_fulltext_v1.0.py): + doindexuje JNJ schranku do PG fulltextu zavolanim SDILENEHO + skriptu 5_enrich_fulltext_emails_vX.Y.py --mailbox + "vbuzalka@its.jnj.com" (stejny extractor jako Graph pipeline -> + konzistentni schema). Verze enrich se auto-detekuje (nejnovejsi + /scripts/5_enrich_fulltext_emails_v*.py). Spousti se JEN kdyz + parse pridal nove dokumenty (jinak preskok — JNJ stejne enrichuje + pipeline v 6:00/18:00). --no-enrich vypne, --enrich-always vynuti. + + PORADI: parse -> sync -> enrich. Cerstve naparsovane maily dostanou cestu + (sync) i fulltext (enrich) hned ve stejnem behu (drive: pokud sync/enrich + predbehl parse, novy mail nemel co zpracovat). Tri nezavisle udalosti + (nova .msg / nova .db / nove doc pro PG) -> skript udela jen to, co ma + praci; jinak levny no-op (vhodne pro cron kazdych 5 minut). + + Spojovaci klic vsude = Internet Message-ID = Mongo _id. + +Prostredi: + Docker container "python-runner" na Unraid Tower. + /mnt/user/JNJEMAILS -> /mnt/JNJEMAILS (.msg v rootu, .db v db/) + MongoDB 192.168.1.76:27017 (externi). + +Argumenty: + --dry-run nic nezapise, jen spocita a vypise plan vsech fazi + --full parse: reparsuj vse; sync: ignoruj watermark + --limit N max N souboru (parse) / radku (sync) — test + --reindex vynut vytvoreni indexu na konci parse faze + --force sync: ignoruj zkratku last_db (zpracuj i hotovou DB) + --parse-only spust jen fazi PARSE + --sync-only spust jen fazi SYNC + --enrich-only spust jen fazi ENRICH (vynuti enrich i bez novych dat) + --no-enrich preskoc fazi ENRICH + --enrich-always spust enrich i kdyz parse nepridal nove dokumenty + +Spousteni (v kontejneru python-runner): + # Test: + docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.1.py --dry-run + # Ostry inkrementalni beh (cron): + docker exec python-runner python3 /scripts/jnj_tower_ingest_v1.1.py + # Plny reparse + reindex: + docker exec -it python-runner python3 /scripts/jnj_tower_ingest_v1.1.py --full --reindex + +Zavislosti (v image python-runner): + extract-msg==0.55.0, olefile, pymongo, python-dateutil, sqlite3 (stdlib). + Enrich faze deleguje na 5_enrich_fulltext_emails (psycopg, bs4 v image). + Python 3.10+. + +Historie verzi: + 1.0.0 2026-06-10 Sjednoceni parse_emails_tower_v1.3 + sync_jnj_state_v1.0 + do jedineho skriptu. Parse zinkrementalnen pres mtime + watermark (drive scan celeho adresare kazdy beh). + Indexy jen pri full/seed/--reindex. Poradi parse->sync. + 1.1.0 2026-06-10 + FAZE 3 ENRICH: deleguje na sdileny + 5_enrich_fulltext_emails --mailbox (auto-detekce verze), + jen kdyz parse pridal nove dokumenty. Nahrazuje + jnj_emails_to_fulltext_v1.0.py (ten -> Trash). + Flagy --enrich-only/--no-enrich/--enrich-always. +""" + +import sys +import os +import re +import glob +import logging +import argparse +import base64 +import struct +import sqlite3 +import subprocess +from pathlib import Path +from datetime import datetime, timezone +from typing import Optional + +import extract_msg +from extract_msg.enums import ErrorBehavior +import olefile +from dateutil import parser as dtparser +from pymongo import MongoClient, UpdateOne, ASCENDING, TEXT + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +MSGS_DIR = Path("/mnt/JNJEMAILS") +DB_DIR = "/mnt/JNJEMAILS/db" +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +EMAILS_COL = "vbuzalka@its.jnj.com" +MIRROR_COL = "jnj_messages" +STATE_COL = "jnj_sync_state" +BATCH_SIZE = 200 +LOG_FILE = Path(__file__).parent / "jnj_tower_ingest_errors.log" +ENRICH_GLOB = "/scripts/5_enrich_fulltext_emails_v*.py" # sdileny PG enrich +SCRIPT_VERSION = "1.1.0" + +# Sloupce zrcadlene ze SQLite messages -> jnj_messages +ROW_COLS = ["message_id", "subject", "sender", "received_at", "folder", + "jnj_folder", "is_read", "not_in_mailbox_anymore", "left_mailbox_at", + "entry_id", "graph_id", "updated_at", "source"] +# ────────────────────────────────────────────────────────────────────────────── + +logging.basicConfig( + filename=str(LOG_FILE), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + + +# ══════════════════════════════════════════════════════════════════════════════ +# FAZE 1 — PARSE (.msg -> Mongo emaily) [drive parse_emails_tower_v1.3.py] +# ══════════════════════════════════════════════════════════════════════════════ + +def safe(obj, *attrs, default=None): + """Bezpecne cteni atributu — vrati prvni non-None hodnotu.""" + for attr in attrs: + try: + val = getattr(obj, attr, None) + if val is None: + continue + if isinstance(val, str) and not val.strip(): + continue + return val + except Exception: + continue + return default + + +def parse_date(raw) -> Optional[datetime]: + """Libovolny datum -> UTC datetime bez tzinfo (pro MongoDB).""" + if raw is None: + return None + if isinstance(raw, datetime): + if raw.tzinfo: + return raw.astimezone(timezone.utc).replace(tzinfo=None) + return raw + try: + dt = dtparser.parse(str(raw)) + if dt.tzinfo: + return dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt + except Exception: + return None + + +_INT64_MIN, _INT64_MAX = -(2 ** 63), 2 ** 63 - 1 + + +def to_bson(val): + """Konvertuje hodnotu na BSON-serializovatelny typ. + + Pozor: BSON umi jen signed int64. Python ma neomezene integery, takze + velke MAPI hodnoty (PR_CHANGE_KEY, FILETIME, 64-bit handle) mimo rozsah + int64 prevadime na string — jinak cely bulk_write spadne na + 'MongoDB can only handle up to 8-byte ints'. + """ + # bool musi byt PRED int (isinstance(True, int) == True) + if isinstance(val, bool): + return val + if isinstance(val, bytes): + return val.hex() if len(val) <= 128 else f"" + if isinstance(val, datetime): + return parse_date(val) + if isinstance(val, int): + return val if _INT64_MIN <= val <= _INT64_MAX else str(val) + if isinstance(val, (str, float, type(None))): + return val + if isinstance(val, list): + return [to_bson(v) for v in val] + try: + iv = int(val) + return iv if _INT64_MIN <= iv <= _INT64_MAX else str(iv) + except Exception: + pass + return str(val) + + +def extract_headers(msg) -> dict: + headers = {} + try: + hdr = msg.header + if not hdr: + return {} + from email.header import decode_header as _dh + + def _decode(v: str) -> str: + try: + parts = _dh(v) + out = "" + for part, enc in parts: + out += part.decode(enc or "utf-8", errors="replace") if isinstance(part, bytes) else part + return out + except Exception: + return v + + for key in set(hdr.keys()): + k = key.lower().replace("-", "_") + vals = [_decode(v) for v in hdr.get_all(key, [])] + headers[k] = vals if len(vals) > 1 else (vals[0] if vals else "") + except Exception as e: + logging.error("extract_headers: %s", e) + return headers + + +def extract_recipients(msg) -> list: + result = [] + type_map = {1: "to", 2: "cc", 3: "bcc"} + try: + for r in msg.recipients: + rtype = getattr(r, "type", 1) + try: + rtype = int(rtype) + except Exception: + try: + rtype = int(rtype.value) + except Exception: + rtype = 1 + rec = { + "type": type_map.get(rtype, "to"), + "email": safe(r, "email", default=""), + "name": safe(r, "name", default=""), + } + result.append(rec) + except Exception as e: + logging.error("extract_recipients: %s", e) + return result + + +def extract_attachments(msg) -> list: + result = [] + try: + for att in msg.attachments: + fname = safe(att, "longFilename", "shortFilename", default="") + if not fname: + continue + size = 0 + try: + d = att.data + size = len(d) if d else 0 + except Exception: + pass + result.append({ + "filename": fname, + "size_bytes": size, + "mime_type": safe(att, "mimetype", "mimeType", default="application/octet-stream"), + "content_id": safe(att, "cid", default=None), + "is_inline": bool(safe(att, "isInline", default=False)), + }) + except Exception as e: + logging.error("extract_attachments: %s", e) + return result + + +def extract_mapi_props(msg) -> dict: + """Vsechny raw MAPI properties jako {0xXXXX: value}.""" + result = {} + try: + props = msg.props + if not hasattr(props, "items"): + return {} + for key, prop in props.items(): + try: + val = to_bson(prop.value) + prop_id = f"0x{key[:4].upper()}" if len(key) >= 4 else f"0x{key.upper()}" + result[prop_id] = val + except Exception: + pass + except Exception as e: + logging.error("extract_mapi_props: %s", e) + return result + + +# ─── Tolerantni otevirani a raw-OLE fallback ───────────────────────────────── +_CPID_TO_CODEC = { + 1250: "cp1250", 1251: "cp1251", 1252: "cp1252", 1253: "cp1253", + 1254: "cp1254", 1255: "cp1255", 1256: "cp1256", 1257: "cp1257", + 1258: "cp1258", 874: "cp874", 932: "shift_jis", 936: "gb2312", + 949: "euc_kr", 950: "big5", 65001: "utf-8", 28591: "iso-8859-1", + 28592: "iso-8859-2", 20127: "ascii", +} + + +def _read_u32_prop(ole, propid): + """Precte 32-bit hodnotu MAPI property z top-level __properties_version1.0.""" + try: + data = ole.openstream("__properties_version1.0").read() + except Exception: + return None + body = data[32:] # 32-bajtova hlavicka top-level property streamu + for i in range(0, len(body) - 16 + 1, 16): + rec = body[i:i + 16] + tag = struct.unpack("> 16) & 0xFFFF) == propid: + return struct.unpack(" Optional[str]: + """Codec dle PR_INTERNET_CPID / PR_MESSAGE_CODEPAGE (jako napoveda, ne dogma).""" + for pid in (0x3FDE, 0x3FFD): # INTERNET_CPID, MESSAGE_CODEPAGE + codec = _CPID_TO_CODEC.get(_read_u32_prop(ole, pid)) + # utf-8/ascii nejsou dobry hint pro 8-bit stream (casto lzou) + if codec and codec not in ("utf-8", "ascii"): + return codec + return None + + +def _cascade_decode(raw: bytes, is_unicode: bool, cpid_codec: Optional[str]) -> str: + """Dekoduje bajty MAPI stringu. Hlavickam se neveri — zkousime striktne + v poradi priorit a vezmeme prvni, co projde bez chyby.""" + if not raw: + return "" + if is_unicode: # PT_UNICODE = utf-16-le + try: + return raw.decode("utf-16-le") + except Exception: + return raw.decode("utf-16-le", errors="replace") + order = ["utf-8"] # utf-8 strict = silny rozlisovac + if cpid_codec: + order.append(cpid_codec) + order += ["cp1250", "cp1252", "gb2312", "big5"] + for enc in order: + try: + return raw.decode(enc, errors="strict") + except Exception: + continue + return raw.decode("latin-1", errors="replace") # nikdy nespadne + + +def _raw_mapi_strings(msg_path: Path) -> dict: + """Cte klicova textova MAPI pole PRIMO z OLE (mimo extract_msg). + Pouzije se jen kdyz extract_msg vrati degradovane pole.""" + out = {"subject": "", "normalized_subject": "", "sender_name": "", + "sender_email": "", "sender_smtp": "", "body_text": "", "body_html": ""} + try: + ole = olefile.OleFileIO(str(msg_path)) + except Exception: + return out + try: + cpid = _detect_cpid(ole) + wanted = { # MAPI tag -> klic v out + "0037": "subject", "0E1D": "normalized_subject", + "0C1A": "sender_name", "5D01": "sender_smtp", + "0C1F": "sender_email", "1000": "body_text", "1013": "body_html", + } + prefix = "__substg1.0_" + found = {} # key -> (priorita_typu, hodnota) + for entry in ole.listdir(): + if len(entry) != 1: # jen top-level (ne vnorene zpravy) + continue + name = entry[0] + if not name.startswith(prefix): + continue + tag = name[len(prefix):len(prefix) + 4].upper() + key = wanted.get(tag) + if not key: + continue + typ = name[-4:].upper() + prio = {"001F": 3, "001E": 2, "0102": 1}.get(typ, 0) + if prio == 0: + continue + prev = found.get(key) + if prev and prev[0] >= prio: # preferuj unicode > ansi > binarni + continue + try: + raw = ole.openstream(entry).read() + val = _cascade_decode(raw, typ == "001F", cpid) + except Exception: + continue + found[key] = (prio, val) + for key, (_, val) in found.items(): + out[key] = val + finally: + ole.close() + return out + + +def _degraded(s) -> bool: + """Pole je degradovane: prazdne nebo obsahuje U+FFFD (nahradni znak).""" + return (not s) or ("�" in s) + + +def open_message(msg_path: Path): + """Kaskadove otevreni .msg -> (msg, mode) nebo (None, None).""" + try: + return extract_msg.Message(str(msg_path)), "normal" + except Exception: + pass + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL), "suppress_all" + except Exception: + pass + encs = [] + try: + ole = olefile.OleFileIO(str(msg_path)) + c = _detect_cpid(ole) + ole.close() + if c: + encs.append(c) + except Exception: + pass + for e in encs + ["cp1250", "cp1252"]: + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL, + overrideEncoding=e), f"override:{e}" + except Exception: + continue + return None, None + + +def extract_message(msg_path: Path) -> Optional[dict]: + """Parsuje jeden .msg soubor -> MongoDB dokument.""" + msg, parse_mode = open_message(msg_path) + if msg is None: + logging.error("open failed [%s]: vsechny pokusy o otevreni selhaly", msg_path.name) + return None + + try: + # ── Message-ID ──────────────────────────────────────────────── + mid = None + for attr in ("messageId", "message_id", "internetMessageId"): + mid = safe(msg, attr) + if mid: + break + if not mid: + mid = f"filename:{msg_path.stem}" + mid = str(mid).strip() + + # ── Predmet ─────────────────────────────────────────────────── + try: + subject = msg.subject or "" + except Exception: + subject = "" + + normalized_subject = safe(msg, "normalizedSubject", "normalized_subject", default="") + + # ── Telo ────────────────────────────────────────────────────── + try: + body_text = msg.body or "" + except Exception: + body_text = "" + + body_html = None + try: + bh = msg.htmlBody + if isinstance(bh, bytes): + bh = bh.decode("utf-8", errors="replace") + if bh: + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + except Exception: + pass + + # ── Odesilatel ──────────────────────────────────────────────── + try: + sender_email = msg.sender or "" + except Exception: + sender_email = "" + + sender_name = safe(msg, "senderName", "sender_name", default="") + sender_smtp = safe(msg, "senderSmtpAddress", "sent_representing_smtp_address", default="") + + # ── Prijemci ────────────────────────────────────────────────── + recipients = extract_recipients(msg) + + try: + to_raw = msg.to or "" + except Exception: + to_raw = "" + try: + cc_raw = msg.cc or "" + except Exception: + cc_raw = "" + try: + bcc_raw = getattr(msg, "bcc", None) or "" + except Exception: + bcc_raw = "" + + display_to = safe(msg, "displayTo", "display_to", default="") + display_cc = safe(msg, "displayCc", "display_cc", default="") + + # ── Casy ────────────────────────────────────────────────────── + try: + received_at = parse_date(msg.date) + except Exception: + received_at = None + + sent_at = None + for attr in ("clientSubmitTime", "client_submit_time", "sentOn"): + v = safe(msg, attr) + if v: + sent_at = parse_date(v) + break + + # ── MAPI vlastnosti ─────────────────────────────────────────── + importance = 1 + try: + v = msg.importance + if v is not None: + importance = int(v) + except Exception: + pass + + sensitivity = 0 + try: + v = getattr(msg, "sensitivity", None) + if v is not None: + sensitivity = int(v) + except Exception: + pass + + flag_status = 0 + try: + v = safe(msg, "flagStatus", "flag_status") + if v is not None: + flag_status = int(v) + except Exception: + pass + + conversation_topic = safe(msg, "conversationTopic", "conversation_topic", default="") + + conversation_index = "" + try: + ci = safe(msg, "conversationIndex", "conversation_index") + if isinstance(ci, bytes): + conversation_index = base64.b64encode(ci).decode() + elif ci: + conversation_index = str(ci) + except Exception: + pass + + in_reply_to = safe(msg, "inReplyTo", "in_reply_to", default="") + + internet_refs = [] + try: + refs = safe(msg, "internetReferences", "internet_references") + if isinstance(refs, list): + internet_refs = refs + elif isinstance(refs, str) and refs: + internet_refs = [r.strip() for r in refs.split() if r.strip()] + except Exception: + pass + + categories = [] + try: + cats = safe(msg, "categories") + if isinstance(cats, list): + categories = [str(c) for c in cats if c] + elif isinstance(cats, str) and cats: + categories = [c.strip() for c in re.split(r"[;,]", cats) if c.strip()] + except Exception: + pass + + read_receipt = bool(safe(msg, "readReceiptRequested", "read_receipt_requested", default=False)) + delivery_receipt = bool(safe(msg, "deliveryReceiptRequested", "delivery_receipt_requested", default=False)) + + # ── Internet headers ────────────────────────────────────────── + headers = extract_headers(msg) + + if not in_reply_to: + in_reply_to = headers.get("in_reply_to", "") + if not internet_refs: + refs_str = headers.get("references", "") + if isinstance(refs_str, str) and refs_str: + internet_refs = [r.strip() for r in refs_str.split() if r.strip()] + + # ── Prilohy ─────────────────────────────────────────────────── + attachments = extract_attachments(msg) + + # ── Raw MAPI ────────────────────────────────────────────────── + mapi_raw = extract_mapi_props(msg) + + msg.close() + + # ── Raw-OLE fallback pro degradovana textova pole ───────────── + parse_degraded = parse_mode != "normal" + forced = parse_mode != "normal" + if (forced or _degraded(subject) or _degraded(body_text) + or _degraded(sender_email) or (body_html and "�" in body_html)): + raw = _raw_mapi_strings(msg_path) + if raw["subject"] and (forced or _degraded(subject)): + subject = raw["subject"] + if raw["normalized_subject"] and (forced or _degraded(normalized_subject)): + normalized_subject = raw["normalized_subject"] + if raw["body_text"] and (forced or _degraded(body_text)): + body_text = raw["body_text"] + if raw["body_html"] and (forced or not body_html or "�" in body_html): + bh = raw["body_html"] + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + if (raw["sender_smtp"] or raw["sender_email"]) and (forced or _degraded(sender_email)): + sender_email = raw["sender_smtp"] or raw["sender_email"] + if raw["sender_name"] and (forced or _degraded(sender_name)): + sender_name = raw["sender_name"] + if raw["sender_smtp"] and not sender_smtp: + sender_smtp = raw["sender_smtp"] + + # ── Dokument ────────────────────────────────────────────────── + return { + "_id": mid, + "filename": msg_path.name, + + "subject": subject, + "normalized_subject": normalized_subject, + "importance": importance, + "sensitivity": sensitivity, + "flag_status": flag_status, + "read_receipt_requested": read_receipt, + "delivery_receipt_requested": delivery_receipt, + "has_attachments": len(attachments) > 0, + "attachment_count": len(attachments), + "message_size_bytes": msg_path.stat().st_size, + + "conversation_topic": conversation_topic, + "conversation_index": conversation_index, + "in_reply_to": in_reply_to, + "internet_references": internet_refs, + "categories": categories, + + "received_at": received_at, + "sent_at": sent_at, + + "sender": { + "email": sender_email, + "name": sender_name, + "smtp": sender_smtp, + }, + "to": to_raw, + "cc": cc_raw, + "bcc": bcc_raw, + "display_to": display_to, + "display_cc": display_cc, + "recipients": recipients, + + "body_text": body_text, + "body_html": body_html, + + "attachments": attachments, + "headers": headers, + "mapi": mapi_raw, + + "parse_mode": parse_mode, + "parse_degraded": parse_degraded, + + "parsed_at": datetime.now(timezone.utc).replace(tzinfo=None), + } + + except Exception as e: + logging.error("extract_message failed [%s]: %s", msg_path.name, e) + return None + + +def create_indexes(col): + print(" Vytvarim indexy...") + col.create_index([("received_at", ASCENDING)]) + col.create_index([("sent_at", ASCENDING)]) + col.create_index([("sender.email", ASCENDING)]) + col.create_index([("filename", ASCENDING)], unique=True, sparse=True) + col.create_index([("conversation_topic", ASCENDING)]) + col.create_index([("has_attachments", ASCENDING)]) + col.create_index([("categories", ASCENDING)]) + col.create_index([("importance", ASCENDING)]) + col.create_index([("flag_status", ASCENDING)]) + col.create_index([ + ("subject", TEXT), + ("body_text", TEXT), + ("to", TEXT), + ("cc", TEXT), + ], name="text_search", default_language="none") + print(" Indexy hotovy.") + + +def run_parse(col, state_col, args, now) -> dict: + """FAZE 1: inkrementalni parse .msg -> emaily. Vraci statistiku.""" + stats = {"mode": None, "total_files": 0, "candidates": 0, "ok": 0, "err": 0} + print("\n=== FAZE 1: PARSE (.msg -> emaily) ===") + + all_files = sorted(MSGS_DIR.glob("*.msg")) + stats["total_files"] = len(all_files) + if not all_files: + print(" Zadne .msg ve zdroji -> preskakuji.") + return stats + max_mtime = max(f.stat().st_mtime for f in all_files) + + ps = state_col.find_one({"_id": "parse_state"}) or {} + last_mtime = ps.get("last_parse_mtime") + + if args.full: + candidates = all_files + mode = "full" + elif last_mtime is None: + print(" Prvni beh (zadny mtime watermark) -> seed dle filename v Mongu...") + existing = set(col.distinct("filename")) + candidates = [f for f in all_files if f.name not in existing] + mode = "seed" + print(f" V Mongu jiz {len(existing)} filename; nove k naparsovani: {len(candidates)}") + else: + candidates = [f for f in all_files if f.stat().st_mtime > last_mtime] + mode = "incremental" + if args.limit: + candidates = candidates[:args.limit] + + stats["mode"] = mode + stats["candidates"] = len(candidates) + wm_str = datetime.fromtimestamp(last_mtime).strftime("%Y-%m-%d %H:%M:%S") if last_mtime else "(zadny)" + print(f" Rezim: {mode} | .msg celkem {len(all_files)} | watermark {wm_str} | ke zpracovani {len(candidates)}") + + if not candidates: + print(" Nic noveho k parsovani.") + # I tak posun watermark na nejnovejsi soubor (krome --full a dry-run) + if not args.dry_run and mode != "full": + state_col.update_one({"_id": "parse_state"}, + {"$set": {"last_parse_mtime": max_mtime, "last_parse_at": now}}, upsert=True) + return stats + + if args.dry_run: + print(f" DRY-RUN: naparsoval bych {len(candidates)} souboru (Mongo se nemeni). Ukazka:") + for f in candidates[:10]: + mt = datetime.fromtimestamp(f.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") + print(f" + {f.name} (mtime {mt})") + if len(candidates) > 10: + print(f" ... a dalsich {len(candidates) - 10}") + return stats + + batch = [] + verbose = len(candidates) <= 30 + + def flush(): + if not batch: + return + try: + col.bulk_write(batch, ordered=False) + except Exception as e: + logging.error("bulk_write spadl (%s) -- prepinam na per-dokument", e) + print(f" CHYBA bulk_write: {e} -- zkousim per-dokument") + for op in batch: + try: + col.bulk_write([op], ordered=False) + except Exception as e2: + try: + bad_id = getattr(op, "_filter", {}).get("_id", "?") + except Exception: + bad_id = "?" + logging.error("per-dokument selhal [_id=%s]: %s", bad_id, e2) + print(f" ZAHOZEN _id={bad_id}: {e2}") + stats["ok"] -= 1 + stats["err"] += 1 + batch.clear() + + for i, msg_path in enumerate(candidates, 1): + doc = extract_message(msg_path) + if doc is None: + stats["err"] += 1 + else: + batch.append(UpdateOne({"_id": doc["_id"]}, {"$set": doc}, upsert=True)) + stats["ok"] += 1 + if len(batch) >= BATCH_SIZE: + flush() + if verbose: + status = "ERR " if doc is None else "OK " + subj = (doc.get("subject") or "")[:60] if doc else "?" + print(f" {i:>5}/{len(candidates)} {status} {subj}") + elif i % 500 == 0: + print(f" prubeh {i}/{len(candidates)} ok={stats['ok']} err={stats['err']}") + flush() + + # Indexy jen pri full/seed/--reindex (v inkrementalnim behu uz existuji) + if mode in ("full", "seed") or args.reindex: + create_indexes(col) + + # Posun watermark na nejnovejsi soubor + state_col.update_one({"_id": "parse_state"}, + {"$set": {"last_parse_mtime": max_mtime, "last_parse_at": now, + "last_parsed_count": stats["ok"], "last_parse_mode": mode}}, + upsert=True) + print(f" PARSE hotovo: ok={stats['ok']} err={stats['err']} " + f"watermark={datetime.fromtimestamp(max_mtime):%Y-%m-%d %H:%M:%S}") + return stats + + +# ══════════════════════════════════════════════════════════════════════════════ +# FAZE 2 — SYNC (SQLite -> Mongo jnj_messages + emaily cesta) +# [drive sync_jnj_state_v1.0.py] +# ══════════════════════════════════════════════════════════════════════════════ + +def norm_mid(s: str) -> str: + return (s or "").strip().strip("<>").strip() + + +def coalesce_path(jnjf, fld) -> str: + return jnjf if (jnjf and jnjf.strip()) else (fld or "") + + +def newest_db(): + cands = glob.glob(os.path.join(DB_DIR, "jnjemails_*.db")) or glob.glob(os.path.join(DB_DIR, "*.db")) + return max(cands, key=os.path.getmtime) if cands else None + + +def run_sync(db, args, now) -> dict: + """FAZE 2: SQLite -> jnj_messages (zrcadlo) + emaily (cesta/stav).""" + stats = {"total": 0, "matched": 0, "skipped": False} + print("\n=== FAZE 2: SYNC (SQLite -> jnj_messages + emaily cesta) ===") + + emails = db[EMAILS_COL] + state_col = db[STATE_COL] + + db_path = newest_db() + if not db_path: + print(f" Zadna .db v {DB_DIR} -> preskakuji.") + stats["skipped"] = True + return stats + db_name = os.path.basename(db_path) + print(f" SQLite: {db_name}") + + st = state_col.find_one({"_id": "watermark"}) or {} + + # ── Zkratka: tuto DB uz jsme zpracovali? (jen inkrementalni rezim) ───── + if not args.full and not args.force and st.get("last_db") == db_name: + print(f" DB {db_name} uz byla zpracovana (last_db) -> nic na praci.") + stats["skipped"] = True + return stats + + wm = None if args.full else st.get("last_updated_at") + print(f" Watermark: {wm or '(zadny -> vse)'}") + + # ── SQLite (read-only) ──────────────────────────────────────────────── + con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + con.row_factory = sqlite3.Row + available = {row[1] for row in con.execute("PRAGMA table_info(messages)")} + sel_cols = [c for c in ROW_COLS if c in available] + missing = [c for c in ROW_COLS if c not in available] + if missing: + print(f" (DB nema sloupce: {', '.join(missing)} -> default None/0)") + has_updated = "updated_at" in available + q = f"SELECT {', '.join(sel_cols)} FROM messages" + params = () + if wm and has_updated: + q += " WHERE updated_at > ?" + params = (wm,) + elif wm and not has_updated: + print(" (DB nema updated_at -> watermark ignorovan, beru vse)") + wm = None + rows = [dict(row) for row in con.execute(q, params).fetchall()] + con.close() + if args.limit: + rows = rows[:args.limit] + total = len(rows) + stats["total"] = total + print(f" Radku ke zpracovani: {total}") + if total == 0: + print(" Neni co synchronizovat (zadne nove radky).") + if not args.dry_run: + state_col.update_one({"_id": "watermark"}, + {"$set": {"last_db": db_name, "synced_at": now}}, upsert=True) + return stats + + # ── Indexy z Monga ──────────────────────────────────────────────────── + print(" Nacitam _id + filename + jnj_folder z Mongo...") + ids_exact = set() + ids_norm = {} + fnames = {} + has_path = set() + for d in emails.find({}, {"_id": 1, "filename": 1, "jnj_folder": 1}): + _id = d["_id"] + ids_exact.add(_id) + ids_norm.setdefault(norm_mid(_id), _id) + fn = d.get("filename") + if fn: + fnames[fn] = _id + if d.get("jnj_folder"): + has_path.add(_id) + print(f" Mongo dokumentu v {EMAILS_COL}: {len(ids_exact)} (z toho s jnj_folder: {len(has_path)})") + + # ── Plan ────────────────────────────────────────────────────────────── + m_exact = m_norm = m_fname = unmatched = 0 + examples = [] + mirror_ops = [] + emaily_ops = [] + max_wm = wm or "" + + for r in rows: + mid = r.get("message_id") + uv = r.get("updated_at") + if uv and uv > max_wm: + max_wm = uv + + # Krok A — zrcadlo (vzdy) + doc = {k: r.get(k) for k in ROW_COLS} + doc["mirrored_at"] = now + mirror_ops.append(UpdateOne({"_id": mid}, {"$set": doc}, upsert=True)) + + # Krok B — match do emaily + target = None + if mid in ids_exact: + target = mid; m_exact += 1 + elif norm_mid(mid) in ids_norm: + target = ids_norm[norm_mid(mid)]; m_norm += 1 + else: + eid = r.get("entry_id") + fn = (eid[-20:] + ".msg") if eid else None + if fn and fn in fnames: + target = fnames[fn]; m_fname += 1 + else: + unmatched += 1 + if len(examples) < 6: + examples.append(mid) + + if target is not None: + setdoc = { + "jnj_folder": coalesce_path(r.get("jnj_folder"), r.get("folder")), + "jnj_is_read": bool(r.get("is_read")), + "jnj_not_in_mailbox": bool(r.get("not_in_mailbox_anymore")), + "jnj_left_mailbox_at": r.get("left_mailbox_at"), + "jnj_folder_synced_at": now, + } + emaily_ops.append(UpdateOne({"_id": target}, {"$set": setdoc})) + + matched = m_exact + m_norm + m_fname + stats["matched"] = matched + print(" --- PLAN ---") + print(f" Zrcadlo -> {MIRROR_COL}: {len(mirror_ops)} upsert") + print(f" Emaily match exact (_id): {m_exact}") + print(f" Emaily match norm (<>): {m_norm}") + print(f" Emaily match filename: {m_fname}") + print(f" Emaily match CELKEM: {matched}/{total} ({100.0*matched/total:.1f}%)") + print(f" NEnamatchovano: {unmatched}") + if examples: + print(" Priklady nenamatchovanych message_id:") + for e in examples: + print(f" {str(e)[:72]}") + + # ── Zapis ───────────────────────────────────────────────────────────── + if args.dry_run: + print(" DRY-RUN: Mongo se NEMENI.") + return stats + + print(" Zapisuji...") + if mirror_ops: + db[MIRROR_COL].bulk_write(mirror_ops, ordered=False) + if emaily_ops: + emails.bulk_write(emaily_ops, ordered=False) + state_col.update_one( + {"_id": "watermark"}, + {"$set": {"last_updated_at": max_wm, "synced_at": now, "last_db": db_name, + "last_total": total, "last_matched": matched}}, + upsert=True, + ) + print(f" SYNC hotovo: zrcadlo={len(mirror_ops)} emaily={len(emaily_ops)} watermark={max_wm}") + return stats + + +# ══════════════════════════════════════════════════════════════════════════════ +# FAZE 3 — ENRICH (Mongo -> PG fulltext, deleguje na sdileny 5_enrich) +# [drive jnj_emails_to_fulltext_v1.0.py] +# ══════════════════════════════════════════════════════════════════════════════ + +def newest_enrich(): + """Najde nejnovejsi /scripts/5_enrich_fulltext_emails_v*.py podle verze vX.Y.""" + cands = glob.glob(ENRICH_GLOB) + if not cands: + return None + + def ver(p): + m = re.search(r"_v(\d+)\.(\d+)", os.path.basename(p)) + return (int(m.group(1)), int(m.group(2))) if m else (0, 0) + + return max(cands, key=ver) + + +def run_enrich(args, new_docs, force) -> dict: + """FAZE 3: doindexuje JNJ schranku do PG fulltextu pres sdileny enrich. + Spousti se jen kdyz parse pridal nove dokumenty (nebo force/enrich-only).""" + stats = {"ran": False, "rc": None, "skipped_reason": None} + print("\n=== FAZE 3: ENRICH (PG fulltext) ===") + + if args.no_enrich: + stats["skipped_reason"] = "--no-enrich" + print(" Preskoceno [--no-enrich].") + return stats + if args.dry_run: + enrich = newest_enrich() + stats["skipped_reason"] = "dry-run" + print(f" DRY-RUN: zavolal bych {enrich or '(enrich nenalezen!)'} --mailbox {EMAILS_COL}" + f" (nove doc z parse: {new_docs}, force={force})") + return stats + if not force and new_docs <= 0: + stats["skipped_reason"] = "zadne nove doc" + print(" Zadne nove maily z parse -> enrich preskocen " + "(JNJ stejne enrichuje pipeline v 6:00/18:00; --enrich-always vynuti).") + return stats + + enrich = newest_enrich() + if not enrich: + stats["skipped_reason"] = "enrich skript nenalezen" + print(f" CHYBA: zadny enrich skript ({ENRICH_GLOB}) -> preskakuji.") + return stats + + cmd = [sys.executable, enrich, "--mailbox", EMAILS_COL] + print(f" Spoustim: {' '.join(cmd)}") + sys.stdout.flush() + r = subprocess.run(cmd) + stats["ran"] = True + stats["rc"] = r.returncode + print(f" ENRICH hotovo: exit code {r.returncode}") + return stats + + +# ══════════════════════════════════════════════════════════════════════════════ +# MAIN +# ══════════════════════════════════════════════════════════════════════════════ + +def main(): + ap = argparse.ArgumentParser(description=f"jnj_tower_ingest v{SCRIPT_VERSION}") + ap.add_argument("--dry-run", action="store_true", help="nic nezapise, jen plan") + ap.add_argument("--full", action="store_true", + help="parse: reparsuj vse; sync: ignoruj watermark") + ap.add_argument("--limit", type=int, default=0, help="max N souboru/radku (test)") + ap.add_argument("--reindex", action="store_true", help="vynut indexy po parse") + ap.add_argument("--force", action="store_true", + help="sync: ignoruj last_db zkratku") + ap.add_argument("--parse-only", action="store_true", help="jen faze PARSE") + ap.add_argument("--sync-only", action="store_true", help="jen faze SYNC") + ap.add_argument("--enrich-only", action="store_true", help="jen faze ENRICH") + ap.add_argument("--no-enrich", action="store_true", help="preskoc fazi ENRICH") + ap.add_argument("--enrich-always", action="store_true", + help="spust enrich i bez novych dokumentu z parse") + args = ap.parse_args() + + now = datetime.now(timezone.utc).replace(tzinfo=None) + + print(f"=== jnj_tower_ingest v{SCRIPT_VERSION} {'[DRY-RUN]' if args.dry_run else ''} ===") + print(f"Start: {datetime.now():%Y-%m-%d %H:%M:%S}") + print(f"MongoDB: {MONGO_URI} -> {MONGO_DB}") + + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + try: + client.admin.command("ping") + print(" MongoDB OK") + except Exception as e: + print(f"CHYBA: MongoDB nedostupna -- {e}") + sys.exit(1) + + db = client[MONGO_DB] + col = db[EMAILS_COL] + state_col = db[STATE_COL] + + p_stats = s_stats = e_stats = None + if not args.sync_only and not args.enrich_only: + p_stats = run_parse(col, state_col, args, now) + if not args.parse_only and not args.enrich_only: + s_stats = run_sync(db, args, now) + if not args.parse_only and not args.sync_only: + new_docs = p_stats["ok"] if p_stats else 0 + force = args.enrich_only or args.enrich_always or args.full + e_stats = run_enrich(args, new_docs, force) + + # ── Souhrn ──────────────────────────────────────────────────────────── + print("\n=== SOUHRN ===") + if p_stats is not None: + print(f" PARSE: rezim={p_stats['mode']} kandidatu={p_stats['candidates']} " + f"ok={p_stats['ok']} err={p_stats['err']}") + if s_stats is not None: + if s_stats.get("skipped"): + print(" SYNC: preskoceno (zadna nova DB / uz zpracovana)") + else: + print(f" SYNC: radku={s_stats['total']} match={s_stats['matched']}") + if e_stats is not None: + if e_stats.get("ran"): + print(f" ENRICH: spusten, exit code {e_stats['rc']}") + else: + print(f" ENRICH: preskoceno ({e_stats.get('skipped_reason')})") + print(f"Konec: {datetime.now():%Y-%m-%d %H:%M:%S}") + client.close() + + +if __name__ == "__main__": + main() diff --git a/Feasibility/77242113UCO2001/fix_email_podruhe_v1.0.py b/Feasibility/77242113UCO2001/fix_email_podruhe_v1.0.py new file mode 100644 index 0000000..a899a99 --- /dev/null +++ b/Feasibility/77242113UCO2001/fix_email_podruhe_v1.0.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# ============================================================================= +# Nazev: fix_email_podruhe_v1.0.py +# Verze: 1.0 +# Datum: 2026-06-10 +# Popis: U center v KROK 1, jejichz STATUS obsahuje "Email odeslán podruhé", +# nahradi tento text za "1. připomínka odeslaná" (= 2. email byl +# fakticky 1. pripominka). Po zapisu spustit classify_krok --apply +# (centra prejdou na KROK 2). Idempotentni. +# Pouziti: python fix_email_podruhe_v1.0.py (dry-run) +# python fix_email_podruhe_v1.0.py --apply (zapise) +# ============================================================================= + +import os +import sys +from pymongo import MongoClient + +MONGO_URI = os.environ.get("MONGO_URI", "mongodb://192.168.1.76:27017") +OLD = "Email odeslán podruhé" +NEW = "1. připomínka odeslaná" + + +def main(): + apply = "--apply" in sys.argv + client = MongoClient(MONGO_URI) + col = client["feasibility"]["investigators"] + + docs = list(col.find( + {"KROK": {"$regex": "^1"}, "STATUS": {"$regex": "odeslán podruhé"}}, + {"prijmeni": 1, "jmeno": 1, "STATUS": 1}, + )) + print(f"Nalezeno {len(docs)} center v KROK 1 s '{OLD}'.\n") + + n = 0 + for d in docs: + status = d.get("STATUS", "") or "" + new_status = status.replace(OLD, NEW) + if new_status == status: + print(f"[SKIP] {d.get('prijmeni')} {d.get('jmeno')}: text nenalezen") + continue + print(f"[OK] {d.get('prijmeni')} {d.get('jmeno')}:") + print(f" '{status.splitlines()[0]}' -> '{new_status.splitlines()[0]}'") + if apply: + res = col.update_one({"_id": d["_id"]}, {"$set": {"STATUS": new_status}}) + n += res.modified_count + + print() + if apply: + print(f">>> ZAPSANO: {n} zaznamu. Ted spust classify_krok_v1.0.py --apply") + else: + print(">>> DRY-RUN. Pro zapis spust s --apply") + + +if __name__ == "__main__": + main() diff --git a/Feasibility/77242113UCO2001/templates/sipiq_email_template_v1.0.html b/Feasibility/77242113UCO2001/templates/sipiq_email_template_v1.0.html new file mode 100644 index 0000000..e40be91 --- /dev/null +++ b/Feasibility/77242113UCO2001/templates/sipiq_email_template_v1.0.html @@ -0,0 +1,39 @@ + +

Dobrý den,

+

ve společnosti Johnson & Johnson posuzujeme centra zvažovaná pro studie rané fáze vývoje. Prvním krokem je vyplnění dotazníku SIPIQ (Site Interest Protocol Information Questionnaire), díky kterému lépe porozumíme postupům, zásadám a možnostem vašeho centra.

+

Níže najdete odkaz na dotazník SIPIQ specifický pro Vaše centrum. Vyplněný dotazník prosím odešlete do {{DEADLINE}}.

+

Odkaz: {{LINK}}

+

Moc prosím vyplňte formulář pečlivě, neuvádějte ani příliš optimistická, ani příliš pesimistická čísla. Na konci dotazníku jsou dotazy na etickou komisi — tyto s přehledem ignorujte, protože situace stran etické komise je nám jasná; vše se podává v rámci centralizovaného EU podání, jehož součástí je i centrální etická komise příslušné země.

+

Naopak nás velice zajímá dotaz ke konci, jak dlouho odhadujete, že bude trvat vyjednávání smlouvy — uveďte to prosím na základě svých zkušeností z předchozích studií.

+

Po vyplnění bude následovat hodnoticí návštěva v centru a finální rozhodnutí o výběru centra.

+

V případě dotazů se na nás neváhejte obrátit.

+

S pozdravem,

+

MUDr. Vladimír BUZALKA
ICON plc
Performing Local Trial Management Services for Janssen – Cilag s.r.o.
Global Clinical Operations
Mobile: +420 775 735 276
Fax: +420 227 012 284
E-mail: vbuzalka@its.jnj.com, vladimir.buzalka@iconplc.com

diff --git a/IWRS/Reports/2026-06-10 42847922MDD3003 IWRS report.xlsx b/IWRS/Reports/2026-06-10 42847922MDD3003 IWRS report.xlsx new file mode 100644 index 0000000..e1f83de Binary files /dev/null and b/IWRS/Reports/2026-06-10 42847922MDD3003 IWRS report.xlsx differ diff --git a/IWRS/Reports/2026-06-10 77242113UCO3001 IWRS report.xlsx b/IWRS/Reports/2026-06-10 77242113UCO3001 IWRS report.xlsx new file mode 100644 index 0000000..6686a6e Binary files /dev/null and b/IWRS/Reports/2026-06-10 77242113UCO3001 IWRS report.xlsx differ diff --git a/IWRS/Trash/Drugs/Trash/create_report.py b/IWRS/Trash/Drugs/Trash/create_report.py new file mode 100644 index 0000000..09eb78b --- /dev/null +++ b/IWRS/Trash/Drugs/Trash/create_report.py @@ -0,0 +1,649 @@ +import os +import sys +import pandas as pd +from datetime import date +from pathlib import Path +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from common.mongo_writer import get_db + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +BASE_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +OUTPUT_DIR = BASE_DIR / "output" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", "Max Visit Date", + "Visit Date", "Scheduled Date", +} + +N_SHIP_COLS = 9 # počet shipment sloupců před detail sloupci + + +# ── Načítání dat z MongoDB ──────────────────────────────────────────────────── + +INVENTORY_COLS = [ + ("site", "Site"), + ("medication_id", "Med ID"), + ("packaged_lot_no", "Lot No."), + ("original_expiration_date", "Orig Exp Date"), + ("expiration_date", "Exp Date"), + ("received_date", "Rcv Date"), + ("receipt_user", "Rcpt User"), + ("subject_identifier", "Subject ID"), + ("quantity_assigned", "Qty Asgn"), + ("irt_transaction", "IRT Tx"), + ("date_assigned", "Date Asgn"), + ("assignment_user", "Asgn User"), + ("dispensation_status", "Disp Status"), + ("dispensing_date", "Disp Date"), + ("quantity_dispensed", "Qty Disp"), + ("dispensing_user", "Disp User"), + ("quantity_returned", "Qty Ret"), + ("date_returned", "Date Ret"), + ("return_user", "Ret User"), +] + + +def load_inventory(study): + db = get_db() + inv = list(db.iwrs_inventory.find({"study": study})) + destr = list(db.iwrs_destruction.find({"study": study})) + # map medication_id -> first basket+date + destr_map = {} + for d in destr: + mid = d.get("medication_id") + if mid and mid not in destr_map: + destr_map[mid] = (d.get("basket_id"), d.get("destruction_date")) + + records = [] + for doc in inv: + row = {label: doc.get(key) for key, label in INVENTORY_COLS} + b, dt = destr_map.get(doc.get("medication_id"), (None, None)) + row["Destroyed"] = dt + row["Basket No."] = b + records.append(row) + + df = pd.DataFrame(records) + if df.empty: + print(" Inventory: 0 kitu") + return df + + df = df.sort_values(["Site", "Rcv Date", "Med ID"], na_position="last").reset_index(drop=True) + for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + print(f" Inventory: {len(df)} kitu") + return df + + +SHIP_COLS = [ + ("shipment_id", "Shipment ID"), + ("status", "IRT Shipment Status"), + ("type", "Type"), + ("ship_from", "Shipment From"), + ("ship_to_site", "Ship To:"), + ("request_date", "Request Date"), + ("received_date", "Received Date"), + ("received_by", "Received by"), + ("expected_arrival", "Expected Arrival"), +] + +ITEM_COLS = [ + ("investigator", "Investigator"), + ("medication_description", "Medication Description"), + ("medication_id", "Medication ID"), + ("packaged_lot_no", "Packaged Lot number"), + ("expiration_date", "Expiration Date"), + ("item_status", "Status"), +] + + +def load_shipments(study): + db = get_db() + ships = list(db.iwrs_shipments.find({"study": study})) + items = list(db.iwrs_shipment_items.find({"study": study})) + + # index items by shipment_id + items_by_ship = {} + for it in items: + items_by_ship.setdefault(it.get("shipment_id"), []).append(it) + + records = [] + for s in ships: + base = {label: s.get(key) for key, label in SHIP_COLS} + for it in items_by_ship.get(s.get("shipment_id"), []): + row = dict(base) + for key, label in ITEM_COLS: + row[label] = it.get(key) + records.append(row) + + df = pd.DataFrame(records) + if df.empty: + print(" Shipments: 0 zásilek, 0 kitu") + return df + + df = df.sort_values(["Ship To:", "Shipment ID", "Medication ID"], na_position="last").reset_index(drop=True) + for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"): + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + n_ship = df["Shipment ID"].nunique() + print(f" Shipments: {n_ship} zásilek, {len(df)} kitu") + return df + + +def load_visits(study): + db = get_db() + cur = db.iwrs_visits.find({ + "study": study, + "visit_type": "Past", + "irt_transaction_no": {"$ne": None}, + }) + rows = [] + for v in cur: + rows.append({ + "Subject": v.get("subject"), + "Visit Date": v.get("actual_date") or v.get("scheduled_date"), + "Scheduled Date": v.get("scheduled_date"), + "IRT Tx No": v.get("irt_transaction_no"), + "Visit": v.get("irt_transaction_description"), + "Medication": v.get("medication_assignment"), + "medication_id": v.get("medication_id"), + "quantity_assigned": v.get("quantity_assigned"), + }) + df = pd.DataFrame(rows) + if df.empty: + print(" Visits: 0 radku") + return df + + # GROUP BY subject/actual/scheduled/irt_no/desc/medication + grouped = ( + df.groupby(["Subject", "Visit Date", "Scheduled Date", "IRT Tx No", "Visit", "Medication"], + dropna=False, as_index=False) + .agg(**{ + "Med IDs": ("medication_id", lambda s: ", ".join(sorted([str(x) for x in s if pd.notna(x)]))), + "Qty": ("quantity_assigned", "sum"), + }) + ) + grouped = grouped.sort_values(["Subject", "Visit Date"]).reset_index(drop=True) + for col in ("Visit Date", "Scheduled Date"): + if col in grouped.columns: + grouped[col] = pd.to_datetime(grouped[col], errors="coerce") + if study == "77242113UCO3001": + grouped["Visit"] = grouped["Visit"].replace("Subject Number Creation", "Screening") + print(f" Visits: {len(grouped)} řádků") + return grouped + + +# ── Odvozené sheety ─────────────────────────────────────────────────────────── + +def build_site_summary(shipments_df): + STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"] + pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0) + for s in STATUS_COLS: + if s not in pivot.columns: + pivot[s] = 0 + pivot = ( + pivot[STATUS_COLS] + .reset_index() + .rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"}) + .sort_values("Site") + .reset_index(drop=True) + ) + pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1) + print(f" Site Summary: {len(pivot)} center") + return pivot + + +def build_expired(df): + today = date.today() + mask = ( + df["Basket No."].isna() & + df["Subject ID"].isna() & + (df["Exp Date"] < pd.Timestamp(today)) + ) + filtered = df[mask].copy().reset_index(drop=True) + sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + print(f" Expired: {len(filtered)}") + return filtered, sheet_name + + +def build_assigned_not_dispensed(df): + mask = df["Subject ID"].notna() & df["Disp Date"].isna() + filtered = df[mask].copy().reset_index(drop=True) + print(f" Assigned not dispensed: {len(filtered)}") + return filtered + + +def build_not_returned(df): + no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].fillna("").str.upper() != "NOT DISPENSED") + ].copy() + max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") + no_ret = no_ret.join(max_asgn, on="Subject ID") + filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) + filtered = filtered.reset_index(drop=True) + print(f" Not returned: {len(filtered)}") + return filtered + + +def build_kits_for_destruction(df): + mask = ( + df["Basket No."].isna() & + (df["Date Ret"].notna() | (df["Disp Status"].fillna("").str.upper() == "NOT DISPENSED")) + ) + filtered = ( + df[mask] + .copy() + .sort_values(["Site", "Date Ret"], ascending=[True, True]) + .drop(columns=["Destroyed", "Basket No."]) + .reset_index(drop=True) + ) + print(f" Kits for destruction: {len(filtered)}") + return filtered + + +# ── Formátování ─────────────────────────────────────────────────────────────── + +STRIPE_GRAY = PatternFill("solid", start_color="F2F2F2") +STRIPE_WHITE = PatternFill("solid", start_color="FFFFFF") + +# pacienti — styly zachovány z create_subject_report.py +_PAT_HEADER_FILL = PatternFill("solid", start_color="1F4E79") +_PAT_HEADER_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10) +_PAT_NORMAL_FONT = Font(name="Arial", size=10) +_PAT_BOLD_FONT = Font(name="Arial", bold=True, size=10) +_PAT_STRIKE_FONT = Font(name="Arial", size=10, strike=True, color="999999") +_PAT_ADOLESC_FONT = Font(name="Arial", bold=True, size=10) +_PAT_THIN = Side(style="thin", color="CCCCCC") +_PAT_BORDER = Border(left=_PAT_THIN, right=_PAT_THIN, top=_PAT_THIN, bottom=_PAT_THIN) +_PAT_EVEN_FILL = PatternFill("solid", start_color="EBF3FB") +_PAT_ODD_FILL = PatternFill("solid", start_color="FFFFFF") +_PAT_CENTER = Alignment(horizontal="center", vertical="center") +_PAT_LEFT = Alignment(horizontal="left", vertical="center") + + +def _autofit(ws): + for col_cells in ws.columns: + max_len = 0 + col_letter = get_column_letter(col_cells[0].column) + for cell in col_cells: + if cell.value is None: + continue + # datum se zobrazí jako DD-MMM-YYYY = 11 znaků + if hasattr(cell.value, "strftime") or cell.number_format == "DD-MMM-YYYY": + length = 11 + else: + length = len(str(cell.value)) + if length > max_len: + max_len = length + ws.column_dimensions[col_letter].width = min(max_len + 3, 50) + + +def format_sheet(ws, header_color, highlight_col=None, highlight_color=None): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + header_fill = PatternFill("solid", start_color=header_color) + header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) + row_font = Font(name="Arial", size=10) + hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None + + headers = [cell.value for cell in ws[1]] + + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + stripe = STRIPE_GRAY if row[0].row % 2 == 0 else STRIPE_WHITE + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if hi_fill and col_name == highlight_col: + cell.fill = hi_fill + else: + cell.fill = stripe + + _autofit(ws) + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +def format_shipment_sheet(ws, header_color_ship, header_color_detail, n_ship_cols): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10) + dfont = Font(name="Arial", size=10) + fill_ship = PatternFill("solid", start_color=header_color_ship) + fill_detail = PatternFill("solid", start_color=header_color_detail) + + for cell in ws[1]: + cell.fill = fill_ship if cell.column <= n_ship_cols else fill_detail + cell.font = hfont + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = border + ws.row_dimensions[1].height = 30 + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + stripe = STRIPE_GRAY if row[0].row % 2 == 0 else STRIPE_WHITE + for cell in row: + cell.font = dfont + cell.border = border + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.fill = stripe + if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"): + cell.number_format = "DD-MMM-YYYY" + + _autofit(ws) + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +# ── Pacienti ───────────────────────────────────────────────────────────────── + +def load_patients(study): + db = get_db() + docs = list(db.iwrs_subject_summary.find({"study": study})) + if not docs: + raise RuntimeError(f"Žádná data v Mongo pro pacienty {study}") + + base_cols = [ + ("subject", "Subject"), + ("investigator", "Investigator"), + ("age", "Subject's age collection"), + ("cohort_per_irt", "Cohort per IRT"), + ("irt_subject_status", "IRT Subject Status"), + ("last_irt_transaction", "Last Recorded IRT Transaction"), + ("next_irt_transaction", "Next Expected IRT Transaction"), + ("next_irt_transaction_date_local", "Next Expected IRT Transaction Date [Local]"), + ] + uco_extra = [ + ("rescreened_subject", "Rescreened Subject"), + ("adt_ir", "ADT-IR"), + ("three_or_more_advanced_therapies", "3+ Adv. Therapies"), + ("only_oral_5asa_compounds", "Only 5-ASA"), + ("ustekinumab", "Ustekinumab"), + ("isolated_proctitis", "Isolated Proctitis"), + ] + cols = list(base_cols) + if study == "77242113UCO3001": + cols += uco_extra + + rows = [{label: d.get(key) for key, label in cols} for d in docs] + df = pd.DataFrame(rows).sort_values("Subject").reset_index(drop=True) + + if "Next Expected IRT Transaction Date [Local]" in df.columns: + df["Next Expected IRT Transaction Date [Local]"] = pd.to_datetime( + df["Next Expected IRT Transaction Date [Local]"], errors="coerce" + ) + print(f" Pacienti: {len(df)} subjektů") + return df + + +def _simplify_cohort(val): + if pd.isna(val): + return "" + val = str(val) + if "dolescent" in val: + return "Adolescent" + if val.startswith("Adult"): + return "Adult" + return val + + +def _fmt_date(val): + if pd.isna(val): + return "" + if hasattr(val, "strftime"): + return val.strftime("%Y-%m-%d") + return str(val)[:10] + + +def _write_prehled(wb, df_raw, study): + ws = wb.create_sheet("Přehled", 0) + ws.sheet_view.showGridLines = False + + is_uco = (study == "77242113UCO3001") + + if is_uco: + display_headers = ["Subject", "Investigator", "Věk", "Cohort", + "Rescreened", "ADT-IR", "≥3 Adv.Th.", "5-ASA only", + "Uste.", "Isol.Proct.", + "Status", "Last IRT", "Next Visit", "Next Date"] + col_widths = [14, 22, 6, 12, 11, 8, 11, 10, 8, 12, 14, 12, 12, 13] + status_col = 11 + flag_cols = set(range(5, 11)) # 1-indexed sloupce s Yes/No hodnotami + else: + display_headers = ["Subject", "Investigator", "Věk", "Cohort", "Status", "Last IRT", "Next Visit", "Next Date"] + col_widths = [14, 22, 6, 12, 14, 12, 12, 13] + status_col = 5 + flag_cols = set() + + last_col = get_column_letter(len(display_headers)) + ws.merge_cells(f"A1:{last_col}1") + title = ws["A1"] + title.value = f"Subject Summary — {study} ({date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + for c, (h, w) in enumerate(zip(display_headers, col_widths), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = _PAT_HEADER_FONT + cell.fill = _PAT_HEADER_FILL + cell.alignment = _PAT_CENTER + cell.border = _PAT_BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + base = { + "Subject": df_raw["Subject"].fillna(""), + "Investigator": df_raw["Investigator"].fillna(""), + "Věk": df_raw["Subject's age collection"].apply(lambda v: "" if pd.isna(v) else int(v)), + "Cohort": df_raw["Cohort per IRT"].apply(_simplify_cohort), + } + if is_uco: + base.update({ + "Rescreened": df_raw["Rescreened Subject"].fillna(""), + "ADT-IR": df_raw["ADT-IR"].fillna(""), + "≥3 Adv.Th.": df_raw["3+ Adv. Therapies"].fillna(""), + "5-ASA only": df_raw["Only 5-ASA"].fillna(""), + "Uste.": df_raw["Ustekinumab"].fillna(""), + "Isol.Proct.": df_raw["Isolated Proctitis"].fillna(""), + }) + base.update({ + "Status": df_raw["IRT Subject Status"].fillna(""), + "Last IRT": df_raw["Last Recorded IRT Transaction"].fillna("—"), + "Next Visit": df_raw["Next Expected IRT Transaction"].fillna("—"), + "Next Date": df_raw["Next Expected IRT Transaction Date [Local]"].apply(_fmt_date), + }) + display = pd.DataFrame(base).sort_values("Subject").reset_index(drop=True) + + for r_idx, row in display.iterrows(): + excel_row = r_idx + 3 + status = str(row["Status"]) + is_failed = "Screen Failed" in status or "Discontinued" in status + is_randomized = "Randomized" in status + is_adolescent = row["Cohort"] == "Adolescent" + fill = _PAT_EVEN_FILL if r_idx % 2 == 0 else _PAT_ODD_FILL + + for c_idx, val in enumerate(row, 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = _PAT_BORDER + cell.alignment = _PAT_CENTER if (c_idx == 3 or c_idx in flag_cols) else _PAT_LEFT + if is_failed: + cell.font = _PAT_STRIKE_FONT + elif c_idx == status_col and is_randomized: + cell.font = _PAT_BOLD_FONT + elif c_idx == 4 and is_adolescent: + cell.font = _PAT_ADOLESC_FONT + else: + cell.font = _PAT_NORMAL_FONT + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + ws.auto_filter.ref = f"A2:{last_col}{len(display) + 2}" + + +def _write_next_visits(wb, df_raw, study, visits_df=None): + ws = wb.create_sheet("Next Visits", 1) + ws.sheet_view.showGridLines = False + + ws.merge_cells("A1:D1") + title = ws["A1"] + title.value = f"Next Expected Visits — {study} ({date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + nv_headers = ["Subject", "Investigator", "Next Visit", "Datum"] + nv_widths = [14, 22, 26, 13] + for c, (h, w) in enumerate(zip(nv_headers, nv_widths), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = _PAT_HEADER_FONT + cell.fill = _PAT_HEADER_FILL + cell.alignment = _PAT_CENTER + cell.border = _PAT_BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + df = pd.DataFrame({ + "Subject": df_raw["Subject"].fillna(""), + "Investigator": df_raw["Investigator"].fillna(""), + "Next Visit": df_raw["Next Expected IRT Transaction"].fillna(""), + "Datum": df_raw["Next Expected IRT Transaction Date [Local]"], + "Status": df_raw["IRT Subject Status"].fillna(""), + }) + + # I-0: datum = screening date + 42 dní + if visits_df is not None and not visits_df.empty: + screen = ( + visits_df[visits_df["Visit"].str.contains("Screen", case=False, na=False)] + .groupby("Subject")["Visit Date"].min() + .rename("Screening Date") + ) + df = df.join(screen, on="Subject") + mask_i0 = df["Next Visit"].str.contains("I-0", na=False) + df.loc[mask_i0, "Datum"] = df.loc[mask_i0, "Screening Date"] + pd.Timedelta(days=42) + df = df.drop(columns=["Screening Date"]) + + df = df[df["Datum"].notna()] + df = df[~df["Status"].str.contains("Screen Failed|Discontinued", na=False)] + df = df.sort_values("Datum").reset_index(drop=True) + + for r_idx, row in df.iterrows(): + excel_row = r_idx + 3 + fill = _PAT_EVEN_FILL if r_idx % 2 == 0 else _PAT_ODD_FILL + datum_val = row["Datum"] + datum_str = datum_val.strftime("%Y-%m-%d") if hasattr(datum_val, "strftime") else str(datum_val)[:10] + for c_idx, val in enumerate([row["Subject"], row["Investigator"], row["Next Visit"], datum_str], 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = _PAT_BORDER + cell.font = _PAT_NORMAL_FONT + cell.alignment = _PAT_LEFT + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + ws.auto_filter.ref = f"A2:D{len(df) + 2}" + + +# ── Jeden report pro jednu studii ───────────────────────────────────────────── + +def create_study_report(study): + today = date.today() + + # číslování: najdi nejvyšší existující verzi pro dnešní datum + existing = sorted(OUTPUT_DIR.glob(f"{today} {study} CZ IWRS overview v*.xlsx")) + if existing: + last = existing[-1].stem # např. "2026-05-12 42847922MDD3003 CZ IWRS overview v3" + last_ver = int(last.rsplit("v", 1)[-1]) + version = last_ver + 1 + else: + version = 1 + + output_file = OUTPUT_DIR / f"{today} {study} CZ IWRS overview v{version}.xlsx" + + print(f"\n[{study}] Nacitam z MongoDB...") + df = load_inventory(study) + shipments_df = load_shipments(study) + df_patients = load_patients(study) + visits_df = load_visits(study) + + expired_df, expired_sheet = build_expired(df) + assigned_df = build_assigned_not_dispensed(df) + not_returned_df = build_not_returned(df) + destruction_df = build_kits_for_destruction(df) + site_summary_df = build_site_summary(shipments_df) + + with pd.ExcelWriter(output_file, engine="openpyxl") as writer: + df.to_excel( writer, index=False, sheet_name="CountryMedicationOverview") + expired_df.to_excel( writer, index=False, sheet_name=expired_sheet) + assigned_df.to_excel( writer, index=False, sheet_name="Assigned not dispensed") + not_returned_df.to_excel( writer, index=False, sheet_name="Not returned") + destruction_df.to_excel( writer, index=False, sheet_name="Kits for destruction") + shipments_df.to_excel( writer, index=False, sheet_name="Shipments") + site_summary_df.to_excel( writer, index=False, sheet_name="Site Summary") + visits_df.to_excel( writer, index=False, sheet_name="Patient Visits") + + wb = load_workbook(output_file) + + ws_main = wb["CountryMedicationOverview"] + format_sheet(ws_main, header_color="1F4E79") + green_fill = PatternFill("solid", start_color="E2EFDA") + headers_main = [c.value for c in ws_main[1]] + for row in ws_main.iter_rows(min_row=2, max_row=ws_main.max_row): + for cell in row: + col_name = headers_main[cell.column - 1] if cell.column <= len(headers_main) else None + if col_name in ("Destroyed", "Basket No."): + cell.fill = green_fill + + format_sheet(wb[expired_sheet], header_color="C00000", highlight_col="Exp Date", highlight_color="FFE0E0") + format_sheet(wb["Assigned not dispensed"], header_color="833C00", highlight_col="Subject ID", highlight_color="FFF2CC") + format_sheet(wb["Not returned"], header_color="375623", highlight_col="Max Visit Date", highlight_color="E2EFDA") + format_sheet(wb["Kits for destruction"], header_color="595959") + format_shipment_sheet(wb["Shipments"], "1F4E79", "375623", N_SHIP_COLS) + format_sheet(wb["Site Summary"], header_color="1F4E79") + format_sheet(wb["Patient Visits"], header_color="1F4E79") + + # ── pacienti (Přehled + Next Visits) na začátek ────────────────────────── + _write_prehled(wb, df_patients, study) + _write_next_visits(wb, df_patients, study, visits_df) + + # ── pořadí listů: Patient Visits jako první ────────────────────────────── + names = wb.sheetnames + wb._sheets = [wb["Patient Visits"]] + [wb[s] for s in names if s != "Patient Visits"] + + wb.save(output_file) + print(f" Uloženo: {output_file.name} ({len(df)} řádků)") + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + OUTPUT_DIR.mkdir(exist_ok=True) + for study in STUDIES: + try: + create_study_report(study) + except Exception as e: + import traceback + print(f"\n[{study}] CHYBA: {e}") + traceback.print_exc() + print("\nHotovo.") + + +main() diff --git a/IWRS/Trash/Drugs/Trash/import_to_mongo.py b/IWRS/Trash/Drugs/Trash/import_to_mongo.py new file mode 100644 index 0000000..18123b2 --- /dev/null +++ b/IWRS/Trash/Drugs/Trash/import_to_mongo.py @@ -0,0 +1,253 @@ +""" +Import Drugs dat (shipments, shipment_items, inventory, destruction) z XLSX do MongoDB. + +Volá se z IWRS/Drugs/run_all.py po stažení reportů. +""" + +import os +import sys +import re +import glob + +import pandas as pd + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from common.mongo_writer import ( + to_str, to_int, to_date, + ensure_indexes, log_import, + bulk_upsert_with_snapshot, bulk_upsert_only, +) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +# ── XLSX parsery (převzaté z run_all.py + úprava na Mongo dokumenty) ───────── + +def parse_shipments_report(study): + path = os.path.join(BASE_DIR, f"xls_shipments_{study}", f"shipments_report_{study}.xlsx") + if not os.path.exists(path): + print(f" CHYBI: {path}") + return [] + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Shipment ID" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + return [] + df = pd.read_excel(path, header=header_row).dropna(how="all") + df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)] + col = df.columns.tolist() + rows = [] + for _, r in df.iterrows(): + sid = to_str(r["Shipment ID"]) + if not sid: + continue + rows.append({ + "_id": sid, + "shipment_id": sid, + "study": study, + "status": to_str(r["IRT Shipment Status"]), + "type": to_str(r["Type"]), + "ship_from": to_str(r["Shipment From"]), + "ship_to_site": to_str(r["Ship To:"]), + "location": to_str(r["Location"]), + "request_date": to_date(r["Request Date"]), + "shipped_date": to_date(r["Shipped Date"]), + "received_date": to_date(r["Received Date"]) if "Received Date" in col else None, + "received_by": to_str(r["Received by"]) if "Received by" in col else None, + "delivered_date_utc": to_date(r["Delivered Date [UTC]"]) if "Delivered Date [UTC]" in col else None, + "delivery_recipient": to_str(r["Delivery Recipient"]) if "Delivery Recipient" in col else None, + "delivery_details": to_str(r["Delivery Details"]) if "Delivery Details" in col else None, + "cancelled_date": to_date(r["Cancelled Date"]) if "Cancelled Date" in col else None, + "total_medication_ids": to_int(r["Total Medication IDs"]) if "Total Medication IDs" in col else None, + "tracking_no": to_str(r["Tracking #"]) if "Tracking #" in col else None, + "shipping_category": to_str(r["Shipping Category"]) if "Shipping Category" in col else None, + "expected_arrival": to_date(r["Expected Arrival"]) if "Expected Arrival" in col else None, + }) + return rows + + +def parse_shipment_details(study): + detail_dir = os.path.join(BASE_DIR, f"xls_shipment_details_{study}") + files = sorted(glob.glob(os.path.join(detail_dir, "shipment_details_*.xlsx"))) + rows = [] + for path in files: + m = re.search(r"shipment_details_(.+)\.xlsx", os.path.basename(path)) + shipment_id = m.group(1) if m else "UNKNOWN" + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Medication ID" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + continue + df = pd.read_excel(path, header=header_row).dropna(how="all") + for _, r in df.iterrows(): + med_desc = (to_str(r.get("Medication Description")) + or to_str(r.get("Medication ID Description"))) + med_type = (to_str(r.get("Medication type")) + or to_str(r.get("Medication ID type"))) + med_id = to_str(r.get("Medication ID")) + if not med_id: + continue + rows.append({ + "_id": f"{shipment_id}:{med_id}", + "study": study, + "shipment_id": shipment_id, + "destination_location": to_str(r.get("Destination Location")), + "shipment_status": to_str(r.get("IRT Shipment Status")), + "shipment_type": to_str(r.get("Type")), + "destination_site": to_str(r.get("Destination Site")), + "investigator": to_str(r.get("Investigator")), + "medication_description": med_desc, + "medication_type": med_type, + "medication_id": med_id, + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "container_id": to_str(r.get("Container ID")), + "quantity": to_int(r.get("Quantity of Medication IDs")), + "expiration_date": to_date(r.get("Expiration Date")), + "item_status": to_str(r.get("Status")), + }) + # dedupe (poslední vyhrává) + by_id = {r["_id"]: r for r in rows} + return list(by_id.values()) + + +def parse_inventory(study): + inv_dir = os.path.join(BASE_DIR, f"xls_reports_{study}") + files = sorted(glob.glob(os.path.join(inv_dir, "onsite_inventory_detail_*.xlsx"))) + rows = [] + for path in files: + raw = pd.read_excel(path, header=None) + site = investigator = location = None + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + if first.startswith("Site:"): + site = first.replace("Site:", "").strip() + elif first.startswith("Investigator:"): + investigator = first.replace("Investigator:", "").strip() + elif first.startswith("Location:"): + location = first.replace("Location:", "").strip() + if first in ("Medication", "Medication ID") and header_row is None: + header_row = i + if header_row is None: + continue + df = pd.read_excel(path, header=header_row).dropna(how="all") + df = df.rename(columns={df.columns[0]: "medication_id"}) + for _, r in df.iterrows(): + med_id = to_str(r["medication_id"]) + if not med_id or not site: + continue + rows.append({ + "_id": f"{site}:{med_id}", + "study": study, + "site": site, + "investigator": investigator, + "location": location, + "medication_id": med_id, + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "original_expiration_date": to_date(r.get("Original Expiration Date when Packaged Lot was Added")), + "expiration_date": to_date(r.get("Expiration date")), + "received_date": to_date(r.get("Received Date")), + "receipt_user": to_str(r.get("Shipment Receipt User")), + "subject_identifier": to_str(r.get("Subject Identifier")), + "quantity_assigned": to_int(r.get("Quantity Assigned")), + "irt_transaction": to_str(r.get("IRT Transaction")), + "date_assigned": to_date(r.get("Date Assigned")), + "assignment_user": to_str(r.get("Assignment User")), + "dispensation_status": to_str(r.get("Dispensation Status")), + "dispensing_date": to_date(r.get("Dispensing date") or r.get("Dispensing Date")), + "quantity_dispensed": to_int(r.get("Quantity Dispensed")), + "dispensing_user": to_str(r.get("Dispensing User")), + "quantity_returned": to_int(r.get("Quantity Returned")), + "date_returned": to_date(r.get("Date Returned")), + "return_user": to_str(r.get("Return User")), + }) + by_id = {r["_id"]: r for r in rows} + return list(by_id.values()) + + +def parse_destruction_files(study): + dest_dir = os.path.join(BASE_DIR, f"xls_ip_destruction_{study}") + files = sorted(glob.glob(os.path.join(dest_dir, "ip_destruction_basket_*.xlsx"))) + rows = [] + for path in files: + raw = pd.read_excel(path, header=None) + meta = {} + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + for key, attr in [ + ("Investigator Name:", "investigator"), + ("Site ID:", "site_id"), + ("Location:", "location"), + ("Basket ID:", "basket_id"), + ("Drug Destruction Created Date:", "destruction_date"), + ]: + if first.startswith(key): + meta[attr] = first.replace(key, "").strip() + if first == "Medication ID Description" and header_row is None: + header_row = i + if header_row is None: + continue + df = pd.read_excel(path, header=header_row).dropna(how="all") + basket_id = meta.get("basket_id") + for _, r in df.iterrows(): + med_id = to_str(r.get("Medication ID")) + if not med_id or not basket_id: + continue + rows.append({ + "_id": f"{basket_id}:{med_id}", + "study": study, + "site_id": meta.get("site_id"), + "investigator": meta.get("investigator"), + "location": meta.get("location"), + "basket_id": basket_id, + "destruction_date": to_date(meta.get("destruction_date")), + "medication_description": to_str(r.get("Medication ID Description")), + "medication_id": med_id, + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "comments": to_str(r.get("Comments")), + }) + by_id = {r["_id"]: r for r in rows} + return list(by_id.values()) + + +# ── hlavní import ──────────────────────────────────────────────────────────── + +def import_study(study): + print(f"\n [{study}] parsovani XLSX...") + shipments = parse_shipments_report(study) + items = parse_shipment_details(study) + inventory = parse_inventory(study) + destruct = parse_destruction_files(study) + print(f" Zasilky: {len(shipments)} | Polozky: {len(items)} | Sklad: {len(inventory)} | Destrukce: {len(destruct)}") + + import_id = log_import(study, f"drugs_{study}", "drugs", { + "shipments": len(shipments), + "shipment_items": len(items), + "inventory": len(inventory), + "destruction": len(destruct), + }) + print(f" import_id = {import_id}") + + bulk_upsert_with_snapshot("iwrs_shipments", "iwrs_shipments_snapshots", shipments, import_id) + bulk_upsert_with_snapshot("iwrs_shipment_items", "iwrs_shipment_items_snapshots", items, import_id) + bulk_upsert_with_snapshot("iwrs_inventory", "iwrs_inventory_snapshots", inventory, import_id) + bulk_upsert_only("iwrs_destruction", destruct, import_id) + + +def run(studies): + ensure_indexes() + for s in studies: + import_study(s) + + +if __name__ == "__main__": + studies = sys.argv[1:] if len(sys.argv) > 1 else ["77242113UCO3001", "42847922MDD3003"] + run(studies) diff --git a/IWRS/Trash/Drugs/Trash/run_all.py b/IWRS/Trash/Drugs/Trash/run_all.py new file mode 100644 index 0000000..136d2f3 --- /dev/null +++ b/IWRS/Trash/Drugs/Trash/run_all.py @@ -0,0 +1,245 @@ +""" +Kompletní pipeline pro Drugs: + 1. Onsite inventory detail (per site, vždy přepisuje) + 2. IP destruction (per košík, přeskočí již existující soubory) + 3. Shipments report (jeden soubor na studii, přepisuje) + 4. Shipment details (per zásilka CZ, vždy přepisuje) + 5. Import do MongoDB (studie.iwrs_shipments / iwrs_shipment_items / iwrs_inventory / iwrs_destruction) + +Spusť tento skript — zpracuje obě studie automaticky. +""" + +import os +import glob +import re +import datetime + +import sys +import pandas as pd +from playwright.sync_api import sync_playwright + +import import_to_mongo as drugs_mongo + +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +SITES = { + "77242113UCO3001": [ + "DD5-CZ10001", "DD5-CZ10003", "DD5-CZ10006", "DD5-CZ10009", + "DD5-CZ10010", "DD5-CZ10012", "DD5-CZ10013", "DD5-CZ10015", + "DD5-CZ10016", "DD5-CZ10020", "DD5-CZ10021", "DD5-CZ10022", + ], + "42847922MDD3003": [ + "S10-CZ10002", "S10-CZ10004", "S10-CZ10005", + "S10-CZ10008", "S10-CZ10011", "S10-CZ10012", + ], +} + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + + +# ── login ──────────────────────────────────────────────────────────────────── + +def login(page, study): + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + +# ── download funkce ────────────────────────────────────────────────────────── + +def download_inventory(page, study): + out_dir = os.path.join(BASE_DIR, f"xls_reports_{study}") + os.makedirs(out_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/onsite_inventory_detail") + page.wait_for_load_state("networkidle", timeout=120000) + + for site_id in SITES[study]: + print(f" [{site_id}] inventory...") + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.get_by_role("option", name=site_id).click() + page.wait_for_load_state("networkidle", timeout=120000) + + filename = os.path.join(out_dir, f"onsite_inventory_detail_{site_id}.xlsx") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + print(f" Inventory OK ({len(SITES[study])} center)") + + +def download_destruction(page, study): + out_dir = os.path.join(BASE_DIR, f"xls_ip_destruction_{study}") + os.makedirs(out_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/ip_destruction_form") + page.wait_for_load_state("networkidle", timeout=120000) + + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.wait_for_timeout(1000) + baskets = [b.strip() for b in page.locator("mat-option").all_inner_texts() + if b.strip() and b.strip() != "No results found"] + page.keyboard.press("Escape") + page.wait_for_timeout(500) + + if not baskets: + print(" Žádné destruction košíky") + return + + new_count = 0 + for basket in baskets: + filename = os.path.join(out_dir, f"ip_destruction_basket_{basket}.xlsx") + if os.path.exists(filename): + continue # destrukce se nemění — přeskočit + print(f" [košík {basket}] stahování...") + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(basket) + page.wait_for_timeout(500) + page.locator("mat-option").first.dispatch_event("click") + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + new_count += 1 + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(f" Destruction OK ({new_count} nových, {len(baskets) - new_count} přeskočeno)") + + +def download_shipments_report(page, study): + out_dir = os.path.join(BASE_DIR, f"xls_shipments_{study}") + os.makedirs(out_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/shipments_report") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = os.path.join(out_dir, f"shipments_report_{study}.xlsx") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" Shipments report OK") + + +def download_shipment_details(page, study): + out_dir = os.path.join(BASE_DIR, f"xls_shipment_details_{study}") + os.makedirs(out_dir, exist_ok=True) + + # načti CZ shipment IDs z právě staženého shipments reportu + report_path = os.path.join(BASE_DIR, f"xls_shipments_{study}", f"shipments_report_{study}.xlsx") + raw = pd.read_excel(report_path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Shipment ID" in [str(v).strip() for v in row]: + header_row = i + break + df = pd.read_excel(report_path, header=header_row) + df = df.dropna(how="all") + df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)] + cz_shipments = list(zip( + df["Shipment ID"].astype(str).str.strip(), + df["IRT Shipment Status"].astype(str).str.strip() if "IRT Shipment Status" in df.columns else [""] * len(df), + )) + print(f" CZ zásilek ke stažení: {len(cz_shipments)}") + + page.goto(f"{BASE_URL}/report/shipment_details_report") + page.wait_for_load_state("networkidle", timeout=120000) + + skipped = 0 + for shipment, status in cz_shipments: + filename = os.path.join(out_dir, f"shipment_details_{shipment}.xlsx") + if os.path.exists(filename) and status.upper() == "RECEIVED": + skipped += 1 + continue # finální stav, soubor se nemění + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(shipment) + page.wait_for_timeout(500) + page.locator("mat-option").first.dispatch_event("click") + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{shipment}] ({status}) OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(f" Přeskočeno (RECEIVED): {skipped}") + + +# ── main ───────────────────────────────────────────────────────────────────── + +def main(): + os.chdir(BASE_DIR) + + # ── Stahování ──────────────────────────────────────────────────────────── + with sync_playwright() as p: + for study in STUDIES: + print(f"\n{'='*60}") + print(f"[{study}] STAHOVÁNÍ") + print(f"{'='*60}") + + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + try: + print(" Přihlášení...") + login(page, study) + + print("\n [1/4] Onsite inventory...") + download_inventory(page, study) + + print("\n [2/4] IP destruction...") + download_destruction(page, study) + + print("\n [3/4] Shipments report...") + download_shipments_report(page, study) + + print("\n [4/4] Shipment details (CZ)...") + download_shipment_details(page, study) + + except Exception as e: + import traceback + print(f" CHYBA při stahování: {e}") + traceback.print_exc() + finally: + browser.close() + + # ── Import do MongoDB ───────────────────────────────────────────────────── + print(f"\n{'='*60}") + print("IMPORT DO MongoDB") + print(f"{'='*60}") + + try: + drugs_mongo.run(STUDIES) + except Exception as e: + import traceback + print(f" CHYBA při importu: {e}") + traceback.print_exc() + + print(f"\n{'='*60}") + print("Vše hotovo.") + print(f"{'='*60}") + + +main() diff --git a/IWRS/Trash/Drugs/Working/_create_tables.py b/IWRS/Trash/Drugs/Working/_create_tables.py new file mode 100644 index 0000000..c364929 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/_create_tables.py @@ -0,0 +1,139 @@ +import mysql.connector +import db_config + +conn = mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME +) +c = conn.cursor() + +# Přidat report_type do iwrs_import (pokud ještě neexistuje) +try: + c.execute("""ALTER TABLE iwrs_import + ADD COLUMN report_type VARCHAR(20) NOT NULL DEFAULT 'patients' + AFTER source_file""") + print("ALTER TABLE iwrs_import OK — report_type přidán") +except mysql.connector.errors.DatabaseError as e: + if "Duplicate column" in str(e): + print("report_type již existuje — přeskočeno") + else: + raise + +stmts = [ + ( + "iwrs_shipments", + """CREATE TABLE IF NOT EXISTS iwrs_shipments ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + study VARCHAR(20) NOT NULL, + shipment_id VARCHAR(20) NOT NULL, + status VARCHAR(50), + type VARCHAR(30), + ship_from VARCHAR(50), + ship_to_site VARCHAR(50), + location VARCHAR(50), + request_date DATE, + shipped_date DATE, + received_date DATE, + received_by VARCHAR(100), + delivered_date_utc DATE, + delivery_recipient VARCHAR(100), + delivery_details VARCHAR(200), + cancelled_date DATE, + total_medication_ids SMALLINT, + tracking_no VARCHAR(100), + shipping_category VARCHAR(50), + expected_arrival DATE, + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_study_shipment (study, shipment_id) +)""" + ), + ( + "iwrs_shipment_items", + """CREATE TABLE IF NOT EXISTS iwrs_shipment_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + study VARCHAR(20) NOT NULL, + shipment_id VARCHAR(20) NOT NULL, + destination_location VARCHAR(50), + shipment_status VARCHAR(50), + shipment_type VARCHAR(30), + destination_site VARCHAR(50), + investigator VARCHAR(100), + medication_description VARCHAR(200), + medication_type VARCHAR(50), + medication_id VARCHAR(20), + packaged_lot_no VARCHAR(50), + packaged_lot_description VARCHAR(100), + container_id VARCHAR(50), + quantity SMALLINT, + expiration_date DATE, + item_status VARCHAR(50), + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_med_id (medication_id) +)""" + ), + ( + "iwrs_inventory", + """CREATE TABLE IF NOT EXISTS iwrs_inventory ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + study VARCHAR(20) NOT NULL, + site VARCHAR(50), + investigator VARCHAR(100), + location VARCHAR(50), + medication_id VARCHAR(20), + packaged_lot_no VARCHAR(50), + original_expiration_date DATE, + expiration_date DATE, + received_date DATE, + receipt_user VARCHAR(100), + subject_identifier VARCHAR(20), + quantity_assigned SMALLINT, + irt_transaction VARCHAR(100), + date_assigned DATE, + assignment_user VARCHAR(100), + dispensation_status VARCHAR(50), + dispensing_date DATE, + quantity_dispensed SMALLINT, + dispensing_user VARCHAR(100), + quantity_returned SMALLINT, + date_returned DATE, + return_user VARCHAR(100), + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_site (study, site) +)""" + ), + ( + "iwrs_destruction", + """CREATE TABLE IF NOT EXISTS iwrs_destruction ( + id INT AUTO_INCREMENT PRIMARY KEY, + study VARCHAR(20) NOT NULL, + site_id VARCHAR(50), + investigator VARCHAR(100), + location VARCHAR(50), + basket_id VARCHAR(20) NOT NULL, + destruction_date DATE, + medication_description VARCHAR(200), + medication_id VARCHAR(20), + packaged_lot_description VARCHAR(100), + comments VARCHAR(500), + imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_destruction (study, basket_id, medication_id), + INDEX idx_study_basket (study, basket_id) +)""" + ), +] + +for name, sql in stmts: + c.execute(sql) + print(f"OK: {name}") + +conn.commit() +c.close() +conn.close() +print("\nVšechny tabulky připraveny.") diff --git a/IWRS/Trash/Drugs/Working/create_accountability_report.py b/IWRS/Trash/Drugs/Working/create_accountability_report.py new file mode 100644 index 0000000..5bb5196 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/create_accountability_report.py @@ -0,0 +1,364 @@ +import sys +import os +import mysql.connector +import pandas as pd +from datetime import date +from pathlib import Path +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import db_config + +STUDY = "42847922MDD3003" +# STUDY = "77242113UCO3001" + +BASE_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +OUTPUT_DIR = BASE_DIR / "output" +OUTPUT_FILE = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} {STUDY} CZ IWRS overview.xlsx" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", "Max Visit Date", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, + "Max Visit Date": 16, +} + +# shipments sheet: kolík kde začínají detail sloupce (1-based, pro format_shipment_sheet) +N_SHIP_COLS = 9 + + +# ── DB ──────────────────────────────────────────────────────────────────────── + +def get_conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + + +def get_latest_import_id(cursor, study): + cursor.execute( + "SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'", + (study,), + ) + row = cursor.fetchone() + mid = row["mid"] + if mid is None: + raise RuntimeError(f"Žádná data v MySQL pro studii {study}") + return mid + + +# ── Načítání dat z MySQL ────────────────────────────────────────────────────── + +def load_inventory(cursor, study, import_id): + """ + Vrátí DataFrame s inventory + destruction join. + Sloupce jsou rovnou přejmenované pro downstream funkce. + """ + sql = """ + SELECT + i.site AS Site, + i.medication_id AS `Med ID`, + i.packaged_lot_no AS `Lot No.`, + i.original_expiration_date AS `Orig Exp Date`, + i.expiration_date AS `Exp Date`, + i.received_date AS `Rcv Date`, + i.receipt_user AS `Rcpt User`, + i.subject_identifier AS `Subject ID`, + i.quantity_assigned AS `Qty Asgn`, + i.irt_transaction AS `IRT Tx`, + i.date_assigned AS `Date Asgn`, + i.assignment_user AS `Asgn User`, + i.dispensation_status AS `Disp Status`, + i.dispensing_date AS `Disp Date`, + i.quantity_dispensed AS `Qty Disp`, + i.dispensing_user AS `Disp User`, + i.quantity_returned AS `Qty Ret`, + i.date_returned AS `Date Ret`, + i.return_user AS `Ret User`, + d.destruction_date AS Destroyed, + d.basket_id AS `Basket No.` + FROM iwrs_inventory i + LEFT JOIN ( + SELECT medication_id, + ANY_VALUE(basket_id) AS basket_id, + ANY_VALUE(destruction_date) AS destruction_date + FROM iwrs_destruction + WHERE study = %s + GROUP BY medication_id + ) d ON d.medication_id = i.medication_id + WHERE i.import_id = %s + AND i.study = %s + ORDER BY i.site, i.received_date, i.medication_id + """ + cursor.execute(sql, (study, import_id, study)) + rows = cursor.fetchall() + df = pd.DataFrame(rows) + for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + print(f" Inventory: {len(df)} kitu") + return df + + +def load_shipments(cursor, study, import_id): + """ + Vrátí DataFrame se spojenými shipments + items. + """ + sql = """ + SELECT + s.shipment_id AS `Shipment ID`, + s.status AS `IRT Shipment Status`, + s.type AS Type, + s.ship_from AS `Shipment From`, + s.ship_to_site AS `Ship To:`, + s.request_date AS `Request Date`, + s.received_date AS `Received Date`, + s.received_by AS `Received by`, + s.expected_arrival AS `Expected Arrival`, + i.investigator AS Investigator, + i.medication_description AS `Medication Description`, + i.medication_id AS `Medication ID`, + i.packaged_lot_no AS `Packaged Lot number`, + i.expiration_date AS `Expiration Date`, + i.item_status AS Status + FROM iwrs_shipments s + JOIN iwrs_shipment_items i + ON i.study = s.study + AND i.shipment_id = s.shipment_id + AND i.import_id = %s + WHERE s.import_id = %s + AND s.study = %s + ORDER BY s.ship_to_site, s.shipment_id, i.medication_id + """ + cursor.execute(sql, (import_id, import_id, study)) + rows = cursor.fetchall() + df = pd.DataFrame(rows) + for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"): + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + print(f" Shipments: {df['Shipment ID'].nunique() if len(df) else 0} zásilek, {len(df)} kitu") + return df + + +# ── Odvozené sheety ─────────────────────────────────────────────────────────── + +def build_site_summary(shipments_df): + STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"] + pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0) + for s in STATUS_COLS: + if s not in pivot.columns: + pivot[s] = 0 + pivot = ( + pivot[STATUS_COLS] + .reset_index() + .rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"}) + .sort_values("Site") + .reset_index(drop=True) + ) + pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1) + print(f" Site Summary: {len(pivot)} center") + return pivot + + +def build_expired(df): + today = date.today() + mask = ( + df["Basket No."].isna() & + df["Subject ID"].isna() & + (df["Exp Date"] < pd.Timestamp(today)) + ) + filtered = df[mask].copy().reset_index(drop=True) + sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + print(f" Expired: {len(filtered)}") + return filtered, sheet_name + + +def build_assigned_not_dispensed(df): + mask = df["Subject ID"].notna() & df["Disp Date"].isna() + filtered = df[mask].copy().reset_index(drop=True) + print(f" Assigned not dispensed: {len(filtered)}") + return filtered + + +def build_not_returned(df): + no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].fillna("").str.upper() != "NOT DISPENSED") + ].copy() + max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") + no_ret = no_ret.join(max_asgn, on="Subject ID") + filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) + filtered = filtered.reset_index(drop=True) + print(f" Not returned: {len(filtered)}") + return filtered + + +def build_kits_for_destruction(df): + mask = ( + df["Basket No."].isna() & + (df["Date Ret"].notna() | (df["Disp Status"].fillna("").str.upper() == "NOT DISPENSED")) + ) + filtered = ( + df[mask] + .copy() + .sort_values(["Site", "Date Ret"], ascending=[True, True]) + .drop(columns=["Destroyed", "Basket No."]) + .reset_index(drop=True) + ) + print(f" Kits for destruction: {len(filtered)}") + return filtered + + +# ── Formátování ─────────────────────────────────────────────────────────────── + +def format_sheet(ws, header_color, highlight_col=None, highlight_color=None): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + header_fill = PatternFill("solid", start_color=header_color) + header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) + row_font = Font(name="Arial", size=10) + hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None + + headers = [cell.value for cell in ws[1]] + + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if hi_fill and col_name == highlight_col: + cell.fill = hi_fill + + for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +def format_shipment_sheet(ws, header_color_ship, header_color_detail, n_ship_cols): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10) + dfont = Font(name="Arial", size=10) + fill_ship = PatternFill("solid", start_color=header_color_ship) + fill_detail = PatternFill("solid", start_color=header_color_detail) + + for cell in ws[1]: + cell.fill = fill_ship if cell.column <= n_ship_cols else fill_detail + cell.font = hfont + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = border + ws.column_dimensions[get_column_letter(cell.column)].width = min( + len(str(cell.value or "")) + 4, 35 + ) + ws.row_dimensions[1].height = 30 + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + cell.font = dfont + cell.border = border + cell.alignment = Alignment(horizontal="center", vertical="center") + if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"): + cell.number_format = "DD-MMM-YYYY" + + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + OUTPUT_DIR.mkdir(exist_ok=True) + + print(f"\nNačítám data z MySQL pro {STUDY}...") + conn = get_conn() + cursor = conn.cursor(dictionary=True) + import_id = get_latest_import_id(cursor, STUDY) + print(f" import_id = {import_id}") + + df = load_inventory(cursor, STUDY, import_id) + shipments_df = load_shipments(cursor, STUDY, import_id) + + cursor.close() + conn.close() + + expired_df, expired_sheet = build_expired(df) + assigned_df = build_assigned_not_dispensed(df) + not_returned_df = build_not_returned(df) + destruction_df = build_kits_for_destruction(df) + site_summary_df = build_site_summary(shipments_df) + + with pd.ExcelWriter(OUTPUT_FILE, engine="openpyxl") as writer: + df.to_excel( writer, index=False, sheet_name="CountryMedicationOverview") + expired_df.to_excel( writer, index=False, sheet_name=expired_sheet) + assigned_df.to_excel( writer, index=False, sheet_name="Assigned not dispensed") + not_returned_df.to_excel( writer, index=False, sheet_name="Not returned") + destruction_df.to_excel( writer, index=False, sheet_name="Kits for destruction") + shipments_df.to_excel( writer, index=False, sheet_name="Shipments") + site_summary_df.to_excel( writer, index=False, sheet_name="Site Summary") + + wb = load_workbook(OUTPUT_FILE) + + ws_main = wb["CountryMedicationOverview"] + format_sheet(ws_main, header_color="1F4E79") + new_col_fill = PatternFill("solid", start_color="E2EFDA") + headers_main = [c.value for c in ws_main[1]] + for row in ws_main.iter_rows(min_row=2, max_row=ws_main.max_row): + for cell in row: + col_name = headers_main[cell.column - 1] if cell.column <= len(headers_main) else None + if col_name in ("Destroyed", "Basket No."): + cell.fill = new_col_fill + + format_sheet(wb[expired_sheet], header_color="C00000", highlight_col="Exp Date", highlight_color="FFE0E0") + format_sheet(wb["Assigned not dispensed"], header_color="833C00", highlight_col="Subject ID", highlight_color="FFF2CC") + format_sheet(wb["Not returned"], header_color="375623", highlight_col="Max Visit Date", highlight_color="E2EFDA") + format_sheet(wb["Kits for destruction"], header_color="595959") + format_shipment_sheet(wb["Shipments"], "1F4E79", "375623", N_SHIP_COLS) + format_sheet(wb["Site Summary"], header_color="1F4E79") + + wb.save(OUTPUT_FILE) + print(f"\nUloženo: {OUTPUT_FILE} ({len(df)} řádků, sheety: {wb.sheetnames})") + + +if __name__ == "__main__": + main() diff --git a/IWRS/Trash/Drugs/Working/create_shipment_report.py b/IWRS/Trash/Drugs/Working/create_shipment_report.py new file mode 100644 index 0000000..3ac66c8 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/create_shipment_report.py @@ -0,0 +1,205 @@ +import sys +import os +import mysql.connector +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter +from datetime import date +import pandas as pd + +# db_config.py je v nadřazeném adresáři (Drugs/) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import db_config + +STUDY = "77242113UCO3001" +OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output") + +os.makedirs(OUTPUT_DIR, exist_ok=True) + + +def get_conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + + +def load_data(study): + conn = get_conn() + cursor = conn.cursor(dictionary=True) + + # nejnovější import_id pro danou studii + cursor.execute( + "SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'", + (study,), + ) + row = cursor.fetchone() + import_id = row["mid"] + if import_id is None: + raise RuntimeError(f"Žádná data v MySQL pro studii {study}") + print(f" import_id = {import_id}") + + sql = """ + SELECT + s.shipment_id, + s.status AS irt_shipment_status, + s.type, + s.ship_from AS shipment_from, + s.ship_to_site AS ship_to, + s.request_date, + s.received_date, + s.received_by, + s.expected_arrival, + i.investigator, + i.medication_description, + i.medication_id, + i.packaged_lot_no, + i.expiration_date, + i.item_status AS status + FROM iwrs_shipments s + JOIN iwrs_shipment_items i + ON i.study = s.study + AND i.shipment_id = s.shipment_id + AND i.import_id = %s + WHERE s.import_id = %s + AND s.study = %s + ORDER BY s.ship_to_site, s.shipment_id, i.medication_id + """ + cursor.execute(sql, (import_id, import_id, study)) + rows = cursor.fetchall() + cursor.close() + conn.close() + print(f" Načteno řádků: {len(rows)}") + return rows + + +# shipment sloupce (modrý header) / detail sloupce (zelený header) +SHIP_COLS = [ + ("shipment_id", "Shipment ID"), + ("irt_shipment_status","IRT Shipment Status"), + ("type", "Type"), + ("shipment_from", "Shipment From"), + ("ship_to", "Ship To:"), + ("request_date", "Request Date"), + ("received_date", "Received Date"), + ("received_by", "Received by"), + ("expected_arrival", "Expected Arrival"), +] + +DETAIL_COLS = [ + ("investigator", "Investigator"), + ("medication_description", "Medication Description"), + ("medication_id", "Medication ID"), + ("packaged_lot_no", "Packaged Lot number"), + ("expiration_date", "Expiration Date"), + ("status", "Status"), +] + +ALL_COLS = SHIP_COLS + DETAIL_COLS +N_SHIP_COLS = len(SHIP_COLS) + +HEADER_FILL_SHIP = PatternFill("solid", fgColor="1F4E79") +HEADER_FILL_DETAIL = PatternFill("solid", fgColor="375623") +HEADER_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10) +DATA_FONT = Font(name="Arial", size=10) +THIN_BORDER = Border( + left=Side(style="thin", color="BFBFBF"), + right=Side(style="thin", color="BFBFBF"), + bottom=Side(style="thin", color="BFBFBF"), +) + + +def write_shipments_sheet(wb, rows): + ws = wb.active + ws.title = "Shipments" + + # záhlaví + for ci, (_, label) in enumerate(ALL_COLS, 1): + cell = ws.cell(row=1, column=ci, value=label) + cell.font = HEADER_FONT + cell.fill = HEADER_FILL_SHIP if ci <= N_SHIP_COLS else HEADER_FILL_DETAIL + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = THIN_BORDER + ws.row_dimensions[1].height = 30 + + # data + for ri, row in enumerate(rows, 2): + for ci, (key, _) in enumerate(ALL_COLS, 1): + val = row[key] + cell = ws.cell(row=ri, column=ci, value=val) + cell.font = DATA_FONT + cell.border = THIN_BORDER + cell.alignment = Alignment(horizontal="center", vertical="center") + if isinstance(val, date): + cell.number_format = "DD-MMM-YYYY" + + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + # šířky sloupců + for ci, (key, label) in enumerate(ALL_COLS, 1): + vals = [label] + [str(r[key]) for r in rows if r[key] is not None] + ws.column_dimensions[get_column_letter(ci)].width = min( + max((len(v) for v in vals), default=10) + 2, 35 + ) + + +def write_summary_sheet(wb, rows): + STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"] + + df = pd.DataFrame(rows) + pivot = df.groupby("ship_to")["status"].value_counts().unstack(fill_value=0) + for s in STATUS_COLS: + if s not in pivot.columns: + pivot[s] = 0 + pivot = ( + pivot[STATUS_COLS] + .reset_index() + .rename(columns={"ship_to": "Site", "Returned by Subject": "Returned"}) + .sort_values("Site") + .reset_index(drop=True) + ) + pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1) + + ws = wb.create_sheet("Site Summary") + s_cols = ["Site", "Available", "Assigned", "Dispensed", "Returned", "Total"] + + for ci, col in enumerate(s_cols, 1): + cell = ws.cell(row=1, column=ci, value=col) + cell.font = HEADER_FONT + cell.fill = PatternFill("solid", fgColor="1F4E79") + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.border = THIN_BORDER + ws.row_dimensions[1].height = 25 + + for ri, (_, row) in enumerate(pivot.iterrows(), 2): + for ci, col in enumerate(s_cols, 1): + cell = ws.cell(row=ri, column=ci, value=row[col]) + cell.font = DATA_FONT + cell.border = THIN_BORDER + cell.alignment = Alignment(horizontal="center", vertical="center") + + for ci, col in enumerate(s_cols, 1): + vals = [col] + [str(pivot.iloc[r][col]) for r in range(len(pivot))] + ws.column_dimensions[get_column_letter(ci)].width = min( + max(len(v) for v in vals) + 4, 35 + ) + + ws.freeze_panes = "A2" + + +def build_report(): + print(f"\nNačítám data z MySQL pro {STUDY}...") + rows = load_data(STUDY) + + wb = openpyxl.Workbook() + write_shipments_sheet(wb, rows) + write_summary_sheet(wb, rows) + + outfile = os.path.join(OUTPUT_DIR, f"{date.today()} {STUDY} CZ Shipments.xlsx") + wb.save(outfile) + print(f"\nUloženo -> {outfile}") + + +build_report() diff --git a/IWRS/Trash/Drugs/Working/create_studie_report.py b/IWRS/Trash/Drugs/Working/create_studie_report.py new file mode 100644 index 0000000..dfbba66 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/create_studie_report.py @@ -0,0 +1,393 @@ +import sys +import os +import mysql.connector +import pandas as pd +from datetime import date +from pathlib import Path +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import db_config + +STUDIES = [ + ("77242113UCO3001", "UCO"), + ("42847922MDD3003", "MDD"), +] + +BASE_DIR = Path(os.path.dirname(os.path.abspath(__file__))) +OUTPUT_DIR = BASE_DIR / "output" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", "Max Visit Date", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, + "Max Visit Date": 16, +} + +N_SHIP_COLS = 9 # počet shipment sloupců (modrý header v Shipments sheetu) + + +# ── DB ──────────────────────────────────────────────────────────────────────── + +def get_conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + + +def get_latest_import_id(cursor, study): + cursor.execute( + "SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'", + (study,), + ) + row = cursor.fetchone() + mid = row["mid"] + if mid is None: + raise RuntimeError(f"Žádná data v MySQL pro studii {study}") + return mid + + +# ── Načítání dat ────────────────────────────────────────────────────────────── + +def load_inventory(cursor, study, import_id): + sql = """ + SELECT + i.site AS Site, + i.medication_id AS `Med ID`, + i.packaged_lot_no AS `Lot No.`, + i.original_expiration_date AS `Orig Exp Date`, + i.expiration_date AS `Exp Date`, + i.received_date AS `Rcv Date`, + i.receipt_user AS `Rcpt User`, + i.subject_identifier AS `Subject ID`, + i.quantity_assigned AS `Qty Asgn`, + i.irt_transaction AS `IRT Tx`, + i.date_assigned AS `Date Asgn`, + i.assignment_user AS `Asgn User`, + i.dispensation_status AS `Disp Status`, + i.dispensing_date AS `Disp Date`, + i.quantity_dispensed AS `Qty Disp`, + i.dispensing_user AS `Disp User`, + i.quantity_returned AS `Qty Ret`, + i.date_returned AS `Date Ret`, + i.return_user AS `Ret User`, + d.destruction_date AS Destroyed, + d.basket_id AS `Basket No.` + FROM iwrs_inventory i + LEFT JOIN ( + SELECT medication_id, + ANY_VALUE(basket_id) AS basket_id, + ANY_VALUE(destruction_date) AS destruction_date + FROM iwrs_destruction + WHERE study = %s + GROUP BY medication_id + ) d ON d.medication_id = i.medication_id + WHERE i.import_id = %s + AND i.study = %s + ORDER BY i.site, i.received_date, i.medication_id + """ + cursor.execute(sql, (study, import_id, study)) + rows = cursor.fetchall() + df = pd.DataFrame(rows) + for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + print(f" Inventory: {len(df)} kitu") + return df + + +def load_shipments(cursor, study, import_id): + sql = """ + SELECT + s.shipment_id AS `Shipment ID`, + s.status AS `IRT Shipment Status`, + s.type AS Type, + s.ship_from AS `Shipment From`, + s.ship_to_site AS `Ship To:`, + s.request_date AS `Request Date`, + s.received_date AS `Received Date`, + s.received_by AS `Received by`, + s.expected_arrival AS `Expected Arrival`, + i.investigator AS Investigator, + i.medication_description AS `Medication Description`, + i.medication_id AS `Medication ID`, + i.packaged_lot_no AS `Packaged Lot number`, + i.expiration_date AS `Expiration Date`, + i.item_status AS Status + FROM iwrs_shipments s + JOIN iwrs_shipment_items i + ON i.study = s.study + AND i.shipment_id = s.shipment_id + AND i.import_id = %s + WHERE s.import_id = %s + AND s.study = %s + ORDER BY s.ship_to_site, s.shipment_id, i.medication_id + """ + cursor.execute(sql, (import_id, import_id, study)) + rows = cursor.fetchall() + df = pd.DataFrame(rows) + for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"): + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + n_ship = df["Shipment ID"].nunique() if len(df) else 0 + print(f" Shipments: {n_ship} zásilek, {len(df)} kitu") + return df + + +# ── Odvozené sheety ─────────────────────────────────────────────────────────── + +def build_site_summary(shipments_df): + STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"] + pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0) + for s in STATUS_COLS: + if s not in pivot.columns: + pivot[s] = 0 + pivot = ( + pivot[STATUS_COLS] + .reset_index() + .rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"}) + .sort_values("Site") + .reset_index(drop=True) + ) + pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1) + print(f" Site Summary: {len(pivot)} center") + return pivot + + +def build_expired(df): + today = date.today() + mask = ( + df["Basket No."].isna() & + df["Subject ID"].isna() & + (df["Exp Date"] < pd.Timestamp(today)) + ) + filtered = df[mask].copy().reset_index(drop=True) + print(f" Expired: {len(filtered)}") + return filtered + + +def build_assigned_not_dispensed(df): + mask = df["Subject ID"].notna() & df["Disp Date"].isna() + filtered = df[mask].copy().reset_index(drop=True) + print(f" Assigned not dispensed: {len(filtered)}") + return filtered + + +def build_not_returned(df): + no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].fillna("").str.upper() != "NOT DISPENSED") + ].copy() + max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") + no_ret = no_ret.join(max_asgn, on="Subject ID") + filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) + filtered = filtered.reset_index(drop=True) + print(f" Not returned: {len(filtered)}") + return filtered + + +def build_kits_for_destruction(df): + mask = ( + df["Basket No."].isna() & + (df["Date Ret"].notna() | (df["Disp Status"].fillna("").str.upper() == "NOT DISPENSED")) + ) + filtered = ( + df[mask] + .copy() + .sort_values(["Site", "Date Ret"], ascending=[True, True]) + .drop(columns=["Destroyed", "Basket No."]) + .reset_index(drop=True) + ) + print(f" Kits for destruction: {len(filtered)}") + return filtered + + +# ── Formátování ─────────────────────────────────────────────────────────────── + +def format_sheet(ws, header_color, highlight_col=None, highlight_color=None): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + header_fill = PatternFill("solid", start_color=header_color) + header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) + row_font = Font(name="Arial", size=10) + hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None + + headers = [cell.value for cell in ws[1]] + + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if hi_fill and col_name == highlight_col: + cell.fill = hi_fill + + for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +def format_overview_sheet(ws): + format_sheet(ws, header_color="1F4E79") + new_col_fill = PatternFill("solid", start_color="E2EFDA") + headers = [c.value for c in ws[1]] + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + if col_name in ("Destroyed", "Basket No."): + cell.fill = new_col_fill + + +def format_shipment_sheet(ws): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10) + dfont = Font(name="Arial", size=10) + fill_ship = PatternFill("solid", start_color="1F4E79") + fill_detail = PatternFill("solid", start_color="375623") + + for cell in ws[1]: + cell.fill = fill_ship if cell.column <= N_SHIP_COLS else fill_detail + cell.font = hfont + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = border + ws.column_dimensions[get_column_letter(cell.column)].width = min( + len(str(cell.value or "")) + 4, 35 + ) + ws.row_dimensions[1].height = 30 + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + cell.font = dfont + cell.border = border + cell.alignment = Alignment(horizontal="center", vertical="center") + if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"): + cell.number_format = "DD-MMM-YYYY" + + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +# ── Main ────────────────────────────────────────────────────────────────────── + +SHEETS_DEF = [ + ("CountryMedicationOverview", "overview"), + ("Expired", "expired"), + ("Assigned not dispensed", "assigned"), + ("Not returned", "not_returned"), + ("Kits for destruction", "destruction"), + ("Shipments", "shipments"), + ("Site Summary", "site_summary"), +] + +FORMAT_MAP = { + "overview": lambda ws: format_overview_sheet(ws), + "expired": lambda ws: format_sheet(ws, "C00000", "Exp Date", "FFE0E0"), + "assigned": lambda ws: format_sheet(ws, "833C00", "Subject ID", "FFF2CC"), + "not_returned": lambda ws: format_sheet(ws, "375623", "Max Visit Date", "E2EFDA"), + "destruction": lambda ws: format_sheet(ws, "595959"), + "shipments": lambda ws: format_shipment_sheet(ws), + "site_summary": lambda ws: format_sheet(ws, "1F4E79"), +} + + +def process_study(cursor, study): + today = date.today().strftime("%d-%b-%Y") + import_id = get_latest_import_id(cursor, study) + print(f" import_id = {import_id}") + + df = load_inventory(cursor, study, import_id) + shipments_df = load_shipments(cursor, study, import_id) + + expired_df = build_expired(df) + assigned_df = build_assigned_not_dispensed(df) + not_returned_df = build_not_returned(df) + destruction_df = build_kits_for_destruction(df) + site_summ_df = build_site_summary(shipments_df) + + return [ + df, expired_df, assigned_df, not_returned_df, + destruction_df, shipments_df, site_summ_df, + ] + + +def save_study_report(study, data_frames): + output_file = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} {study} report.xlsx" + + with pd.ExcelWriter(output_file, engine="openpyxl") as writer: + for (sheet_name, _), df_sheet in zip(SHEETS_DEF, data_frames): + df_sheet.to_excel(writer, index=False, sheet_name=sheet_name) + + wb = load_workbook(output_file) + for (sheet_name, fmt_key) in SHEETS_DEF: + FORMAT_MAP[fmt_key](wb[sheet_name]) + wb.save(output_file) + print(f" Uloženo: {output_file}") + + +def main(): + OUTPUT_DIR.mkdir(exist_ok=True) + + conn = get_conn() + cursor = conn.cursor(dictionary=True) + + for study, _ in STUDIES: + print(f"\n{'='*55}") + print(f"[{study}]") + print(f"{'='*55}") + try: + data_frames = process_study(cursor, study) + save_study_report(study, data_frames) + except Exception as e: + import traceback + print(f" CHYBA: {e}") + traceback.print_exc() + + cursor.close() + conn.close() + print(f"\nHotovo.") + + +if __name__ == "__main__": + main() diff --git a/IWRS/Trash/Drugs/Working/download_ip_destruction.py b/IWRS/Trash/Drugs/Working/download_ip_destruction.py new file mode 100644 index 0000000..d139bd2 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/download_ip_destruction.py @@ -0,0 +1,76 @@ +from playwright.sync_api import sync_playwright +import os + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" + +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +# STUDY = "42847922MDD3003" +STUDY = "77242113UCO3001" + +OUTPUT_DIR = f"xls_ip_destruction_{STUDY}" +# ──────────────────────────────────────────────────────────────────────────── + +def run(page, study): + output_dir = f"xls_ip_destruction_{study}" + os.makedirs(output_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/ip_destruction_form") + page.wait_for_load_state("networkidle", timeout=120000) + + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.wait_for_timeout(1000) + baskets = [b.strip() for b in page.locator('mat-option').all_inner_texts() + if b.strip() and b.strip() != "No results found"] + print(f" Nalezeno {len(baskets)} kosiku: {baskets}") + page.keyboard.press("Escape") + page.wait_for_timeout(500) + + if not baskets: + print(" Zadne destruction kosite — preskakuji.") + return + + for basket in baskets: + filename = os.path.join(output_dir, f"ip_destruction_basket_{basket}.xlsx") + if os.path.exists(filename): + print(f" [{basket}] Preskakuji — existuje.") + continue + print(f" [{basket}] Stahuji...") + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(basket) + page.wait_for_timeout(500) + page.locator('mat-option').first.dispatch_event('click') + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{basket}] OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(" Destruction hotovo.") + + +if __name__ == "__main__": + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + run(page, STUDY) + browser.close() diff --git a/IWRS/Trash/Drugs/Working/download_reports.py b/IWRS/Trash/Drugs/Working/download_reports.py new file mode 100644 index 0000000..e67955a --- /dev/null +++ b/IWRS/Trash/Drugs/Working/download_reports.py @@ -0,0 +1,83 @@ +from playwright.sync_api import sync_playwright +import os + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" + +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +# STUDY = "42847922MDD3003" +STUDY = "77242113UCO3001" + +SITES = { + "42847922MDD3003": [ + "S10-CZ10002", + "S10-CZ10004", + "S10-CZ10005", + "S10-CZ10008", + "S10-CZ10011", + "S10-CZ10012", + ], + "77242113UCO3001": [ + "DD5-CZ10001", + "DD5-CZ10003", + "DD5-CZ10006", + "DD5-CZ10009", + "DD5-CZ10010", + "DD5-CZ10012", + "DD5-CZ10013", + "DD5-CZ10015", + "DD5-CZ10016", + "DD5-CZ10020", + "DD5-CZ10021", + "DD5-CZ10022", + ], +} + +OUTPUT_DIR = f"xls_reports_{STUDY}" +# ──────────────────────────────────────────────────────────────────────────── + +def run(page, study): + output_dir = f"xls_reports_{study}" + os.makedirs(output_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/onsite_inventory_detail") + page.wait_for_load_state("networkidle", timeout=120000) + + for site_id in SITES[study]: + print(f" [{site_id}] Stahuji...") + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.get_by_role("option", name=site_id).click() + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + + dl.value.save_as(os.path.join(output_dir, f"onsite_inventory_detail_{site_id}.xlsx")) + print(f" [{site_id}] OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(" Inventory hotovo.") + + +if __name__ == "__main__": + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + run(page, STUDY) + browser.close() diff --git a/IWRS/Trash/Drugs/Working/download_shipment_details.py b/IWRS/Trash/Drugs/Working/download_shipment_details.py new file mode 100644 index 0000000..d024bb5 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/download_shipment_details.py @@ -0,0 +1,95 @@ +from playwright.sync_api import sync_playwright +import os +import pandas as pd + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" + +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDY = "42847922MDD3003" +#STUDY = "77242113UCO3001" + +OUTPUT_DIR = f"xls_shipment_details_{STUDY}" +# ──────────────────────────────────────────────────────────────────────────── + +def get_cz_shipment_ids(study): + path = f"xls_shipments_{study}/shipments_report_{study}.xlsx" + if not os.path.exists(path): + return None + df = pd.read_excel(path, header=5) + df.columns = df.columns.str.strip() + df = df.dropna(how="all") + df["Shipment ID"] = df["Shipment ID"].astype(str).str.strip() + cz = df[df["Location"].str.contains("Czech", na=False, case=False)] + return cz["Shipment ID"].tolist() + + +def run(page, study): + output_dir = f"xls_shipment_details_{study}" + os.makedirs(output_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/shipment_details_report") + page.wait_for_load_state("networkidle", timeout=120000) + + cz_ids = get_cz_shipment_ids(study) + if cz_ids is not None: + shipments = cz_ids + print(f" Filtrovano ze shipments reportu: {len(shipments)} CZ shipmentu") + else: + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.wait_for_timeout(1000) + shipments = [s.strip() for s in page.locator('mat-option').all_inner_texts() + if s.strip() and s.strip() != "No results found"] + print(f" Nalezeno {len(shipments)} shipmentu z dropdownu") + page.keyboard.press("Escape") + page.wait_for_timeout(500) + + if not shipments: + print(" Zadne shipments — preskakuji.") + return + + for shipment in shipments: + filename = os.path.join(output_dir, f"shipment_details_{shipment}.xlsx") + if os.path.exists(filename): + print(f" [{shipment}] Preskakuji — existuje.") + continue + print(f" [{shipment}] Stahuji...") + + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(shipment) + page.wait_for_timeout(500) + page.locator('mat-option').first.dispatch_event('click') + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{shipment}] OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(" Shipment details hotovo.") + + +if __name__ == "__main__": + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + run(page, STUDY) + browser.close() diff --git a/IWRS/Trash/Drugs/Working/download_shipments_report.py b/IWRS/Trash/Drugs/Working/download_shipments_report.py new file mode 100644 index 0000000..5644e97 --- /dev/null +++ b/IWRS/Trash/Drugs/Working/download_shipments_report.py @@ -0,0 +1,47 @@ +from playwright.sync_api import sync_playwright +import os + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" + +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +# STUDY = "42847922MDD3003" +STUDY = "77242113UCO3001" + +OUTPUT_DIR = f"xls_shipments_{STUDY}" +# ──────────────────────────────────────────────────────────────────────────── + +def run(page, study): + output_dir = f"xls_shipments_{study}" + os.makedirs(output_dir, exist_ok=True) + + page.goto(f"{BASE_URL}/report/shipments_report") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = os.path.join(output_dir, f"shipments_report_{study}.xlsx") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" Shipments report OK -> {filename}") + + +if __name__ == "__main__": + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + run(page, STUDY) + browser.close() diff --git a/IWRS/Trash/Drugs/Working/import_drugs_to_mysql.py b/IWRS/Trash/Drugs/Working/import_drugs_to_mysql.py new file mode 100644 index 0000000..3ae3ccd --- /dev/null +++ b/IWRS/Trash/Drugs/Working/import_drugs_to_mysql.py @@ -0,0 +1,441 @@ +""" +Importuje drugs data z IWRS Excel reportů do MySQL. + +Tabulky: + iwrs_shipments — zásilky (jen CZ, verzováno import_id) + iwrs_shipment_items — obsah zásilek (verzováno import_id) + iwrs_inventory — lékový sklad na centrech (verzováno import_id) + iwrs_destruction — destrukce (bez verzování, přeskočí již importované košíky) + +Spustit po stažení souborů (nebo přes run_all.py). +""" + +import os +import glob +import re +import datetime + +import numpy as np +import pandas as pd +import mysql.connector + +import db_config + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +SITES = { + "77242113UCO3001": [ + "DD5-CZ10001", "DD5-CZ10003", "DD5-CZ10006", "DD5-CZ10009", + "DD5-CZ10010", "DD5-CZ10012", "DD5-CZ10013", "DD5-CZ10015", + "DD5-CZ10016", "DD5-CZ10020", "DD5-CZ10021", "DD5-CZ10022", + ], + "42847922MDD3003": [ + "S10-CZ10002", "S10-CZ10004", "S10-CZ10005", + "S10-CZ10008", "S10-CZ10011", "S10-CZ10012", + ], +} + + +# ── type converters ────────────────────────────────────────────────────────── + +def _py(val): + if isinstance(val, np.generic): + return val.item() + return val + +def to_date(val): + val = _py(val) + if val is None: + return None + if isinstance(val, float) and (val != val): + return None + try: + if pd.isna(val): + return None + except (TypeError, ValueError): + pass + if isinstance(val, pd.Timestamp): + return None if pd.isna(val) else val.date() + if isinstance(val, datetime.datetime): + return val.date() + if isinstance(val, datetime.date): + return val + s = str(val).strip() + if not s or s.lower() in ("nat", "nan", "none", ""): + return None + for fmt in ("%Y-%m-%d", "%d-%b-%Y", "%d-%m-%Y", "%Y-%m-%d %H:%M:%S"): + try: + return datetime.datetime.strptime(s, fmt).date() + except ValueError: + pass + return None + +def to_int(val): + val = _py(val) + try: + v = float(val) + return None if (v != v) else int(v) + except (TypeError, ValueError): + return None + +def to_str(val): + val = _py(val) + if val is None: + return None + if isinstance(val, float) and (val != val): + return None + s = str(val).strip() + return None if s.lower() in ("nan", "nat", "none", "") else s + + +# ── DB helpers ─────────────────────────────────────────────────────────────── + +def get_conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + +def insert_import(cursor, study, source_label): + cursor.execute( + "INSERT INTO iwrs_import (study, imported_at, source_file, report_type) VALUES (%s, %s, %s, %s)", + (study, datetime.datetime.now(), source_label, "drugs"), + ) + return cursor.lastrowid + +def basket_already_imported(cursor, study, basket_id): + cursor.execute( + "SELECT 1 FROM iwrs_destruction WHERE study=%s AND basket_id=%s LIMIT 1", + (study, str(basket_id)), + ) + return cursor.fetchone() is not None + + +# ── parsers ────────────────────────────────────────────────────────────────── + +def parse_shipments_report(study): + path = os.path.join(BASE_DIR, f"xls_shipments_{study}", f"shipments_report_{study}.xlsx") + if not os.path.exists(path): + print(f" CHYBÍ: {path}") + return [] + + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Shipment ID" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + return [] + + df = pd.read_excel(path, header=header_row) + df = df.dropna(how="all") + # pouze CZ zásilky + df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)] + col = df.columns.tolist() + + rows = [] + for _, r in df.iterrows(): + rows.append({ + "shipment_id": to_str(r["Shipment ID"]), + "status": to_str(r["IRT Shipment Status"]), + "type": to_str(r["Type"]), + "ship_from": to_str(r["Shipment From"]), + "ship_to_site": to_str(r["Ship To:"]), + "location": to_str(r["Location"]), + "request_date": to_date(r["Request Date"]), + "shipped_date": to_date(r["Shipped Date"]), + "received_date": to_date(r["Received Date"]) if "Received Date" in col else None, + "received_by": to_str(r["Received by"]) if "Received by" in col else None, + "delivered_date_utc": to_date(r["Delivered Date [UTC]"]) if "Delivered Date [UTC]" in col else None, + "delivery_recipient": to_str(r["Delivery Recipient"]) if "Delivery Recipient" in col else None, + "delivery_details": to_str(r["Delivery Details"]) if "Delivery Details" in col else None, + "cancelled_date": to_date(r["Cancelled Date"]) if "Cancelled Date" in col else None, + "total_medication_ids": to_int(r["Total Medication IDs"]) if "Total Medication IDs" in col else None, + "tracking_no": to_str(r["Tracking #"]) if "Tracking #" in col else None, + "shipping_category": to_str(r["Shipping Category"]) if "Shipping Category" in col else None, + "expected_arrival": to_date(r["Expected Arrival"]) if "Expected Arrival" in col else None, + }) + return rows + + +def parse_shipment_details(study): + detail_dir = os.path.join(BASE_DIR, f"xls_shipment_details_{study}") + files = sorted(glob.glob(os.path.join(detail_dir, "shipment_details_*.xlsx"))) + rows = [] + for path in files: + # shipment ID z názvu souboru + m = re.search(r"shipment_details_(.+)\.xlsx", os.path.basename(path)) + shipment_id = m.group(1) if m else "UNKNOWN" + + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Medication ID" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + continue + + df = pd.read_excel(path, header=header_row) + df = df.dropna(how="all") + col = df.columns.tolist() + + for _, r in df.iterrows(): + # normalizace názvů sloupců lišících se mezi studiemi + med_desc = (to_str(r.get("Medication Description")) + or to_str(r.get("Medication ID Description"))) + med_type = (to_str(r.get("Medication type")) + or to_str(r.get("Medication ID type"))) + rows.append({ + "shipment_id": shipment_id, + "destination_location": to_str(r.get("Destination Location")), + "shipment_status": to_str(r.get("IRT Shipment Status")), + "shipment_type": to_str(r.get("Type")), + "destination_site": to_str(r.get("Destination Site")), + "investigator": to_str(r.get("Investigator")), + "medication_description": med_desc, + "medication_type": med_type, + "medication_id": to_str(r.get("Medication ID")), + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "container_id": to_str(r.get("Container ID")), + "quantity": to_int(r.get("Quantity of Medication IDs")), + "expiration_date": to_date(r.get("Expiration Date")), + "item_status": to_str(r.get("Status")), + }) + return rows + + +def parse_inventory(study): + inv_dir = os.path.join(BASE_DIR, f"xls_reports_{study}") + files = sorted(glob.glob(os.path.join(inv_dir, "onsite_inventory_detail_*.xlsx"))) + rows = [] + for path in files: + raw = pd.read_excel(path, header=None) + + # extrahuj metadata ze záhlaví + site = investigator = location = None + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + if first.startswith("Site:"): + site = first.replace("Site:", "").strip() + elif first.startswith("Investigator:"): + investigator = first.replace("Investigator:", "").strip() + elif first.startswith("Location:"): + location = first.replace("Location:", "").strip() + # hlavička dat — první sloupec je "Medication" nebo "Medication ID" + if first in ("Medication", "Medication ID") and header_row is None: + header_row = i + if header_row is None: + continue + + df = pd.read_excel(path, header=header_row) + df = df.dropna(how="all") + # normalizuj první sloupec na "medication_id" + df = df.rename(columns={df.columns[0]: "medication_id"}) + col = df.columns.tolist() + + for _, r in df.iterrows(): + rows.append({ + "site": site, + "investigator": investigator, + "location": location, + "medication_id": to_str(r["medication_id"]), + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "original_expiration_date": to_date(r.get("Original Expiration Date when Packaged Lot was Added")), + "expiration_date": to_date(r.get("Expiration date")), + "received_date": to_date(r.get("Received Date")), + "receipt_user": to_str(r.get("Shipment Receipt User")), + "subject_identifier": to_str(r.get("Subject Identifier")), + "quantity_assigned": to_int(r.get("Quantity Assigned")), + "irt_transaction": to_str(r.get("IRT Transaction")), + "date_assigned": to_date(r.get("Date Assigned")), + "assignment_user": to_str(r.get("Assignment User")), + "dispensation_status": to_str(r.get("Dispensation Status")), + "dispensing_date": to_date(r.get("Dispensing date") or r.get("Dispensing Date")), + "quantity_dispensed": to_int(r.get("Quantity Dispensed")), + "dispensing_user": to_str(r.get("Dispensing User")), + "quantity_returned": to_int(r.get("Quantity Returned")), + "date_returned": to_date(r.get("Date Returned")), + "return_user": to_str(r.get("Return User")), + }) + return rows + + +def parse_destruction_files(study): + dest_dir = os.path.join(BASE_DIR, f"xls_ip_destruction_{study}") + files = sorted(glob.glob(os.path.join(dest_dir, "ip_destruction_basket_*.xlsx"))) + baskets = [] + for path in files: + raw = pd.read_excel(path, header=None) + + # metadata z záhlaví + meta = {} + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + for key, attr in [ + ("Investigator Name:", "investigator"), + ("Site ID:", "site_id"), + ("Location:", "location"), + ("Basket ID:", "basket_id"), + ("Drug Destruction Created Date:", "destruction_date"), + ]: + if first.startswith(key): + meta[attr] = first.replace(key, "").strip() + if first == "Medication ID Description" and header_row is None: + header_row = i + + if header_row is None: + continue + + df = pd.read_excel(path, header=header_row) + df = df.dropna(how="all") + + items = [] + for _, r in df.iterrows(): + items.append({ + "medication_description": to_str(r.get("Medication ID Description")), + "medication_id": to_str(r.get("Medication ID")), + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "comments": to_str(r.get("Comments")), + }) + + baskets.append({ + "site_id": meta.get("site_id"), + "investigator": meta.get("investigator"), + "location": meta.get("location"), + "basket_id": meta.get("basket_id"), + "destruction_date": to_date(meta.get("destruction_date")), + "items": items, + }) + return baskets + + +# ── inserters ──────────────────────────────────────────────────────────────── + +def insert_shipments(cursor, import_id, study, rows): + sql = """INSERT INTO iwrs_shipments + (import_id, study, shipment_id, status, type, ship_from, ship_to_site, + location, request_date, shipped_date, received_date, received_by, + delivered_date_utc, delivery_recipient, delivery_details, cancelled_date, + total_medication_ids, tracking_no, shipping_category, expected_arrival) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" + for r in rows: + cursor.execute(sql, ( + import_id, study, r["shipment_id"], r["status"], r["type"], + r["ship_from"], r["ship_to_site"], r["location"], + r["request_date"], r["shipped_date"], r["received_date"], + r["received_by"], r["delivered_date_utc"], r["delivery_recipient"], + r["delivery_details"], r["cancelled_date"], r["total_medication_ids"], + r["tracking_no"], r["shipping_category"], r["expected_arrival"], + )) + + +def insert_shipment_items(cursor, import_id, study, rows): + sql = """INSERT INTO iwrs_shipment_items + (import_id, study, shipment_id, destination_location, shipment_status, + shipment_type, destination_site, investigator, medication_description, + medication_type, medication_id, packaged_lot_no, packaged_lot_description, + container_id, quantity, expiration_date, item_status) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" + for r in rows: + cursor.execute(sql, ( + import_id, study, r["shipment_id"], r["destination_location"], + r["shipment_status"], r["shipment_type"], r["destination_site"], + r["investigator"], r["medication_description"], r["medication_type"], + r["medication_id"], r["packaged_lot_no"], r["packaged_lot_description"], + r["container_id"], r["quantity"], r["expiration_date"], r["item_status"], + )) + + +def insert_inventory(cursor, import_id, study, rows): + sql = """INSERT INTO iwrs_inventory + (import_id, study, site, investigator, location, medication_id, + packaged_lot_no, original_expiration_date, expiration_date, received_date, + receipt_user, subject_identifier, quantity_assigned, irt_transaction, + date_assigned, assignment_user, dispensation_status, dispensing_date, + quantity_dispensed, dispensing_user, quantity_returned, date_returned, return_user) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" + for r in rows: + cursor.execute(sql, ( + import_id, study, r["site"], r["investigator"], r["location"], + r["medication_id"], r["packaged_lot_no"], r["original_expiration_date"], + r["expiration_date"], r["received_date"], r["receipt_user"], + r["subject_identifier"], r["quantity_assigned"], r["irt_transaction"], + r["date_assigned"], r["assignment_user"], r["dispensation_status"], + r["dispensing_date"], r["quantity_dispensed"], r["dispensing_user"], + r["quantity_returned"], r["date_returned"], r["return_user"], + )) + + +def insert_destruction(cursor, study, baskets): + sql = """INSERT IGNORE INTO iwrs_destruction + (study, site_id, investigator, location, basket_id, destruction_date, + medication_description, medication_id, packaged_lot_description, comments) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""" + skipped = 0 + imported = 0 + for b in baskets: + if basket_already_imported(cursor, study, b["basket_id"]): + skipped += 1 + continue + for item in b["items"]: + cursor.execute(sql, ( + study, b["site_id"], b["investigator"], b["location"], + b["basket_id"], b["destruction_date"], + item["medication_description"], item["medication_id"], + item["packaged_lot_description"], item["comments"], + )) + imported += 1 + return imported, skipped + + +# ── main ───────────────────────────────────────────────────────────────────── + +def import_study(study): + print(f"\n Parsování dat pro {study}...") + shipments = parse_shipments_report(study) + items = parse_shipment_details(study) + inventory = parse_inventory(study) + baskets = parse_destruction_files(study) + + print(f" Zásilky: {len(shipments)} | Položky zásilek: {len(items)} | Sklad: {len(inventory)} | Destrukční košíky: {len(baskets)}") + + conn = get_conn() + cursor = conn.cursor() + + import_id = insert_import(cursor, study, f"drugs_{study}") + print(f" import_id = {import_id}") + + insert_shipments(cursor, import_id, study, shipments) + insert_shipment_items(cursor, import_id, study, items) + insert_inventory(cursor, import_id, study, inventory) + dest_imported, dest_skipped = insert_destruction(cursor, study, baskets) + + conn.commit() + cursor.close() + conn.close() + print(f" Destrukce: {dest_imported} nových | {dest_skipped} košíků přeskočeno (již importováno)") + + +def main(): + for study in STUDIES: + print(f"\n{'='*60}") + print(f"[{study}]") + print(f"{'='*60}") + try: + import_study(study) + print(f" OK") + except Exception as e: + import traceback + print(f" CHYBA: {e}") + traceback.print_exc() + print("\nHotovo.") + + +main() diff --git a/IWRS/Trash/Drugs/Working/output/2026-05-05 42847922MDD3003 report.xlsx b/IWRS/Trash/Drugs/Working/output/2026-05-05 42847922MDD3003 report.xlsx new file mode 100644 index 0000000..1b185f9 Binary files /dev/null and b/IWRS/Trash/Drugs/Working/output/2026-05-05 42847922MDD3003 report.xlsx differ diff --git a/IWRS/Trash/Drugs/Working/output/2026-05-05 77242113UCO3001 report.xlsx b/IWRS/Trash/Drugs/Working/output/2026-05-05 77242113UCO3001 report.xlsx new file mode 100644 index 0000000..e2d221b Binary files /dev/null and b/IWRS/Trash/Drugs/Working/output/2026-05-05 77242113UCO3001 report.xlsx differ diff --git a/IWRS/Trash/Drugs/Working/run.py b/IWRS/Trash/Drugs/Working/run.py new file mode 100644 index 0000000..945444c --- /dev/null +++ b/IWRS/Trash/Drugs/Working/run.py @@ -0,0 +1,85 @@ +import sys +import os +from playwright.sync_api import sync_playwright + +import download_reports +import download_ip_destruction +import download_shipments_report +import download_shipment_details +import create_accountability_report + +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = { + "1": "77242113UCO3001", + "2": "42847922MDD3003", +} + + +def pick_study(): + print("Vyber studii:") + for k, v in STUDIES.items(): + print(f" {k}) {v}") + while True: + choice = input("Volba (1/2): ").strip() + if choice in STUDIES: + return STUDIES[choice] + print(" Neplatna volba, zkus znovu.") + + +def login_and_select_study(page, study): + print(f"\n[1/5] Prihlaseni a vyber studie {study}...") + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + print(" OK") + + +def main(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + study = pick_study() + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + login_and_select_study(page, study) + + print(f"\n[2/5] Stahuji inventory reporty...") + download_reports.run(page, study) + + print(f"\n[3/5] Stahuji IP destruction reporty...") + download_ip_destruction.run(page, study) + + print(f"\n[4/5] Stahuji shipments report...") + download_shipments_report.run(page, study) + + print(f"\n[5/5] Stahuji shipment details...") + download_shipment_details.run(page, study) + + browser.close() + + print(f"\n[6/6] Generuji accountability report...") + create_accountability_report.STUDY = study + create_accountability_report.INVENTORY_DIR = __import__("pathlib").Path(f"xls_reports_{study}") + create_accountability_report.DESTRUCTION_DIR= __import__("pathlib").Path(f"xls_ip_destruction_{study}") + create_accountability_report.SHIPMENTS_FILE = __import__("pathlib").Path(f"xls_shipments_{study}/shipments_report_{study}.xlsx") + create_accountability_report.DETAILS_DIR = __import__("pathlib").Path(f"xls_shipment_details_{study}") + create_accountability_report.OUTPUT_FILE = create_accountability_report.OUTPUT_DIR / f"{__import__('datetime').date.today().strftime('%Y-%m-%d')} {study} CZ IWRS overview.xlsx" + create_accountability_report.main() + + print("\nVse hotovo!") + + +main() diff --git a/IWRS/Trash/Drugs/db_config.py b/IWRS/Trash/Drugs/db_config.py new file mode 100644 index 0000000..bfa5959 --- /dev/null +++ b/IWRS/Trash/Drugs/db_config.py @@ -0,0 +1,5 @@ +DB_HOST = "192.168.1.76" +DB_PORT = 3306 +DB_USER = "root" +DB_PASSWORD = "Vlado9674+" +DB_NAME = "studie" diff --git a/IWRS/Trash/Drugs/output/2026-04-21 42847922MDD3003 CZ IWRS overview.xlsx b/IWRS/Trash/Drugs/output/2026-04-21 42847922MDD3003 CZ IWRS overview.xlsx new file mode 100644 index 0000000..a9515da Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-04-21 42847922MDD3003 CZ IWRS overview.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments.xlsx b/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments.xlsx new file mode 100644 index 0000000..cc31dc5 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments_100177.xlsx b/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments_100177.xlsx new file mode 100644 index 0000000..cfd1874 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-04-21 77242113UCO3001 CZ Shipments_100177.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-04-27 77242113UCO3001 CZ IWRS overview.xlsx b/IWRS/Trash/Drugs/output/2026-04-27 77242113UCO3001 CZ IWRS overview.xlsx new file mode 100644 index 0000000..91f0461 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-04-27 77242113UCO3001 CZ IWRS overview.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..993c7da Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..92607f3 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-12 42847922MDD3003 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..ed4ecad Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..a8a616b Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-12 77242113UCO3001 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-13 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-13 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..30a7849 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-13 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-13 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-13 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..a8dd09b Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-13 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..e5dedb7 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..8d65305 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v3.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v3.xlsx new file mode 100644 index 0000000..bf8628a Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v3.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v4.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v4.xlsx new file mode 100644 index 0000000..16d0130 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v4.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v5.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v5.xlsx new file mode 100644 index 0000000..5c1a8c4 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 42847922MDD3003 CZ IWRS overview v5.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..87cebc1 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..8ac7c7d Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v3.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v3.xlsx new file mode 100644 index 0000000..2d2afa4 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v3.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v4.xlsx b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v4.xlsx new file mode 100644 index 0000000..30f62cd Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-15 77242113UCO3001 CZ IWRS overview v4.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-19 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-19 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..548968d Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-19 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-19 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-19 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..ec01f02 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-19 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-20 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-20 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..b2ceeaf Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-20 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-20 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-20 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..c257c0b Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-20 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-21 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-21 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..3d2ed0d Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-21 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-21 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-21 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..f620180 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-21 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..668d48b Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..1ef233d Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v3.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v3.xlsx new file mode 100644 index 0000000..0910f2c Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 42847922MDD3003 CZ IWRS overview v3.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..4833832 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v2.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v2.xlsx new file mode 100644 index 0000000..1430f62 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v2.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v3.xlsx b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v3.xlsx new file mode 100644 index 0000000..9b86d08 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-25 77242113UCO3001 CZ IWRS overview v3.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-26 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-26 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..c145e10 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-26 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-26 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-26 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..82ba4d0 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-26 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-27 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-27 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..ad1b0bc Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-27 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-27 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-27 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..148526a Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-27 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-31 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-31 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..751fd35 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-31 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-05-31 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-05-31 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..3f27a64 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-05-31 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-01 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-01 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..29c9241 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-01 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-01 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-01 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..50fb7e8 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-01 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-03 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-03 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..992ce3f Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-03 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-03 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-03 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..b9aed45 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-03 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-05 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-05 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..831a7c4 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-05 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-05 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-05 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..b5d15d8 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-05 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-09 42847922MDD3003 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-09 42847922MDD3003 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..450f348 Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-09 42847922MDD3003 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/output/2026-06-09 77242113UCO3001 CZ IWRS overview v1.xlsx b/IWRS/Trash/Drugs/output/2026-06-09 77242113UCO3001 CZ IWRS overview v1.xlsx new file mode 100644 index 0000000..673f5db Binary files /dev/null and b/IWRS/Trash/Drugs/output/2026-06-09 77242113UCO3001 CZ IWRS overview v1.xlsx differ diff --git a/IWRS/Trash/Drugs/preview_visits.py b/IWRS/Trash/Drugs/preview_visits.py new file mode 100644 index 0000000..979f425 --- /dev/null +++ b/IWRS/Trash/Drugs/preview_visits.py @@ -0,0 +1,52 @@ +import mysql.connector +import pandas as pd +import db_config + +conn = mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, +) +cursor = conn.cursor(dictionary=True) + +# Vezmi nejnovější import_id pro každou studii +for study in ["77242113UCO3001", "42847922MDD3003"]: + cursor.execute( + "SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='patients'", + (study,), + ) + row = cursor.fetchone() + mid = row["mid"] + print(f"\n=== {study} (import_id={mid}) ===") + + cursor.execute(""" + SELECT + v.subject, + v.actual_date, + v.scheduled_date, + v.irt_transaction_no, + v.irt_transaction_description, + v.medication_assignment, + GROUP_CONCAT(v.medication_id ORDER BY v.medication_id SEPARATOR ', ') AS medication_ids, + SUM(v.quantity_assigned) AS quantity_assigned + FROM iwrs_subject_visits v + WHERE v.import_id = %s AND v.study = %s AND v.visit_type = 'Past' + AND v.irt_transaction_no IS NOT NULL + GROUP BY v.subject, v.actual_date, v.scheduled_date, v.irt_transaction_no, + v.irt_transaction_description, v.medication_assignment + ORDER BY v.subject, v.actual_date + LIMIT 20 + """, (mid, study)) + + rows = cursor.fetchall() + df = pd.DataFrame(rows) + if df.empty: + print(" Žádná data.") + else: + pd.set_option("display.max_columns", None) + pd.set_option("display.width", 200) + pd.set_option("display.max_colwidth", 30) + print(df.to_string(index=False)) + +cursor.close() +conn.close() diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_194.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_194.xlsx new file mode 100644 index 0000000..f7c1e0a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_194.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_202.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_202.xlsx new file mode 100644 index 0000000..a890c9e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_202.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_248.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_248.xlsx new file mode 100644 index 0000000..46fad8f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_248.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_269.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_269.xlsx new file mode 100644 index 0000000..0e590b5 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_269.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_273.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_273.xlsx new file mode 100644 index 0000000..86ffc69 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_273.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_276.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_276.xlsx new file mode 100644 index 0000000..d193ae7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_276.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_286.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_286.xlsx new file mode 100644 index 0000000..6e29d3a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_286.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_289.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_289.xlsx new file mode 100644 index 0000000..ce22899 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_289.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_301.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_301.xlsx new file mode 100644 index 0000000..eb84fc5 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_301.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_313.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_313.xlsx new file mode 100644 index 0000000..a855cf2 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_313.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_326.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_326.xlsx new file mode 100644 index 0000000..3397dc6 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_326.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_343.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_343.xlsx new file mode 100644 index 0000000..400e438 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_343.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_358.xlsx b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_358.xlsx new file mode 100644 index 0000000..8642e53 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_ip_destruction_42847922MDD3003/ip_destruction_basket_358.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10002.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10002.xlsx new file mode 100644 index 0000000..970b463 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10002.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10004.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10004.xlsx new file mode 100644 index 0000000..126414f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10004.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10005.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10005.xlsx new file mode 100644 index 0000000..5580549 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10005.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10008.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10008.xlsx new file mode 100644 index 0000000..72bd572 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10008.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10011.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10011.xlsx new file mode 100644 index 0000000..627f772 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10011.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10012.xlsx b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10012.xlsx new file mode 100644 index 0000000..80279df Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_42847922MDD3003/onsite_inventory_detail_S10-CZ10012.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10001.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10001.xlsx new file mode 100644 index 0000000..3e9a340 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10001.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10003.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10003.xlsx new file mode 100644 index 0000000..5d5eee7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10003.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10006.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10006.xlsx new file mode 100644 index 0000000..7533a8b Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10006.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10009.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10009.xlsx new file mode 100644 index 0000000..5365856 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10009.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10010.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10010.xlsx new file mode 100644 index 0000000..a677935 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10010.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10012.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10012.xlsx new file mode 100644 index 0000000..866e5fc Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10012.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10013.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10013.xlsx new file mode 100644 index 0000000..41c70c8 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10013.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10015.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10015.xlsx new file mode 100644 index 0000000..f645880 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10015.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10016.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10016.xlsx new file mode 100644 index 0000000..a1b0376 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10016.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10020.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10020.xlsx new file mode 100644 index 0000000..e07c3e2 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10020.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10021.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10021.xlsx new file mode 100644 index 0000000..ee7ae1c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10021.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10022.xlsx b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10022.xlsx new file mode 100644 index 0000000..b23e88d Binary files /dev/null and b/IWRS/Trash/Drugs/xls_reports_77242113UCO3001/onsite_inventory_detail_DD5-CZ10022.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100873.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100873.xlsx new file mode 100644 index 0000000..2f7fa54 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100873.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100874.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100874.xlsx new file mode 100644 index 0000000..258c572 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100874.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100880.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100880.xlsx new file mode 100644 index 0000000..5b6ccc1 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100880.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100881.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100881.xlsx new file mode 100644 index 0000000..f02f476 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100881.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100895.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100895.xlsx new file mode 100644 index 0000000..4200081 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100895.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100905.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100905.xlsx new file mode 100644 index 0000000..ef9904d Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100905.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100946.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100946.xlsx new file mode 100644 index 0000000..72d2c9c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100946.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100971.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100971.xlsx new file mode 100644 index 0000000..0fc1dfe Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100971.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100980.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100980.xlsx new file mode 100644 index 0000000..f18a7c0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100980.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100986.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100986.xlsx new file mode 100644 index 0000000..079ad78 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100986.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100994.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100994.xlsx new file mode 100644 index 0000000..5dfdae7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_100994.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101085.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101085.xlsx new file mode 100644 index 0000000..8c81ed6 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101085.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101092.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101092.xlsx new file mode 100644 index 0000000..4b516c3 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101092.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101102.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101102.xlsx new file mode 100644 index 0000000..2efc8ec Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101102.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101110.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101110.xlsx new file mode 100644 index 0000000..a534d23 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101110.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101117.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101117.xlsx new file mode 100644 index 0000000..2fd361e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101117.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101118.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101118.xlsx new file mode 100644 index 0000000..4e2e32a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101118.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101119.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101119.xlsx new file mode 100644 index 0000000..08d8da7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101119.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101139.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101139.xlsx new file mode 100644 index 0000000..33bd95c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101139.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101246.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101246.xlsx new file mode 100644 index 0000000..b6818ac Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101246.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101270.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101270.xlsx new file mode 100644 index 0000000..fa69ca4 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101270.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101274.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101274.xlsx new file mode 100644 index 0000000..96769ef Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101274.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101293.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101293.xlsx new file mode 100644 index 0000000..bd3d5c9 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101293.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101300.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101300.xlsx new file mode 100644 index 0000000..b2b8248 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101300.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101322.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101322.xlsx new file mode 100644 index 0000000..e8823a8 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101322.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101327.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101327.xlsx new file mode 100644 index 0000000..0925291 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101327.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101357.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101357.xlsx new file mode 100644 index 0000000..4884854 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101357.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101378.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101378.xlsx new file mode 100644 index 0000000..94534aa Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101378.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101385.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101385.xlsx new file mode 100644 index 0000000..b16cc01 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101385.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101418.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101418.xlsx new file mode 100644 index 0000000..f67b7f5 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101418.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101444.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101444.xlsx new file mode 100644 index 0000000..8be9ff9 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101444.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101487.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101487.xlsx new file mode 100644 index 0000000..6de6ab8 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101487.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101508.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101508.xlsx new file mode 100644 index 0000000..3218104 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101508.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101524.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101524.xlsx new file mode 100644 index 0000000..90184b7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101524.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101530.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101530.xlsx new file mode 100644 index 0000000..12f82a0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101530.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101531.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101531.xlsx new file mode 100644 index 0000000..6bda309 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101531.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101555.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101555.xlsx new file mode 100644 index 0000000..35372ba Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101555.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101589.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101589.xlsx new file mode 100644 index 0000000..6949101 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101589.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101662.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101662.xlsx new file mode 100644 index 0000000..b56e198 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101662.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101688.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101688.xlsx new file mode 100644 index 0000000..bc8adc3 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101688.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101700.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101700.xlsx new file mode 100644 index 0000000..c7755da Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101700.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101720.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101720.xlsx new file mode 100644 index 0000000..3162abb Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101720.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101732.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101732.xlsx new file mode 100644 index 0000000..f659594 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101732.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101738.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101738.xlsx new file mode 100644 index 0000000..a3a2165 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101738.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101750.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101750.xlsx new file mode 100644 index 0000000..40a86e9 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101750.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101751.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101751.xlsx new file mode 100644 index 0000000..c968f07 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101751.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101784.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101784.xlsx new file mode 100644 index 0000000..0d0521a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101784.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101785.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101785.xlsx new file mode 100644 index 0000000..9eb04d1 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101785.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101827.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101827.xlsx new file mode 100644 index 0000000..a173fe5 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101827.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101858.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101858.xlsx new file mode 100644 index 0000000..61d9755 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101858.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101910.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101910.xlsx new file mode 100644 index 0000000..b1fbaf4 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101910.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101919.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101919.xlsx new file mode 100644 index 0000000..b09acd7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101919.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101925.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101925.xlsx new file mode 100644 index 0000000..063ec16 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101925.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101962.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101962.xlsx new file mode 100644 index 0000000..75ea307 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101962.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101963.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101963.xlsx new file mode 100644 index 0000000..eafb3a0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101963.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101964.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101964.xlsx new file mode 100644 index 0000000..b9982d8 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101964.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101965.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101965.xlsx new file mode 100644 index 0000000..81ad3ae Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101965.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101966.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101966.xlsx new file mode 100644 index 0000000..d59ba91 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101966.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101967.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101967.xlsx new file mode 100644 index 0000000..ac045f4 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_101967.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102071.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102071.xlsx new file mode 100644 index 0000000..46a38d2 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102071.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102075.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102075.xlsx new file mode 100644 index 0000000..394516a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102075.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102094.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102094.xlsx new file mode 100644 index 0000000..c5cb372 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102094.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102108.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102108.xlsx new file mode 100644 index 0000000..6ad52c0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102108.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102136.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102136.xlsx new file mode 100644 index 0000000..41d1c5f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102136.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102137.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102137.xlsx new file mode 100644 index 0000000..db40fe0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102137.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102160.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102160.xlsx new file mode 100644 index 0000000..992a07f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102160.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102193.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102193.xlsx new file mode 100644 index 0000000..93e239a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102193.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102199.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102199.xlsx new file mode 100644 index 0000000..d233738 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102199.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102247.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102247.xlsx new file mode 100644 index 0000000..3a8f04f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102247.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102256.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102256.xlsx new file mode 100644 index 0000000..a1e7bd6 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102256.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102275.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102275.xlsx new file mode 100644 index 0000000..ead0f33 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102275.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102295.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102295.xlsx new file mode 100644 index 0000000..baa5a4e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102295.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102322.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102322.xlsx new file mode 100644 index 0000000..f2a1d6b Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102322.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102341.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102341.xlsx new file mode 100644 index 0000000..71a421f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102341.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102403.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102403.xlsx new file mode 100644 index 0000000..5c7bddf Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102403.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102418.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102418.xlsx new file mode 100644 index 0000000..ac31e27 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102418.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102439.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102439.xlsx new file mode 100644 index 0000000..dd6627e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102439.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102455.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102455.xlsx new file mode 100644 index 0000000..db4a44b Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102455.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102497.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102497.xlsx new file mode 100644 index 0000000..08b64b4 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102497.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102538.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102538.xlsx new file mode 100644 index 0000000..61eb222 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102538.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102550.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102550.xlsx new file mode 100644 index 0000000..219c81c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102550.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102596.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102596.xlsx new file mode 100644 index 0000000..67de467 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102596.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102602.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102602.xlsx new file mode 100644 index 0000000..72dd112 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102602.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102640.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102640.xlsx new file mode 100644 index 0000000..81e9679 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102640.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102641.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102641.xlsx new file mode 100644 index 0000000..2206827 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102641.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102758.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102758.xlsx new file mode 100644 index 0000000..df43764 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102758.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102784.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102784.xlsx new file mode 100644 index 0000000..265d30b Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102784.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102814.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102814.xlsx new file mode 100644 index 0000000..d53eaac Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102814.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102839.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102839.xlsx new file mode 100644 index 0000000..914560d Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102839.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102840.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102840.xlsx new file mode 100644 index 0000000..77b74a7 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_42847922MDD3003/shipment_details_102840.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100177.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100177.xlsx new file mode 100644 index 0000000..8d40975 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100177.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100222.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100222.xlsx new file mode 100644 index 0000000..e5d0347 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100222.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100354.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100354.xlsx new file mode 100644 index 0000000..f943d52 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100354.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100382.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100382.xlsx new file mode 100644 index 0000000..eaf5976 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100382.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100411.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100411.xlsx new file mode 100644 index 0000000..bb5fa9e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100411.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100421.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100421.xlsx new file mode 100644 index 0000000..6dc3f08 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100421.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100498.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100498.xlsx new file mode 100644 index 0000000..e16b7ce Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100498.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100510.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100510.xlsx new file mode 100644 index 0000000..36e1676 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100510.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100587.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100587.xlsx new file mode 100644 index 0000000..8b4b60e Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100587.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100593.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100593.xlsx new file mode 100644 index 0000000..b4d740c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100593.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100616.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100616.xlsx new file mode 100644 index 0000000..557406f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100616.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100678.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100678.xlsx new file mode 100644 index 0000000..d121f04 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100678.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100717.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100717.xlsx new file mode 100644 index 0000000..97f5958 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100717.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100728.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100728.xlsx new file mode 100644 index 0000000..1100be4 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100728.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100733.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100733.xlsx new file mode 100644 index 0000000..972ff9a Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100733.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100740.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100740.xlsx new file mode 100644 index 0000000..bd3c538 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100740.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100776.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100776.xlsx new file mode 100644 index 0000000..1436168 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100776.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100843.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100843.xlsx new file mode 100644 index 0000000..3841e30 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100843.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100845.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100845.xlsx new file mode 100644 index 0000000..7dd92ad Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100845.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100956.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100956.xlsx new file mode 100644 index 0000000..2a4700c Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100956.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100974.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100974.xlsx new file mode 100644 index 0000000..1d141e0 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100974.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100991.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100991.xlsx new file mode 100644 index 0000000..68a67b5 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_100991.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101039.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101039.xlsx new file mode 100644 index 0000000..0dadc82 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101039.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101060.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101060.xlsx new file mode 100644 index 0000000..32ec8c9 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101060.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101061.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101061.xlsx new file mode 100644 index 0000000..7fb9f95 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101061.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101087.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101087.xlsx new file mode 100644 index 0000000..422a853 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101087.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101104.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101104.xlsx new file mode 100644 index 0000000..3486dfa Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101104.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101132.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101132.xlsx new file mode 100644 index 0000000..30cfcfc Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101132.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101135.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101135.xlsx new file mode 100644 index 0000000..d6847eb Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101135.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101185.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101185.xlsx new file mode 100644 index 0000000..6443df2 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101185.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101204.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101204.xlsx new file mode 100644 index 0000000..9548536 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101204.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101210.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101210.xlsx new file mode 100644 index 0000000..e64a94f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101210.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101245.xlsx b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101245.xlsx new file mode 100644 index 0000000..632aa3f Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipment_details_77242113UCO3001/shipment_details_101245.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipments_42847922MDD3003/shipments_report_42847922MDD3003.xlsx b/IWRS/Trash/Drugs/xls_shipments_42847922MDD3003/shipments_report_42847922MDD3003.xlsx new file mode 100644 index 0000000..a2193d2 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipments_42847922MDD3003/shipments_report_42847922MDD3003.xlsx differ diff --git a/IWRS/Trash/Drugs/xls_shipments_77242113UCO3001/shipments_report_77242113UCO3001.xlsx b/IWRS/Trash/Drugs/xls_shipments_77242113UCO3001/shipments_report_77242113UCO3001.xlsx new file mode 100644 index 0000000..5a34722 Binary files /dev/null and b/IWRS/Trash/Drugs/xls_shipments_77242113UCO3001/shipments_report_77242113UCO3001.xlsx differ diff --git a/IWRS/Trash/Patients/CONTEXT.md b/IWRS/Trash/Patients/CONTEXT.md new file mode 100644 index 0000000..8e0a7d5 --- /dev/null +++ b/IWRS/Trash/Patients/CONTEXT.md @@ -0,0 +1,56 @@ +# Kontext práce — IWRS Notifications Pipeline +## Datum: 2026-06-01 + +## Co bylo uděláno + +### Nové soubory +- `download_subject_notifications.py` — standalone skript pro stažení notifikací (referenční, nepoužívaný v pipeline) +- `test_notifications.py` — testovací skript pro jednoho pacienta (CZ100222003 / UCO3001) +- `create_iwrs_tables.py` — jednorázový skript pro vytvoření MySQL tabulek + +### Upravené soubory +- `download_subject_details.py` — přidáno stahování notifikací (PDF + JSON) pro každý subjekt přímo v loopě +- `import_to_mysql.py` — přidána funkce `import_notifications()` která importuje JSON+PDF do DB a přesouvá do `Zpracováno/` +- `create_iwrs_tables.sql` — přidána tabulka `iwrs_notifications` +- `run_all.py` — krok 2 nyní volá `dsd.run()` z `download_subject_details.py` + +## Jak to funguje + +### Stahování notifikací (v `download_subject_details.py`) +1. Při výběru subjektu se zachytí `table_1` API response (obsahuje notifikace s `pk`, `et_title`, `label`, `body`, `actual_date_raw`) +2. Porovná `pk` s DB (`iwrs_notifications`) — stahuje jen nové +3. Stáhne PDF přes `page.request.get()` s Bearer tokenem (JWT se načítá čerstvě před každým requestem) +4. Uloží PDF + JSON do `IncomingSourceReportsDetails/{study}/` +5. Název souboru: `{actual_date_raw}_{label_s_podtržítky}.pdf` (při kolizi přidá `_pk{pk}`) + +### API endpointy +- **Notifikace data**: `POST /_/p/{instance_id}/api/v1/reports_api/report_data?path=patient_detail_report&id={subject}&key=table_1&unblinded=false` +- **PDF download**: `GET /_/p/{instance_id}/api/v1/meta_api/pdfnotification?pk={pk}&title={et_title}&html=true` +- **app_instances** (pro zjištění instance_id): `GET /_/api/dispatch/app_instances/` +- Headers: `Authorization: Bearer {JWT}`, `lang: en`, `prancer_study: {study_code}` + +### Instance ID mapping +- `77242113UCO3001` → `/_/p/106` +- `42847922MDD3003` → `/_/p/70` +- `77242113CRD3001` → `/_/p/103` + +### Import (`import_to_mysql.py`) +- Čte všechny `.json` soubory z `IncomingSourceReportsDetails/{study}/` +- Načte příslušné `.pdf` jako binární data +- Uloží do tabulky `iwrs_notifications` (UNIQUE KEY na `pk` — bez duplikátů) +- Přesune soubory do `IncomingSourceReportsDetails/{study}/Zpracováno/` + +## MySQL tabulka `iwrs_notifications` +```sql +id, study, subject, pk (UNIQUE), title, label, event, actual_date, text (TEXT), pdf (MEDIUMBLOB), source_file, imported_at +``` + +## Aktuální stav +- UCO3001: ~76 notifikací importováno +- MDD3003: ~119 notifikací importováno (část 403 chyb — JWT expiroval, opraveno načítáním JWT čerstvě) +- MDD3003 notifikace s 403 čekají na příští `run_all.py` (soubory nejsou v `Zpracováno`, takže se znovu stáhnou) + +## Co zbývá / možná vylepšení +- Ověřit že MDD3003 403 chyby jsou opraveny (JWT refresh) +- `CZ100132003` UCO3001 — timeout při stahování XLS (subjekt přeskočen, zkusit znovu) +- Případně přidat retry logiku pro timeout diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary 1306.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary 1306.xlsx new file mode 100644 index 0000000..8e81c20 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary 1306.xlsx differ diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary.xlsx new file mode 100644 index 0000000..6971da4 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-04 42847922MDD3003 Subject Summary.xlsx differ diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary 1306.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary 1306.xlsx new file mode 100644 index 0000000..31d24d0 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary 1306.xlsx differ diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary.xlsx new file mode 100644 index 0000000..b315716 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-04 77242113UCO3001 Subject Summary.xlsx differ diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-12 42847922MDD3003 Subject Summary.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-12 42847922MDD3003 Subject Summary.xlsx new file mode 100644 index 0000000..d0c6c06 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-12 42847922MDD3003 Subject Summary.xlsx differ diff --git a/IWRS/Trash/Patients/CreatedReports/2026-05-12 77242113UCO3001 Subject Summary.xlsx b/IWRS/Trash/Patients/CreatedReports/2026-05-12 77242113UCO3001 Subject Summary.xlsx new file mode 100644 index 0000000..430c0e3 Binary files /dev/null and b/IWRS/Trash/Patients/CreatedReports/2026-05-12 77242113UCO3001 Subject Summary.xlsx differ diff --git a/IWRS/Trash/Patients/Trash/create_iwrs_tables.py b/IWRS/Trash/Patients/Trash/create_iwrs_tables.py new file mode 100644 index 0000000..1f80b74 --- /dev/null +++ b/IWRS/Trash/Patients/Trash/create_iwrs_tables.py @@ -0,0 +1,39 @@ +""" +Jednorázový skript — vytvoří/aktualizuje tabulky v MySQL. +Spusť jednou: python create_iwrs_tables.py +""" +import os +import mysql.connector +import db_config + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +SQL_FILE = os.path.join(BASE_DIR, "create_iwrs_tables.sql") + +conn = mysql.connector.connect( + host=db_config.DB_HOST, + port=db_config.DB_PORT, + user=db_config.DB_USER, + password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, +) +cursor = conn.cursor() + +sql = open(SQL_FILE, encoding="utf-8").read() +# Odstraň komentáře a rozdělíme na příkazy +stmts = [s.strip() for s in sql.split(";")] +for stmt in stmts: + # Odstraň řádkové komentáře + lines = [l for l in stmt.splitlines() if not l.strip().startswith("--")] + stmt = "\n".join(lines).strip() + if not stmt or stmt.upper().startswith("USE"): + continue + try: + cursor.execute(stmt) + print(f"OK: {stmt[:80]}") + except Exception as e: + print(f"SKIP: {e}") + +conn.commit() +cursor.close() +conn.close() +print("\nHotovo.") diff --git a/IWRS/Trash/Patients/Trash/create_iwrs_tables.sql b/IWRS/Trash/Patients/Trash/create_iwrs_tables.sql new file mode 100644 index 0000000..b022bc7 --- /dev/null +++ b/IWRS/Trash/Patients/Trash/create_iwrs_tables.sql @@ -0,0 +1,128 @@ +-- IWRS tabulky pro databázi studie +-- Spustit jednou: mysql -h 192.168.1.76 -u root -p studie < create_iwrs_tables.sql + +USE studie; + +-- ── Import log ─────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS iwrs_import ( + import_id INT AUTO_INCREMENT PRIMARY KEY, + study VARCHAR(20) NOT NULL, + imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + source_file VARCHAR(500) NOT NULL, + INDEX idx_study (study) +); + +-- ── UCO3001 subject summary ─────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS iwrs_uco3001_subject_summary ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + subject VARCHAR(20) NOT NULL, + prior_subject_identifier VARCHAR(20), + site VARCHAR(50), + investigator VARCHAR(100), + location VARCHAR(50), + cohort_per_irt VARCHAR(100), + informed_consent_date DATE, + adolescent_assent_date DATE, + age SMALLINT, + weight DECIMAL(5,1), + rescreened_subject VARCHAR(10), + adt_ir VARCHAR(10), + three_or_more_advanced_therapies VARCHAR(10), + only_oral_5asa_compounds VARCHAR(10), + ustekinumab VARCHAR(10), + isolated_proctitis VARCHAR(10), + clinical_responder_status_i12_m0 VARCHAR(100), + irt_subject_status VARCHAR(50), + i0_rand_date_local DATE, + last_irt_transaction VARCHAR(100), + last_irt_transaction_date_local DATE, + last_irt_transaction_date_utc DATE, + next_irt_transaction VARCHAR(100), + next_irt_transaction_date_local DATE, + most_recent_med_assignment_date DATE, + days_since_last_med_assignment SMALLINT, + patient_forecast_status VARCHAR(50), + patient_forecast_status_changed_date DATE, + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_subject (subject) +); + +-- ── MDD3003 subject summary ─────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS iwrs_mdd3003_subject_summary ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + subject VARCHAR(20) NOT NULL, + prior_subject_identifier VARCHAR(20), + site VARCHAR(50), + investigator VARCHAR(100), + location VARCHAR(50), + cohort_per_irt VARCHAR(50), + madrs_criteria_integrated VARCHAR(50), + informed_consent_date DATE, + age SMALLINT, + madrs_criteria_v15 VARCHAR(10), + madrs_criteria_v16 VARCHAR(10), + madrs_criteria_v17 VARCHAR(10), + stratification_country VARCHAR(10), + age_group VARCHAR(20), + stable_remitters VARCHAR(50), + irt_subject_status VARCHAR(100), + last_irt_transaction VARCHAR(100), + last_irt_transaction_date_local DATE, + last_irt_transaction_date_utc DATE, + next_irt_transaction VARCHAR(100), + next_irt_transaction_date_local DATE, + date_screened DATE, + date_screen_failed DATE, + date_randomized_part1 DATE, + date_early_withdraw_randomized_part1 DATE, + date_open_label_induction DATE, + date_early_withdraw_open_label_induction DATE, + date_randomized_part2 DATE, + date_early_withdraw_randomized_part2 DATE, + date_completed DATE, + date_unblinded DATE, + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_subject (subject) +); + +-- ── Notifications ──────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS iwrs_notifications ( + id INT AUTO_INCREMENT PRIMARY KEY, + study VARCHAR(20) NOT NULL, + subject VARCHAR(20) NOT NULL, + pk INT NOT NULL, + title VARCHAR(100), + label VARCHAR(500), + event VARCHAR(50), + actual_date DATE, + text TEXT, + pdf MEDIUMBLOB, + source_file VARCHAR(500), + imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_pk (pk), + INDEX idx_study_subject (study, subject) +); + +-- ── Subject visits / transactions (obě studie) ─────────────────────────────── +CREATE TABLE IF NOT EXISTS iwrs_subject_visits ( + id INT AUTO_INCREMENT PRIMARY KEY, + import_id INT NOT NULL, + study VARCHAR(20) NOT NULL, + subject VARCHAR(20) NOT NULL, + visit_type ENUM('Past','Upcoming') NOT NULL, + scheduled_date DATE, + window_days VARCHAR(20), + actual_date DATE, + irt_transaction_no SMALLINT, + irt_transaction_description VARCHAR(200), + medication_assignment VARCHAR(200), + quantity_assigned SMALLINT, + medication_id VARCHAR(20), + FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id), + INDEX idx_import (import_id), + INDEX idx_study_subject (study, subject) +); diff --git a/IWRS/Trash/Patients/Trash/create_subject_report.py b/IWRS/Trash/Patients/Trash/create_subject_report.py new file mode 100644 index 0000000..2b5af71 --- /dev/null +++ b/IWRS/Trash/Patients/Trash/create_subject_report.py @@ -0,0 +1,310 @@ +import os +import glob +import datetime +import pandas as pd +from openpyxl import Workbook +from openpyxl.styles import ( + Font, PatternFill, Alignment, Border, Side, GradientFill +) +from openpyxl.utils import get_column_letter + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "IncomingSourceReports") +CREATED_DIR = os.path.join(BASE_DIR, "CreatedReports") + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +SOURCE_COLS = [ + "Subject", + "Investigator", + "Subject's age collection", + "Cohort per IRT", + "IRT Subject Status", + "Last Recorded IRT Transaction", + "Next Expected IRT Transaction", + "Next Expected IRT Transaction Date [Local]", +] + +DISPLAY_HEADERS = [ + "Subject", + "Investigator", + "Věk", + "Cohort", + "Status", + "Last IRT", + "Next Visit", + "Next Date", +] + +COL_WIDTHS = [14, 22, 6, 12, 14, 12, 12, 13] + +# ── Styles ─────────────────────────────────────────────────────────────────── +HEADER_FILL = PatternFill("solid", fgColor="1F4E79") +HEADER_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10) +NORMAL_FONT = Font(name="Arial", size=10) +BOLD_FONT = Font(name="Arial", bold=True, size=10) +STRIKE_FONT = Font(name="Arial", size=10, strike=True, color="999999") +ADOLESC_FONT = Font(name="Arial", bold=True, size=10) + +THIN = Side(style="thin", color="CCCCCC") +BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) + +EVEN_FILL = PatternFill("solid", fgColor="EBF3FB") +ODD_FILL = PatternFill("solid", fgColor="FFFFFF") + +CENTER = Alignment(horizontal="center", vertical="center", wrap_text=False) +LEFT = Alignment(horizontal="left", vertical="center", wrap_text=False) + + +def unique_path(directory, stem): + path = os.path.join(directory, f"{stem}.xlsx") + if not os.path.exists(path): + return path + time_tag = datetime.datetime.now().strftime("%H%M") + return os.path.join(directory, f"{stem} {time_tag}.xlsx") + + +def find_latest_source(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} Subject Summary Report.xlsx") + files = sorted( + [f for f in glob.glob(pattern) if not os.path.basename(f).startswith("~$")], + key=os.path.getmtime, + reverse=True, + ) + if not files: + raise FileNotFoundError(f"Nenalezen zdrojový soubor pro {study} v {INCOMING_DIR}") + return files[0] + + +def load_source(path): + raw = pd.read_excel(path, header=None) + # find header row (row with "Subject" in first cell) + header_row = None + for i, row in raw.iterrows(): + if "Subject" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + raise ValueError("Hlavičkový řádek nenalezen") + df = pd.read_excel(path, header=header_row) + return df + + +def simplify_cohort(val): + if pd.isna(val): + return "" + val = str(val) + if "dolescent" in val: + return "Adolescent" + if val.startswith("Adult"): + return "Adult" + # MDD3003: "Part 1", "Part 2" — keep as-is + return val + + +def format_date(val): + if pd.isna(val): + return "" + if hasattr(val, "strftime"): + return val.strftime("%Y-%m-%d") + return str(val)[:10] + + +def write_zdroj(wb, df_raw, source_path): + mtime = datetime.datetime.fromtimestamp(os.path.getmtime(source_path)) + sheet_name = f"ZDROJ ({mtime.strftime('%d%b%Y').upper()})" + ws = wb.create_sheet(sheet_name) + ws.sheet_view.showGridLines = True + + # write raw headers + data as plain table + headers = list(df_raw.columns) + for c, h in enumerate(headers, 1): + cell = ws.cell(row=1, column=c, value=h) + cell.font = Font(name="Arial", bold=True, size=9, color="FFFFFF") + cell.fill = PatternFill("solid", fgColor="404040") + cell.alignment = LEFT + cell.border = BORDER + ws.column_dimensions[get_column_letter(c)].width = 20 + + for r, (_, row) in enumerate(df_raw.iterrows(), 2): + fill = EVEN_FILL if r % 2 == 0 else ODD_FILL + for c, col in enumerate(headers, 1): + val = row[col] + if pd.isna(val): + val = "" + elif hasattr(val, "strftime"): + val = val.strftime("%Y-%m-%d") + cell = ws.cell(row=r, column=c, value=val) + cell.font = Font(name="Arial", size=9) + cell.fill = fill + cell.border = BORDER + cell.alignment = LEFT + + ws.freeze_panes = "A2" + ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1" + + +def write_prehled(wb, df_raw, study): + ws = wb.create_sheet("Přehled") + ws.sheet_view.showGridLines = False + ws.sheet_view.showRowColHeaders = True + + # ── title row ──────────────────────────────────────────────────────────── + ws.merge_cells("A1:H1") + title = ws["A1"] + title.value = f"Subject Summary — {study} ({datetime.date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + # ── header row ─────────────────────────────────────────────────────────── + for c, (h, w) in enumerate(zip(DISPLAY_HEADERS, COL_WIDTHS), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = HEADER_FONT + cell.fill = HEADER_FILL + cell.alignment = CENTER + cell.border = BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + # ── build display dataframe ─────────────────────────────────────────────── + display = pd.DataFrame() + display["Subject"] = df_raw["Subject"].fillna("") + display["Investigator"]= df_raw["Investigator"].fillna("") + display["Věk"] = df_raw["Subject's age collection"].apply( + lambda v: "" if pd.isna(v) else int(v)) + display["Cohort"] = df_raw["Cohort per IRT"].apply(simplify_cohort) + display["Status"] = df_raw["IRT Subject Status"].fillna("") + display["Last IRT"] = df_raw["Last Recorded IRT Transaction"].fillna("—") + display["Next Visit"] = df_raw["Next Expected IRT Transaction"].fillna("—") + display["Next Date"] = df_raw["Next Expected IRT Transaction Date [Local]"].apply(format_date) + + display = display.sort_values("Subject").reset_index(drop=True) + + # ── data rows ──────────────────────────────────────────────────────────── + for r_idx, row in display.iterrows(): + excel_row = r_idx + 3 # row 1=title, row 2=header + status = str(row["Status"]) + is_failed = "Screen Failed" in status or "Discontinued" in status + is_randomized = "Randomized" in status + is_adolescent = row["Cohort"] == "Adolescent" + fill = EVEN_FILL if r_idx % 2 == 0 else ODD_FILL + + values = [ + row["Subject"], row["Investigator"], row["Věk"], + row["Cohort"], row["Status"], row["Last IRT"], + row["Next Visit"], row["Next Date"], + ] + + for c_idx, val in enumerate(values, 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = BORDER + + # alignment + cell.alignment = CENTER if c_idx in (3,) else LEFT + + # font logic + if is_failed: + cell.font = STRIKE_FONT + elif c_idx == 5 and is_randomized: + cell.font = BOLD_FONT + elif c_idx == 4 and is_adolescent: + cell.font = ADOLESC_FONT + else: + cell.font = NORMAL_FONT + + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + last_data_row = len(display) + 2 + ws.auto_filter.ref = f"A2:H{last_data_row}" + + +def write_next_visits(wb, df_raw, study): + ws = wb.create_sheet("Next Visits") + ws.sheet_view.showGridLines = False + + # title + ws.merge_cells("A1:D1") + title = ws["A1"] + title.value = f"Next Expected Visits — {study} ({datetime.date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + # headers + nv_headers = ["Subject", "Investigator", "Next Visit", "Datum"] + nv_widths = [14, 22, 26, 13] + for c, (h, w) in enumerate(zip(nv_headers, nv_widths), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = HEADER_FONT + cell.fill = HEADER_FILL + cell.alignment = CENTER + cell.border = BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + # data — only rows with a Next Date, exclude Screen Failed / Discontinued + df = pd.DataFrame() + df["Subject"] = df_raw["Subject"].fillna("") + df["Investigator"]= df_raw["Investigator"].fillna("") + df["Next Visit"] = df_raw["Next Expected IRT Transaction"].fillna("") + df["Datum"] = df_raw["Next Expected IRT Transaction Date [Local]"] + df["Status"] = df_raw["IRT Subject Status"].fillna("") + + df = df[df["Datum"].notna()] + df = df[~df["Status"].str.contains("Screen Failed|Discontinued", na=False)] + df = df.sort_values("Datum").reset_index(drop=True) + + for r_idx, row in df.iterrows(): + excel_row = r_idx + 3 + fill = EVEN_FILL if r_idx % 2 == 0 else ODD_FILL + datum_val = row["Datum"] + datum_str = datum_val.strftime("%Y-%m-%d") if hasattr(datum_val, "strftime") else str(datum_val)[:10] + + values = [row["Subject"], row["Investigator"], row["Next Visit"], datum_str] + for c_idx, val in enumerate(values, 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = BORDER + cell.font = NORMAL_FONT + cell.alignment = LEFT + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + last_data_row = len(df) + 2 + ws.auto_filter.ref = f"A2:D{last_data_row}" + + +def create_report(study): + source_path = find_latest_source(study) + print(f"[{study}] Čtu: {os.path.basename(source_path)}") + + df_raw = load_source(source_path) + + wb = Workbook() + wb.remove(wb.active) # remove default sheet + + write_prehled(wb, df_raw, study) + write_next_visits(wb, df_raw, study) + write_zdroj(wb, df_raw, source_path) + + today = datetime.date.today().strftime("%Y-%m-%d") + out_path = unique_path(CREATED_DIR, f"{today} {study} Subject Summary") + wb.save(out_path) + print(f"[{study}] Uloženo: {out_path}") + return out_path + + +def main(): + os.makedirs(CREATED_DIR, exist_ok=True) + for study in STUDIES: + try: + create_report(study) + except FileNotFoundError as e: + print(f"[{study}] PŘESKOČENO: {e}") + print("\nHotovo.") + + +main() diff --git a/IWRS/Trash/Patients/Trash/download_all.py b/IWRS/Trash/Patients/Trash/download_all.py new file mode 100644 index 0000000..72376af --- /dev/null +++ b/IWRS/Trash/Patients/Trash/download_all.py @@ -0,0 +1,90 @@ +""" +Stažení reportů z IWRS portálu — vše do jednoho adresáře `Incoming/`. + + 1. Subject Summary Report (per studie) + 2. Subject Detail Reports + notifikace (per subjekt) + +Import se spouští samostatně skriptem `import_all.py`. +""" + +import os +import datetime + +from playwright.sync_api import sync_playwright + +import download_subject_details as dsd + +# ── CONFIG ─────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "Incoming") + + +def unique_path(directory, stem, ext=".xlsx"): + path = os.path.join(directory, f"{stem}{ext}") + if not os.path.exists(path): + return path + time_tag = datetime.datetime.now().strftime("%H%M") + return os.path.join(directory, f"{stem} {time_tag}{ext}") + + +def login(page, study): + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + +def download_summary(page, study, today): + print(f" [{study}] Stahuji Subject Summary Report...") + page.goto(f"{BASE_URL}/report/patient_summary_report") + page.wait_for_load_state("networkidle", timeout=120000) + filename = unique_path(INCOMING_DIR, f"{today} {study} Subject Summary Report") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{study}] Summary OK -> {os.path.basename(filename)}") + return filename + + +def main(): + today = datetime.date.today().strftime("%Y-%m-%d") + os.makedirs(INCOMING_DIR, exist_ok=True) + + with sync_playwright() as p: + for study in STUDIES: + print("\n" + "=" * 60) + print(f"[{study}] Stažení reportů") + print("=" * 60) + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + try: + login(page, study) + download_summary(page, study, today) + # detail XLSX + notifikace přímo do Incoming/ + dsd.run(page, study, out_dir=INCOMING_DIR, subjects_source_dir=INCOMING_DIR) + except Exception as e: + print(f" [{study}] CHYBA: {e}") + finally: + browser.close() + + print("\n" + "=" * 60) + print(f"Stahování hotovo. Soubory v: {INCOMING_DIR}") + print("Pro import spusť: python import_all.py") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/IWRS/Trash/Patients/Trash/download_subject_notifications.py b/IWRS/Trash/Patients/Trash/download_subject_notifications.py new file mode 100644 index 0000000..029a3e1 --- /dev/null +++ b/IWRS/Trash/Patients/Trash/download_subject_notifications.py @@ -0,0 +1,201 @@ +from playwright.sync_api import sync_playwright +import os +import glob +import datetime +import requests + +import pandas as pd + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "IncomingSourceReports") +DETAILS_DIR = os.path.join(BASE_DIR, "IncomingSourceReportsDetails") +# ──────────────────────────────────────────────────────────────────────────── + + +def get_subjects(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} Subject Summary Report.xlsx") + files = sorted( + [f for f in glob.glob(pattern) if not os.path.basename(f).startswith("~$")], + key=os.path.getmtime, + reverse=True, + ) + if not files: + raise FileNotFoundError(f"Nenalezen Subject Summary Report pro {study}") + today = datetime.date.today().strftime("%Y-%m-%d") + if not os.path.basename(files[0]).startswith(today): + raise FileNotFoundError( + f"Dnešní Subject Summary Report pro {study} neexistuje — spusť nejdříve download_subject_summary.py" + ) + path = files[0] + print(f" Čtu subjekty z: {os.path.basename(path)}") + + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Subject" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + raise ValueError("Hlavičkový řádek nenalezen") + + df = pd.read_excel(path, header=header_row) + subjects = df["Subject"].dropna().astype(str).str.strip().tolist() + return subjects + + +def get_jwt_and_api_base(page, study): + """Získá JWT token a api_base_url pro danou studii.""" + jwt = page.evaluate("localStorage.getItem('JWT.access')") + if not jwt: + raise ValueError("JWT token nenalezen v localStorage") + + instances = page.evaluate("""async (jwt) => { + const res = await fetch('/_/api/dispatch/app_instances/', { + headers: { 'Authorization': `Bearer ${jwt}` } + }); + return res.json(); + }""", jwt) + + instance = next( + (i for i in instances if study in i.get("label", "")), + None + ) + if not instance: + raise ValueError(f"app_instance pro studii {study} nenalezena") + + return jwt, instance["api_base_url"] + + +def get_notifications(jwt, api_base, study, subject): + """Načte seznam notifikací pro daného subjekta přes report_data API.""" + url = f"{BASE_URL}{api_base}/api/v1/reports_api/report_data" + params = { + "path": "patient_detail_report", + "id": subject, + "key": "table_1", + "unblinded": "false", + } + payload = { + "path": "patient_detail_report", + "study": study, + "id": subject, + "key": "table_1", + "fields": {}, + "filters": [{"tableId": "table_1", "tableFilters": {}}], + "pagination_details": {"order": "type", "reverseOrder": False, "page": 1, "limit": 500}, + "cache_key": f"py_{subject}_{datetime.datetime.now().timestamp()}", + } + headers = { + "Authorization": f"Bearer {jwt}", + "Content-Type": "application/json", + "lang": "en", + } + resp = requests.post(url, params=params, json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + + notifications = [] + for row in data.get("data", []): + for notif in row.get("notification", []): + item = notif.get("item", {}) + pk = item.get("pk") + title = item.get("et_title") + if pk and title: + notifications.append({"pk": pk, "title": title, "event": row.get("event_event_id", "")}) + return notifications + + +def download_pdf(jwt, api_base, pk, title, out_path): + """Stáhne PDF notifikaci a uloží ji.""" + url = f"{BASE_URL}{api_base}/api/v1/meta_api/pdfnotification" + params = {"pk": pk, "title": title, "html": "true"} + headers = { + "Authorization": f"Bearer {jwt}", + "lang": "en", + "Accept": "*/*", + } + resp = requests.get(url, params=params, headers=headers) + resp.raise_for_status() + with open(out_path, "wb") as f: + f.write(resp.content) + + +def run(page, study): + out_dir = os.path.join(DETAILS_DIR, study) + os.makedirs(out_dir, exist_ok=True) + + subjects = get_subjects(study) + print(f" Nalezeno {len(subjects)} subjektů") + today = datetime.date.today().strftime("%Y-%m-%d") + + # Načteme stránku aby byl platný session kontext + page.goto(f"{BASE_URL}/report/patient_detail_report") + page.wait_for_load_state("networkidle", timeout=120000) + + jwt, api_base = get_jwt_and_api_base(page, study) + print(f" API base: {api_base}") + + for subject in subjects: + print(f" [{subject}] Stahuji notifikace...") + try: + notifications = get_notifications(jwt, api_base, study, subject) + if not notifications: + print(f" [{subject}] Žádné notifikace") + continue + + for notif in notifications: + pk = notif["pk"] + title = notif["title"] + filename = os.path.join(out_dir, f"{today} {study} {subject} Notification {title} pk{pk}.pdf") + if os.path.exists(filename): + print(f" [{subject}] {title} (pk={pk}) — již existuje, přeskakuji") + continue + download_pdf(jwt, api_base, pk, title, filename) + print(f" [{subject}] {title} (pk={pk}) OK") + + except Exception as e: + print(f" [{subject}] CHYBA při notifikacích: {e}") + + print(f" [{study}] Notifikace hotovo.") + + +def main(): + os.makedirs(DETAILS_DIR, exist_ok=True) + + with sync_playwright() as p: + for study in STUDIES: + print(f"\n[{study}] Přihlášení...") + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + try: + run(page, study) + except Exception as e: + print(f" [{study}] CHYBA: {e}") + + browser.close() + + print("\nVše hotovo.") + + +main() diff --git a/IWRS/Trash/Patients/Trash/download_subject_summary.py b/IWRS/Trash/Patients/Trash/download_subject_summary.py new file mode 100644 index 0000000..517f2bc --- /dev/null +++ b/IWRS/Trash/Patients/Trash/download_subject_summary.py @@ -0,0 +1,76 @@ +from playwright.sync_api import sync_playwright +import os +import datetime + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "IncomingSourceReports") +CREATED_DIR = os.path.join(BASE_DIR, "CreatedReports") +# ──────────────────────────────────────────────────────────────────────────── + + +def unique_path(directory, stem): + path = os.path.join(directory, f"{stem}.xlsx") + if not os.path.exists(path): + return path + time_tag = datetime.datetime.now().strftime("%H%M") + return os.path.join(directory, f"{stem} {time_tag}.xlsx") + + +def download_study(page, study, today): + print(f"\n[{study}] Prihlaseni...") + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + + print(f"[{study}] Vyber studie...") + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + print(f"[{study}] Stahuji Subject Summary Report...") + page.goto(f"{BASE_URL}/report/patient_summary_report") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = unique_path(INCOMING_DIR, f"{today} {study} Subject Summary Report") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f"[{study}] OK -> {filename}") + return filename + + +def main(): + today = datetime.date.today().strftime("%Y-%m-%d") + os.makedirs(INCOMING_DIR, exist_ok=True) + os.makedirs(CREATED_DIR, exist_ok=True) + + downloaded = [] + + with sync_playwright() as p: + for study in STUDIES: + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + filename = download_study(page, study, today) + downloaded.append((study, filename)) + + browser.close() + + print("\nVse stazeno:") + for study, path in downloaded: + print(f" {study}: {path}") + + +main() diff --git a/IWRS/Trash/Patients/Trash/import_all.py b/IWRS/Trash/Patients/Trash/import_all.py new file mode 100644 index 0000000..fd9ed8f --- /dev/null +++ b/IWRS/Trash/Patients/Trash/import_all.py @@ -0,0 +1,107 @@ +""" +Import všech čekajících reportů z `Incoming/` do MongoDB. + +Pořadí zpracování per typ + studie: nejstarší soubor podle mtime první +(důležité pro chronologickou správnost snapshotů). + +Po úspěšném importu se soubor přesune do `Incoming/Zpracováno/`. +Při chybě zůstane soubor v `Incoming/`. +""" + +import os +import sys +import glob +import shutil + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from common.mongo_writer import ensure_indexes + +import import_to_mongo +import import_notifications_to_mongo + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "Incoming") +DONE_DIR = os.path.join(INCOMING_DIR, "Zpracováno") + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + + +def _move_done(path): + os.makedirs(DONE_DIR, exist_ok=True) + dst = os.path.join(DONE_DIR, os.path.basename(path)) + # kolize → přepiš (Mongo už má aktuální data, soubor je jen archiv) + if os.path.exists(dst): + os.remove(dst) + shutil.move(path, dst) + + +def _sorted_by_mtime(paths): + """Nejstarší první.""" + return sorted( + (p for p in paths if not os.path.basename(p).startswith("~$")), + key=os.path.getmtime, + ) + + +def import_summaries(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} Subject Summary Report*.xlsx") + files = _sorted_by_mtime(glob.glob(pattern)) + if not files: + print(f" [{study}] summary: nic ke zpracování") + return + print(f" [{study}] summary: {len(files)} soubor(ů) (oldest first)") + for path in files: + try: + import_to_mongo.import_subject_summary(study, path) + _move_done(path) + except Exception as e: + print(f" [{study}] CHYBA summary {os.path.basename(path)}: {e}") + + +def import_details(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} * Subject Detail.xlsx") + files = _sorted_by_mtime(glob.glob(pattern)) + if not files: + print(f" [{study}] detail: nic ke zpracování") + return + print(f" [{study}] detail: {len(files)} soubor(ů) (oldest first)") + for path in files: + parsed = import_to_mongo.parse_detail_filename(path) + if not parsed: + print(f" [{study}] PŘESKAKUJI (nelze parsovat název): {os.path.basename(path)}") + continue + _, parsed_study, subject = parsed + if parsed_study != study: + continue # patří jiné studii + try: + import_to_mongo.import_visits_single_file(study, subject, path) + _move_done(path) + except Exception as e: + print(f" [{study}] CHYBA detail {os.path.basename(path)}: {e}") + + +def main(): + if not os.path.isdir(INCOMING_DIR): + print(f"Adresář neexistuje: {INCOMING_DIR}") + return + ensure_indexes() + + print("=" * 60) + print("Import Subject Summary + Visits") + print("=" * 60) + for study in STUDIES: + import_summaries(study) + import_details(study) + + print("\n" + "=" * 60) + print("Import notifikací") + print("=" * 60) + import_notifications_to_mongo.import_from_dir(INCOMING_DIR, DONE_DIR, STUDIES) + + print("\n" + "=" * 60) + print(f"Hotovo. Zpracované soubory: {DONE_DIR}") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/IWRS/Trash/Patients/Trash/import_to_mysql.py b/IWRS/Trash/Patients/Trash/import_to_mysql.py new file mode 100644 index 0000000..d8ffe6b --- /dev/null +++ b/IWRS/Trash/Patients/Trash/import_to_mysql.py @@ -0,0 +1,453 @@ +""" +Importuje data z IWRS Excel reportů do MySQL (databáze studie). + +Pořadí spuštění: + 1. download_subject_summary.py + 2. download_subject_details.py + 3. tento skript + +Každé spuštění vytvoří nový import_id v iwrs_import. +Reportovací skripty pracují vždy s MAX(import_id) pro danou studii. +""" + +import os +import glob +import datetime +import re + +import numpy as np +import pandas as pd +import mysql.connector + +import db_config + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "IncomingSourceReports") +DETAILS_DIR = os.path.join(BASE_DIR, "IncomingSourceReportsDetails") + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + + +# ── helpers ────────────────────────────────────────────────────────────────── + +def get_conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, + port=db_config.DB_PORT, + user=db_config.DB_USER, + password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + + +def _py(val): + """Převede numpy skalár na Python nativní typ.""" + if isinstance(val, np.generic): + return val.item() + return val + + +def to_date(val): + """Převede pandas Timestamp / string / NaT / NaN na date nebo None.""" + val = _py(val) + if val is None or (isinstance(val, float) and (val != val)): # NaN check + return None + try: + if pd.isna(val): + return None + except (TypeError, ValueError): + pass + if isinstance(val, pd.Timestamp): + return None if pd.isna(val) else val.date() + if isinstance(val, datetime.datetime): + return val.date() + if isinstance(val, datetime.date): + return val + s = str(val).strip() + if not s or s.lower() in ("nat", "nan", "none", ""): + return None + for fmt in ("%Y-%m-%d", "%d-%b-%Y", "%d-%m-%Y", "%Y-%m-%d %H:%M:%S"): + try: + return datetime.datetime.strptime(s, fmt).date() + except ValueError: + pass + return None + + +def to_int(val): + val = _py(val) + try: + v = float(val) + return None if (v != v) else int(v) # v != v je True jen pro NaN + except (TypeError, ValueError): + return None + + +def to_float(val): + val = _py(val) + try: + v = float(val) + return None if (v != v) else float(v) + except (TypeError, ValueError): + return None + + +def to_str(val): + val = _py(val) + if val is None: + return None + if isinstance(val, float) and (val != val): # NaN + return None + s = str(val).strip() + return None if s.lower() in ("nan", "nat", "none", "") else s + + +def find_summary_file(study): + today = datetime.date.today().strftime("%Y-%m-%d") + pattern = os.path.join(INCOMING_DIR, f"* {study} Subject Summary Report.xlsx") + files = sorted( + [f for f in glob.glob(pattern) if not os.path.basename(f).startswith("~$")], + key=os.path.getmtime, + reverse=True, + ) + if not files: + raise FileNotFoundError(f"Nenalezen Subject Summary Report pro {study}") + if not os.path.basename(files[0]).startswith(today): + print(f" UPOZORNĚNÍ: nejnovější Summary Report pro {study} není z dnešního dne ({os.path.basename(files[0])[:10]})") + return files[0] + + +def read_summary_df(path): + """Přečte Summary xlsx, vrátí DataFrame od řádku s hlavičkou.""" + raw = pd.read_excel(path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Subject" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + raise ValueError(f"Hlavičkový řádek nenalezen v {path}") + return pd.read_excel(path, header=header_row) + + +def find_detail_files(study): + out_dir = os.path.join(DETAILS_DIR, study) + # Vezme soubory ze stejného dne jako nejnovější Summary Report + summary_path = find_summary_file(study) + file_date = os.path.basename(summary_path)[:10] # "YYYY-MM-DD" + pattern = os.path.join(out_dir, f"{file_date} {study} * Subject Detail.xlsx") + files = [f for f in glob.glob(pattern) if not os.path.basename(f).startswith("~$")] + return sorted(files) + + +def parse_detail_visits(path): + """ + Vrátí list slovníků s daty visitů z Detail xlsx. + Každý řádek tabulky (od řádku s hlavičkou Visit Type) je jedna transakce. + """ + df = pd.read_excel(path, sheet_name="patient_detail_report", header=None) + + header_row = None + for i, row in df.iterrows(): + if "Visit Type" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + return [] + + visits_df = df.iloc[header_row + 1:].copy() + visits_df.columns = range(visits_df.shape[1]) + + rows = [] + for _, r in visits_df.iterrows(): + visit_type = to_str(r.get(0)) + if visit_type not in ("Past", "Upcoming"): + continue + rows.append({ + "visit_type": visit_type, + "scheduled_date": to_date(r.get(1)), + "window_days": to_str(r.get(2)), + "actual_date": to_date(r.get(3)), + "irt_transaction_no": to_int(r.get(4)), + "irt_transaction_description": to_str(r.get(5)), + "medication_assignment": to_str(r.get(6)), + "quantity_assigned": to_int(r.get(7)), + "medication_id": to_str(r.get(8)), + }) + return rows + + +# ── insert helpers ──────────────────────────────────────────────────────────── + +def insert_import(cursor, study, source_file): + cursor.execute( + "INSERT INTO iwrs_import (study, imported_at, source_file) VALUES (%s, %s, %s)", + (study, datetime.datetime.now(), os.path.basename(source_file)), + ) + return cursor.lastrowid + + +def insert_uco3001_summary(cursor, import_id, df): + sql = """ + INSERT INTO iwrs_uco3001_subject_summary ( + import_id, subject, prior_subject_identifier, site, investigator, location, + cohort_per_irt, informed_consent_date, adolescent_assent_date, age, weight, + rescreened_subject, adt_ir, three_or_more_advanced_therapies, + only_oral_5asa_compounds, ustekinumab, isolated_proctitis, + clinical_responder_status_i12_m0, irt_subject_status, + i0_rand_date_local, last_irt_transaction, + last_irt_transaction_date_local, last_irt_transaction_date_utc, + next_irt_transaction, next_irt_transaction_date_local, + most_recent_med_assignment_date, days_since_last_med_assignment, + patient_forecast_status, patient_forecast_status_changed_date + ) VALUES ( + %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s + ) + """ + col = df.columns.tolist() + + def c(name): + return col.index(name) if name in col else None + + for _, r in df.iterrows(): + cursor.execute(sql, ( + import_id, + to_str(r["Subject"]), + to_str(r["Prior Subject Identifier"]) if "Prior Subject Identifier" in col else None, + to_str(r["Site"]), + to_str(r["Investigator"]), + to_str(r["Location"]), + to_str(r["Cohort per IRT"]), + to_date(r["Informed Consent Date"]), + to_date(r["Adolescent Assent Date"]) if "Adolescent Assent Date" in col else None, + to_int(r["Subject's age collection"]), + to_float(r["Subject's weight collection"]) if "Subject's weight collection" in col else None, + to_str(r["Rescreened Subject"]) if "Rescreened Subject" in col else None, + to_str(r["ADT-IR"]) if "ADT-IR" in col else None, + to_str(r["3 or More Advanced Therapies"]) if "3 or More Advanced Therapies" in col else None, + to_str(r["Only Oral 5-ASA Compounds"]) if "Only Oral 5-ASA Compounds" in col else None, + to_str(r["Ustekinumab"]) if "Ustekinumab" in col else None, + to_str(r["Isolated Proctitis"]) if "Isolated Proctitis" in col else None, + to_str(r["Clinical Responder Status at I-12 / M-0"]) if "Clinical Responder Status at I-12 / M-0" in col else None, + to_str(r["IRT Subject Status"]), + to_date(r["I0_RAND_TIMESTAMP_LOCAL [Local]"]) if "I0_RAND_TIMESTAMP_LOCAL [Local]" in col else None, + to_str(r["Last Recorded IRT Transaction"]), + to_date(r["Last Recorded IRT Transaction Date [Local]"]), + to_date(r["Last Recorded IRT Transaction Date (UTC)"]), + to_str(r["Next Expected IRT Transaction"]), + to_date(r["Next Expected IRT Transaction Date [Local]"]), + to_date(r["Most Recent Medication Assignment Transaction [Local]"]) if "Most Recent Medication Assignment Transaction [Local]" in col else None, + to_int(r["Days Since Last Medication Assignment Transaction"]) if "Days Since Last Medication Assignment Transaction" in col else None, + to_str(r["Patient Forecast Status"]) if "Patient Forecast Status" in col else None, + to_date(r["Patient Forecast Status Changed Date (UTC)"]) if "Patient Forecast Status Changed Date (UTC)" in col else None, + )) + + +def insert_mdd3003_summary(cursor, import_id, df): + sql = """ + INSERT INTO iwrs_mdd3003_subject_summary ( + import_id, subject, prior_subject_identifier, site, investigator, location, + cohort_per_irt, madrs_criteria_integrated, informed_consent_date, age, + madrs_criteria_v15, madrs_criteria_v16, madrs_criteria_v17, + stratification_country, age_group, stable_remitters, irt_subject_status, + last_irt_transaction, last_irt_transaction_date_local, + last_irt_transaction_date_utc, next_irt_transaction, + next_irt_transaction_date_local, date_screened, date_screen_failed, + date_randomized_part1, date_early_withdraw_randomized_part1, + date_open_label_induction, date_early_withdraw_open_label_induction, + date_randomized_part2, date_early_withdraw_randomized_part2, + date_completed, date_unblinded + ) VALUES ( + %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s + ) + """ + col = df.columns.tolist() + + for _, r in df.iterrows(): + cursor.execute(sql, ( + import_id, + to_str(r["Subject"]), + to_str(r["Prior Subject Identifier"]) if "Prior Subject Identifier" in col else None, + to_str(r["Site"]), + to_str(r["Investigator"]), + to_str(r["Location"]), + to_str(r["Cohort per IRT"]), + to_str(r["MADRS response criteria integrated or manually entered"]) if "MADRS response criteria integrated or manually entered" in col else None, + to_date(r["Informed Consent Date"]), + to_int(r["Subject's age collection"]), + to_str(r["MADRS response criteria v1.5 from RAVE"]) if "MADRS response criteria v1.5 from RAVE" in col else None, + to_str(r["MADRS response criteria v1.6 from RAVE"]) if "MADRS response criteria v1.6 from RAVE" in col else None, + to_str(r["MADRS response criteria v1.7 from RAVE"]) if "MADRS response criteria v1.7 from RAVE" in col else None, + to_str(r["Stratification Country"]) if "Stratification Country" in col else None, + to_str(r["Age Group"]) if "Age Group" in col else None, + to_str(r["Stable Remitters vs. Non Stable Remitters"]) if "Stable Remitters vs. Non Stable Remitters" in col else None, + to_str(r["IRT Subject Status"]), + to_str(r["Last Recorded IRT Transaction"]), + to_date(r["Last Recorded IRT Transaction Date [Local]"]), + to_date(r["Last Recorded IRT Transaction Date (UTC)"]), + to_str(r["Next Expected IRT Transaction"]), + to_date(r["Next Expected IRT Transaction Date [Local]"]), + to_date(r["Date Screened [Local]"]) if "Date Screened [Local]" in col else None, + to_date(r["Date Screen Failed [Local]"]) if "Date Screen Failed [Local]" in col else None, + to_date(r["Date Randomized Part 1 [Local]"]) if "Date Randomized Part 1 [Local]" in col else None, + to_date(r["Date Early Withdraw Randomized Part 1 [Local]"]) if "Date Early Withdraw Randomized Part 1 [Local]" in col else None, + to_date(r["Date Open Label Induction [Local]"]) if "Date Open Label Induction [Local]" in col else None, + to_date(r["Date Early Withdraw Open Label Induction [Local]"]) if "Date Early Withdraw Open Label Induction [Local]" in col else None, + to_date(r["Date Randomized Part 2 [Local]"]) if "Date Randomized Part 2 [Local]" in col else None, + to_date(r["Date Early Withdraw Randomized Part 2 [Local]"]) if "Date Early Withdraw Randomized Part 2 [Local]" in col else None, + to_date(r["Date Completed [Local]"]) if "Date Completed [Local]" in col else None, + to_date(r["Date Unblinded [Local]"]) if "Date Unblinded [Local]" in col else None, + )) + + +def insert_visits(cursor, import_id, study, subject, visits): + if not visits: + return + sql = """ + INSERT INTO iwrs_subject_visits ( + import_id, study, subject, visit_type, scheduled_date, window_days, + actual_date, irt_transaction_no, irt_transaction_description, + medication_assignment, quantity_assigned, medication_id + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + """ + for v in visits: + cursor.execute(sql, ( + import_id, study, subject, + v["visit_type"], v["scheduled_date"], v["window_days"], + v["actual_date"], v["irt_transaction_no"], + v["irt_transaction_description"], v["medication_assignment"], + v["quantity_assigned"], v["medication_id"], + )) + + +# ── notifications ───────────────────────────────────────────────────────────── + +def find_notification_json_files(study): + """Najde všechny .json soubory notifikací pro danou studii.""" + out_dir = os.path.join(DETAILS_DIR, study) + return sorted(glob.glob(os.path.join(out_dir, "*.json"))) + + +def import_notifications(conn, study): + import json as json_lib + json_files = find_notification_json_files(study) + if not json_files: + print(f" Žádné notifikace k importu pro {study}") + return 0 + + sql = """ + INSERT INTO iwrs_notifications + (study, subject, pk, title, label, event, actual_date, text, pdf, source_file) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + label = VALUES(label), + text = VALUES(text), + pdf = VALUES(pdf), + source_file = VALUES(source_file) + """ + + done_dir = os.path.join(os.path.join(DETAILS_DIR, study), "Zpracováno") + os.makedirs(done_dir, exist_ok=True) + + cursor = conn.cursor() + count = 0 + for json_path in json_files: + try: + with open(json_path, "r", encoding="utf-8") as f: + meta = json_lib.load(f) + + pdf_path = json_path.replace(".json", ".pdf") + pdf_data = None + if os.path.exists(pdf_path): + with open(pdf_path, "rb") as f: + pdf_data = f.read() + + cursor.execute(sql, ( + meta.get("study", study), + meta.get("subject"), + meta.get("pk"), + meta.get("title"), + meta.get("label"), + meta.get("event"), + to_date(meta.get("actual_date")), + meta.get("text"), + pdf_data, + os.path.basename(json_path), + )) + count += 1 + + # Přesun do Zpracováno + import shutil + shutil.move(json_path, os.path.join(done_dir, os.path.basename(json_path))) + if os.path.exists(pdf_path): + shutil.move(pdf_path, os.path.join(done_dir, os.path.basename(pdf_path))) + + except Exception as e: + print(f" CHYBA při importu {os.path.basename(json_path)}: {e}") + + conn.commit() + cursor.close() + print(f" Notifikací uloženo/přesunuto: {count}") + return count + + +# ── main ────────────────────────────────────────────────────────────────────── + +def import_study(conn, study): + summary_path = find_summary_file(study) + print(f" Summary: {os.path.basename(summary_path)}") + + df_summary = read_summary_df(summary_path) + df_summary = df_summary.dropna(how="all") + + detail_files = find_detail_files(study) + print(f" Detail souborů: {len(detail_files)}") + + cursor = conn.cursor() + import_id = insert_import(cursor, study, summary_path) + print(f" import_id = {import_id}") + + if study == "77242113UCO3001": + insert_uco3001_summary(cursor, import_id, df_summary) + else: + insert_mdd3003_summary(cursor, import_id, df_summary) + print(f" Summary řádků: {len(df_summary)}") + + visited = 0 + for path in detail_files: + fname = os.path.basename(path) + # název: "2026-05-04 77242113UCO3001 CZ100012001 Subject Detail.xlsx" + m = re.search(r"\d{4}-\d{2}-\d{2} \S+ (\S+) Subject Detail\.xlsx", fname) + subject = m.group(1) if m else "UNKNOWN" + visits = parse_detail_visits(path) + insert_visits(cursor, import_id, study, subject, visits) + visited += len(visits) + + conn.commit() + cursor.close() + print(f" Transakce uloženo: {visited}") + return import_id + + +def main(): + conn = get_conn() + print("Připojeno k MySQL.\n") + + for study in STUDIES: + print(f"[{study}]") + try: + import_id = import_study(conn, study) + print(f" OK — import_id {import_id}") + except Exception as e: + print(f" CHYBA: {e}") + try: + import_notifications(conn, study) + except Exception as e: + print(f" CHYBA notifikace: {e}") + print() + + conn.close() + print("Hotovo.") + + +main() diff --git a/IWRS/Trash/Patients/Trash/run_all.py b/IWRS/Trash/Patients/Trash/run_all.py new file mode 100644 index 0000000..8ea266a --- /dev/null +++ b/IWRS/Trash/Patients/Trash/run_all.py @@ -0,0 +1,175 @@ +""" +Kompletní pipeline: + 1. Stažení Subject Summary Reportů (obě studie) + 2. Stažení Subject Detail Reportů + notifikací (obě studie) + 3. Import do MongoDB (subject_summary + visits + notifications) + +Spusť tento skript místo samostatných skriptů. +""" + +import os +import sys +import datetime +import glob + +from playwright.sync_api import sync_playwright + +import download_subject_details as dsd +import import_to_mongo +import import_notifications_to_mongo + +# ── CONFIG ─────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDIES = ["77242113UCO3001", "42847922MDD3003"] + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +INCOMING_DIR = os.path.join(BASE_DIR, "IncomingSourceReports") +DETAILS_DIR = os.path.join(BASE_DIR, "IncomingSourceReportsDetails") + + +# ── helpers ─────────────────────────────────────────────────────────────────── + +def unique_path(directory, stem): + path = os.path.join(directory, f"{stem}.xlsx") + if not os.path.exists(path): + return path + time_tag = datetime.datetime.now().strftime("%H%M") + return os.path.join(directory, f"{stem} {time_tag}.xlsx") + + +def login(page, study): + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + page.get_by_label("Study *").click() + page.get_by_role("option", name=study).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + +# ── KROK 1: Subject Summary ─────────────────────────────────────────────────── + +def download_summary(page, study, today): + print(f" [{study}] Stahuji Subject Summary Report...") + page.goto(f"{BASE_URL}/report/patient_summary_report") + page.wait_for_load_state("networkidle", timeout=120000) + filename = unique_path(INCOMING_DIR, f"{today} {study} Subject Summary Report") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{study}] Summary OK -> {os.path.basename(filename)}") + return filename + + +# ── KROK 2: Subject Details ─────────────────────────────────────────────────── + +def get_subjects_from_summary(summary_path): + import pandas as pd + raw = pd.read_excel(summary_path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Subject" in [str(v).strip() for v in row]: + header_row = i + break + if header_row is None: + raise ValueError("Hlavičkový řádek nenalezen") + df = pd.read_excel(summary_path, header=header_row) + return df["Subject"].dropna().astype(str).str.strip().tolist() + + +def download_details(page, study, summary_path, today): + out_dir = os.path.join(DETAILS_DIR, study) + os.makedirs(out_dir, exist_ok=True) + + subjects = get_subjects_from_summary(summary_path) + print(f" [{study}] Subjektů k stažení: {len(subjects)}") + + page.goto(f"{BASE_URL}/report/patient_detail_report") + page.wait_for_load_state("networkidle", timeout=120000) + + for subject in subjects: + filename = os.path.join(out_dir, f"{today} {study} {subject} Subject Detail.xlsx") + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(subject) + page.wait_for_timeout(500) + page.locator("mat-option").first.dispatch_event("click") + page.wait_for_load_state("networkidle", timeout=120000) + + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{study}] Detail {subject} OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + +# ── KROK 3: Import do MongoDB ──────────────────────────────────────────────── + +def main(): + today = datetime.date.today().strftime("%Y-%m-%d") + os.makedirs(INCOMING_DIR, exist_ok=True) + os.makedirs(DETAILS_DIR, exist_ok=True) + + summary_paths = {} + + # Krok 1 + 2: stahování (Playwright, každá studie zvlášť kvůli session) + with sync_playwright() as p: + for study in STUDIES: + print("\n" + "=" * 60) + print(f"[{study}] KROK 1: Subject Summary Report") + print("=" * 60) + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + try: + login(page, study) + summary_path = download_summary(page, study, today) + summary_paths[study] = summary_path + + print(f"\n[{study}] KROK 2: Subject Detail Reports + notifikace") + dsd.run(page, study) + + except Exception as e: + print(f" [{study}] CHYBA při stahování: {e}") + summary_paths[study] = None + finally: + browser.close() + + # Krok 3: import do MongoDB + print("\n" + "=" * 60) + print("KROK 3: Import do MongoDB") + print("=" * 60) + + for study in STUDIES: + summary_path = summary_paths.get(study) + if not summary_path: + print(f" [{study}] PŘESKOČENO — stahování selhalo") + continue + + try: + import_to_mongo.run(study, summary_path, DETAILS_DIR, today) + except Exception as e: + print(f" [{study}] CHYBA při importu summary/visits: {e}") + + # Notifikace: PDF/JSON z disku rovnou do Mongo iwrs_notifications + print("\n [notifikace] import PDF/JSON do Mongo...") + try: + import_notifications_to_mongo.main(STUDIES) + except Exception as e: + print(f" CHYBA při importu notifikací: {e}") + + print("\n" + "=" * 60) + print("Vše hotovo.") + print("=" * 60) + + +main() diff --git a/IWRS/Trash/Patients/Trash/test_notifications.py b/IWRS/Trash/Patients/Trash/test_notifications.py new file mode 100644 index 0000000..ae2c5d3 --- /dev/null +++ b/IWRS/Trash/Patients/Trash/test_notifications.py @@ -0,0 +1,172 @@ +from playwright.sync_api import sync_playwright +import re +import os +import datetime +import mysql.connector +import db_config + + +def get_existing_pks(study): + """Vrátí set pk notifikací které už jsou v DB pro danou studii.""" + try: + conn = mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + cursor = conn.cursor() + cursor.execute("SELECT pk FROM iwrs_notifications WHERE study = %s", (study,)) + pks = {row[0] for row in cursor.fetchall()} + cursor.close() + conn.close() + return pks + except Exception as e: + print(f" UPOZORNĚNÍ: nelze načíst existující pk z DB ({e}), stahuji vše") + return set() + +BASE_URL = "https://janssen.4gclinical.com" +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-+" + +STUDY = "77242113UCO3001" +SUBJECT = "CZ100222003" + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DETAILS_DIR = os.path.join(BASE_DIR, "IncomingSourceReportsDetails") + + +def strip_html(html): + text = re.sub(r"", "\n", html, flags=re.IGNORECASE) + text = re.sub(r"<[^>]+>", "", text) + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +def main(): + existing_pks = get_existing_pks(STUDY) + print(f"V DB již existuje {len(existing_pks)} notifikací pro {STUDY}") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=False, args=["--start-maximized"]) + context = browser.new_context(no_viewport=True) + page = context.new_page() + + print("Přihlašuji se...") + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator("#login__submit").click() + page.wait_for_load_state("networkidle") + + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + page.goto(f"{BASE_URL}/report/patient_detail_report") + page.wait_for_load_state("networkidle", timeout=60000) + + # JWT + api_base + jwt = page.evaluate("localStorage.getItem('JWT.access')") + print(f"JWT: {jwt[:30]}...") + instances = page.evaluate("""async (jwt) => { + const res = await fetch('/_/api/dispatch/app_instances/', { + headers: { 'Authorization': `Bearer ${jwt}` } + }); + return res.json(); + }""", jwt) + instance = next((i for i in instances if STUDY in i.get("label", "")), None) + if not instance: + raise ValueError(f"Instance pro {STUDY} nenalezena") + api_base = instance["api_base_url"] + print(f"API base: {api_base}") + + # Vyber subjekt a zachyť table_1 response přímo + print(f"Vybírám subjekt {SUBJECT}...") + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(SUBJECT) + page.wait_for_timeout(1000) + + captured = {} + with page.expect_response( + lambda r: "report_data" in r.url and "table_1" in r.url, + timeout=60000 + ) as resp_info: + page.locator("mat-option").first.dispatch_event("click") + + response = resp_info.value + data = response.json() + + out_dir = os.path.join(DETAILS_DIR, STUDY) + os.makedirs(out_dir, exist_ok=True) + today = datetime.date.today().strftime("%Y-%m-%d") + + print(f"\n{'='*60}") + print(f"Subjekt: {SUBJECT} | Studie: {STUDY}") + print(f"{'='*60}") + + count = 0 + for row in data.get("data", []): + for notif in (row.get("notification") or []): + item = notif.get("item", {}) + pk = item.get("pk") + title = item.get("et_title") + label = (notif.get("label") or title or "").strip() + # Celý label, mezery → podtržítka, nepovolené znaky pryč + safe_label = re.sub(r'[\\/*?:"<>|]', "", label).replace(" ", "_") + body = item.get("body", "") + text = strip_html(body) + count += 1 + print(f"\n--- Notifikace #{count}: {safe_label} (pk={pk}) | event: {row.get('event_event_id')} ---") + print(text) + + if pk in existing_pks: + print(f" → pk={pk} již v DB, přeskakuji") + continue + + actual_date = row.get("actual_date_raw", "0000-00-00") + pdf_filename = os.path.join(out_dir, f"{actual_date}_{safe_label}.pdf") + if os.path.exists(pdf_filename): + pdf_filename = os.path.join(out_dir, f"{actual_date}_{safe_label}_pk{pk}.pdf") + + pdf_url = f"{BASE_URL}{api_base}/api/v1/meta_api/pdfnotification?pk={pk}&title={title}&html=true" + pdf_resp = page.request.get(pdf_url, headers={ + "Authorization": f"Bearer {jwt}", + "lang": "en", + "prancer_study": STUDY, + "Accept": "application/json, text/plain, */*", + }) + if pdf_resp.ok: + with open(pdf_filename, "wb") as f: + f.write(pdf_resp.body()) + print(f" → PDF uloženo: {os.path.basename(pdf_filename)}") + json_filename = pdf_filename.replace(".pdf", ".json") + import json + with open(json_filename, "w", encoding="utf-8") as f: + json.dump({ + "pk": pk, + "title": title, + "label": label, + "event": row.get("event_event_id"), + "actual_date": actual_date, + "subject": SUBJECT, + "study": STUDY, + "text": text, + }, f, ensure_ascii=False, indent=2) + print(f" → JSON uloženo: {os.path.basename(json_filename)}") + else: + print(f" → PDF chyba: {pdf_resp.status}") + page.wait_for_timeout(300) + + if count == 0: + print("Žádné notifikace nalezeny.") + else: + print(f"\n{'='*60}") + print(f"Celkem notifikací: {count}") + + browser.close() + + +main() diff --git a/IWRS/Trash/Patients/db_config.py b/IWRS/Trash/Patients/db_config.py new file mode 100644 index 0000000..bfa5959 --- /dev/null +++ b/IWRS/Trash/Patients/db_config.py @@ -0,0 +1,5 @@ +DB_HOST = "192.168.1.76" +DB_PORT = 3306 +DB_USER = "root" +DB_PASSWORD = "Vlado9674+" +DB_NAME = "studie" diff --git a/IWRS/Trash/Testing/format_accountability.py b/IWRS/Trash/Testing/format_accountability.py new file mode 100644 index 0000000..a7ef956 --- /dev/null +++ b/IWRS/Trash/Testing/format_accountability.py @@ -0,0 +1,118 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +INPUT_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "accountability_formatted.xlsx" +SHEET_NAME = "CountryMedicationOverview" + +COLUMN_RENAMES = { + "Site": "Site", + "Medication ID": "Med ID", + "Packaged Lot number": "Lot No.", + "Original Expiration Date when Packaged Lot was Added": "Orig Exp Date", + "Expiration date": "Exp Date", + "Received Date": "Rcv Date", + "Shipment Receipt User": "Rcpt User", + "Subject Identifier": "Subject ID", + "Quantity Assigned": "Qty Asgn", + "IRT Transaction": "IRT Tx", + "Date Assigned": "Date Asgn", + "Assignment User": "Asgn User", + "Dispensation Status": "Disp Status", + "Dispensing Date": "Disp Date", + "Quantity Dispensed": "Qty Disp", + "Dispensing User": "Disp User", + "Quantity Returned": "Qty Ret", + "Date Returned": "Date Ret", + "Return User": "Ret User", + "DestroyedOn": "Destroyed", + "Basket number": "Basket No.", +} + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +# ── 1. Load with pandas and convert date columns ───────────────────────────── +df = pd.read_excel(INPUT_FILE) +df.rename(columns=COLUMN_RENAMES, inplace=True) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], dayfirst=True, errors="coerce") + +df.sort_values(["Site", "Rcv Date", "Med ID"], inplace=True, ignore_index=True) +df.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# ── 2. Format with openpyxl ─────────────────────────────────────────────────── +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="1F4E79") +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +new_col_fill = PatternFill("solid", start_color="E2EFDA") +row_font = Font(name="Arial", size=10) + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] +new_cols = {"Destroyed", "Basket No."} + +# Header row +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +# Data rows +max_row = ws.max_row +for row in ws.iter_rows(min_row=2, max_row=max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name in new_cols: + cell.fill = new_col_fill + +# Column widths +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} ({max_row - 1} rows, sheet '{SHEET_NAME}')") diff --git a/IWRS/Trash/Testing/list_reports.py b/IWRS/Trash/Testing/list_reports.py new file mode 100644 index 0000000..ab0e876 --- /dev/null +++ b/IWRS/Trash/Testing/list_reports.py @@ -0,0 +1,74 @@ +from playwright.sync_api import sync_playwright +import json + +# ── CONFIG ────────────────────────────────────────────────────────────────── +BASE_URL = "https://janssen.4gclinical.com" +STUDY = "42847922MDD3003" + +EMAIL = "vbuzalka@its.jnj.com" +PASSWORD = "Vlado123++-" # doplň heslo +# ──────────────────────────────────────────────────────────────────────────── + + +def list_reports(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + # Přihlášení + page.goto(BASE_URL) + page.wait_for_load_state("networkidle") + + page.get_by_label("Email *").fill(EMAIL) + page.get_by_label("Password *").fill(PASSWORD) + page.locator('#login__submit').click() + page.wait_for_load_state("networkidle") + + # Výběr studie — klikni na dropdown, vyber studii, klikni SELECT + page.get_by_label("Study *").click() + page.get_by_role("option", name=STUDY).click() + page.get_by_role("button", name="SELECT").click() + page.wait_for_load_state("networkidle") + + # Přejdi na seznam reportů + page.goto(f"{BASE_URL}/reports") + page.wait_for_load_state("networkidle") + page.wait_for_selector('[role="gridcell"] a', timeout=15000) + + # Získej názvy reportů + names = page.evaluate(""" + () => Array.from(document.querySelectorAll('[role="gridcell"] a')) + .map(a => a.innerText.trim()) + .filter(n => n) + """) + print(f"\nNalezeno {len(names)} reportů, zjišťuji URL...\n") + + # Pro každý report klikni, zaznamenej URL a vrať se zpět + reports = [] + for name in names: + with page.expect_navigation(timeout=15000): + page.locator('[role="gridcell"] a').filter(has_text=name).click() + page.wait_for_load_state("networkidle") + page.wait_for_timeout(2000) + path = page.url.replace(BASE_URL, "") + reports.append({"name": name, "href": path}) + print(f" {name:50s} {path}") + # Průběžné uložení po každém reportu + with open("reports.json", "w", encoding="utf-8") as f: + json.dump(reports, f, ensure_ascii=False, indent=2) + if page.url != f"{BASE_URL}/reports": + page.goto(f"{BASE_URL}/reports") + page.wait_for_load_state("networkidle") + page.wait_for_timeout(2000) + page.wait_for_selector('[role="gridcell"] a', timeout=30000) + + browser.close() + + with open("reports.json", "w", encoding="utf-8") as f: + json.dump(reports, f, ensure_ascii=False, indent=2) + print(f"\nUloženo do reports.json") + + return reports + + +list_reports() diff --git a/IWRS/Trash/Testing/sheet_assigned_not_dispensed.py b/IWRS/Trash/Testing/sheet_assigned_not_dispensed.py new file mode 100644 index 0000000..3b97d1a --- /dev/null +++ b/IWRS/Trash/Testing/sheet_assigned_not_dispensed.py @@ -0,0 +1,92 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_assigned_not_dispensed.xlsx" +SHEET_NAME = "Assigned not dispensed" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: Subject ID present AND Disp Date missing +mask = df["Subject ID"].notna() & df["Disp Date"].isna() +filtered = df[mask].copy().reset_index(drop=True) + +print(f"Assigned not dispensed: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="833C00") # dark orange +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +subj_fill = PatternFill("solid", start_color="FFF2CC") # light yellow highlight for Subject ID + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Subject ID": + cell.fill = subj_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')") diff --git a/IWRS/Trash/Testing/sheet_expired.py b/IWRS/Trash/Testing/sheet_expired.py new file mode 100644 index 0000000..b41d353 --- /dev/null +++ b/IWRS/Trash/Testing/sheet_expired.py @@ -0,0 +1,97 @@ +import pandas as pd +from datetime import date +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_expired.xlsx" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +today = date.today() +sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + +# Load source +df = pd.read_excel(SOURCE_FILE) + +# Convert date columns +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: not in basket AND not assigned to patient AND Exp Date < today +mask = df["Basket No."].isna() & df["Subject ID"].isna() & (df["Exp Date"] < pd.Timestamp(today)) +filtered = df[mask].copy().reset_index(drop=True) + +print(f"Expired kits not in basket: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=sheet_name) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[sheet_name] + +header_fill = PatternFill("solid", start_color="C00000") # dark red +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +exp_fill = PatternFill("solid", start_color="FFE0E0") # light red highlight for Exp Date + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Exp Date": + cell.fill = exp_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{sheet_name}')") diff --git a/IWRS/Trash/Testing/sheet_kits_for_destruction.py b/IWRS/Trash/Testing/sheet_kits_for_destruction.py new file mode 100644 index 0000000..a6b6dd8 --- /dev/null +++ b/IWRS/Trash/Testing/sheet_kits_for_destruction.py @@ -0,0 +1,99 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_kits_for_destruction.xlsx" +SHEET_NAME = "Kits for destruction" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: no basket AND (Date Ret filled OR Disp Status == NOT DISPENSED) +mask = ( + df["Basket No."].isna() & + ( + df["Date Ret"].notna() | + (df["Disp Status"].str.upper() == "NOT DISPENSED") + ) +) +filtered = df[mask].copy().sort_values(["Site", "Date Ret"], ascending=[True, True]) +filtered = filtered.drop(columns=["Destroyed", "Basket No."]).reset_index(drop=True) + +print(f"Kits for destruction: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="595959") # dark grey +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +basket_fill = PatternFill("solid", start_color="FFE0E0") # light red for empty Basket No. + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Basket No.": + cell.fill = basket_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')") diff --git a/IWRS/Trash/Testing/sheet_not_returned.py b/IWRS/Trash/Testing/sheet_not_returned.py new file mode 100644 index 0000000..79f68e2 --- /dev/null +++ b/IWRS/Trash/Testing/sheet_not_returned.py @@ -0,0 +1,102 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_not_returned.xlsx" +SHEET_NAME = "Not returned" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Max Visit Date", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Max Visit Date": 16, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Kits with no return date, assigned to a patient, and not "NOT DISPENSED" +no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].str.upper() != "NOT DISPENSED") +].copy() + +# Max Date Asgn per patient (from full dataset) +max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") +no_ret = no_ret.join(max_asgn, on="Subject ID") + +# Keep only kits where Date Asgn is NOT the latest for that patient +filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + +# Drop columns Q-U and keep Max Visit Date +filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) +filtered = filtered.reset_index(drop=True) + +print(f"Not returned kits: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="375623") # dark green +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +ret_fill = PatternFill("solid", start_color="E2EFDA") # light green highlight for Date Ret + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Max Visit Date": + cell.fill = ret_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')") diff --git a/IWRS/Trash/backfill_mysql_to_mongo.py b/IWRS/Trash/backfill_mysql_to_mongo.py new file mode 100644 index 0000000..cc52b2e --- /dev/null +++ b/IWRS/Trash/backfill_mysql_to_mongo.py @@ -0,0 +1,272 @@ +""" +Jednorázový backfill historických dat z MySQL do MongoDB. + +Pro každou snapshotovanou tabulku: + - všechny řádky všech import_id → snapshot kolekce + - řádky z MAX(import_id) per studie → hlavní kolekce (replace_one upsert) + +Pro idempotentní tabulky (notifications, destruction): + - všechno → hlavní kolekce (replace_one upsert) + +Notifikace jsou už v Mongo z parse_notifications_to_mongo.py — přeskočí se. +""" + +import os +import sys +import datetime + +import mysql.connector +from pymongo import ReplaceOne + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from common.mongo_writer import get_db, ensure_indexes, MONGO_DB + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "Patients")) +import db_config + + +def conn(): + return mysql.connector.connect( + host=db_config.DB_HOST, port=db_config.DB_PORT, + user=db_config.DB_USER, password=db_config.DB_PASSWORD, + database=db_config.DB_NAME, + ) + + +def dict_rows(cursor): + cols = [d[0] for d in cursor.description] + for row in cursor: + yield dict(zip(cols, row)) + + +def to_mongo_date(v): + if isinstance(v, datetime.datetime): + return v + if isinstance(v, datetime.date): + return datetime.datetime(v.year, v.month, v.day) + return v + + +def normalize(doc): + return {k: to_mongo_date(v) for k, v in doc.items() if v is not None} + + +# ── 1. iwrs_imports → iwrs_imports ─────────────────────────────────────────── + +def backfill_imports(): + print("[iwrs_imports]") + c = conn(); cur = c.cursor() + cur.execute("SELECT import_id, study, imported_at, source_file, report_type FROM iwrs_import") + db = get_db() + ops = [] + for r in dict_rows(cur): + d = normalize(r) + d["_id"] = d["import_id"] + ops.append(ReplaceOne({"_id": d["_id"]}, d, upsert=True)) + if ops: + db.iwrs_imports.bulk_write(ops, ordered=False) + print(f" -> {len(ops)} import logu") + cur.close(); c.close() + + +# ── 2. subject_summary (UCO + MDD sjednoceno) ──────────────────────────────── + +UCO_TABLE = "iwrs_uco3001_subject_summary" +MDD_TABLE = "iwrs_mdd3003_subject_summary" + + +def backfill_subject_summary(): + print("[iwrs_subject_summary]") + db = get_db() + # zjisti import_id → study mapování + c = conn(); cur = c.cursor() + cur.execute("SELECT import_id, study, imported_at FROM iwrs_import") + import_meta = {r[0]: {"study": r[1], "imported_at": r[2]} for r in cur.fetchall()} + cur.close(); c.close() + + total_snap = 0 + total_main = 0 + + for table, study in [(UCO_TABLE, "77242113UCO3001"), (MDD_TABLE, "42847922MDD3003")]: + c = conn(); cur = c.cursor() + cur.execute(f"SELECT * FROM {table}") + all_rows = list(dict_rows(cur)) + cur.close(); c.close() + + # MAX import_id per studie (pro hlavní kolekci) + import_ids = [r["import_id"] for r in all_rows if r.get("import_id") is not None] + if not import_ids: + continue + max_import = max(import_ids) + + # snapshoty: každý řádek → iwrs_subject_summary_snapshots + snap_docs = [] + main_ops = [] + for r in all_rows: + doc = normalize(r) + doc.pop("id", None) # MySQL autoincrement nezachováváme + doc["study"] = study + subject = doc.get("subject") + if not subject: + continue + natural = f"{study}:{subject}" + + snap = dict(doc) + snap["natural_id"] = natural + meta = import_meta.get(doc.get("import_id"), {}) + snap["imported_at"] = meta.get("imported_at") + snap_docs.append(snap) + + if doc["import_id"] == max_import: + main = dict(doc) + main["_id"] = natural + main["last_import_id"] = max_import + main["last_imported_at"] = meta.get("imported_at") + main_ops.append(ReplaceOne({"_id": natural}, main, upsert=True)) + + if snap_docs: + db.iwrs_subject_summary_snapshots.insert_many(snap_docs, ordered=False) + total_snap += len(snap_docs) + if main_ops: + db.iwrs_subject_summary.bulk_write(main_ops, ordered=False) + total_main += len(main_ops) + print(f" {study}: snap={len(snap_docs)} main={len(main_ops)}") + + print(f" TOTAL snap={total_snap} main={total_main}") + + +# ── 3. visits, shipments, items, inventory (per import_id) ─────────────────── + +def backfill_per_import(mysql_table, main_coll, snap_coll, id_fn, + drop_cols=("id",)): + print(f"[{mysql_table} -> {main_coll}/{snap_coll}]") + db = get_db() + c = conn(); cur = c.cursor() + + # import_id metadata + cur.execute("SELECT import_id, imported_at FROM iwrs_import") + import_meta = {r[0]: r[1] for r in cur.fetchall()} + + # MAX import_id per studie + cur.execute(f"SELECT study, MAX(import_id) FROM {mysql_table} GROUP BY study") + max_per_study = {r[0]: r[1] for r in cur.fetchall()} + + cur.execute(f"SELECT * FROM {mysql_table}") + all_rows = list(dict_rows(cur)) + cur.close(); c.close() + + snap_docs = [] + main_ops = [] + seen_main = set() + for r in all_rows: + doc = normalize(r) + for col in drop_cols: + doc.pop(col, None) + natural = id_fn(doc) + if not natural: + continue + imp_at = import_meta.get(doc.get("import_id")) + + snap = dict(doc) + snap["natural_id"] = natural + snap["imported_at"] = imp_at + snap_docs.append(snap) + + study = doc.get("study") + if study and doc.get("import_id") == max_per_study.get(study): + if natural in seen_main: + continue + seen_main.add(natural) + main = dict(doc) + main["_id"] = natural + main["last_import_id"] = doc["import_id"] + main["last_imported_at"] = imp_at + main_ops.append(ReplaceOne({"_id": natural}, main, upsert=True)) + + if snap_docs: + db[snap_coll].insert_many(snap_docs, ordered=False) + if main_ops: + db[main_coll].bulk_write(main_ops, ordered=False) + print(f" snap={len(snap_docs)} main={len(main_ops)}") + + +def visit_id(doc): + s, sub = doc.get("study"), doc.get("subject") + if not s or not sub: + return None + key = doc.get("irt_transaction_no") + if key is None: + sd = doc.get("scheduled_date") + key = sd.strftime("%Y%m%d") if sd else "noidx" + desc = (doc.get("irt_transaction_description") or "").replace(" ", "_")[:30] + return f"{s}:{sub}:{key}:{desc}" + + +def shipment_id_(doc): + return doc.get("shipment_id") + + +def shipment_item_id(doc): + s, m = doc.get("shipment_id"), doc.get("medication_id") + return f"{s}:{m}" if s and m else None + + +def inventory_id(doc): + s, m = doc.get("site"), doc.get("medication_id") + return f"{s}:{m}" if s and m else None + + +# ── 4. destruction (idempotentní, jen do main) ─────────────────────────────── + +def backfill_destruction(): + print("[iwrs_destruction]") + db = get_db() + c = conn(); cur = c.cursor() + cur.execute("SELECT * FROM iwrs_destruction") + rows = list(dict_rows(cur)) + cur.close(); c.close() + ops = [] + seen = set() + for r in rows: + doc = normalize(r) + doc.pop("id", None) + basket, med = doc.get("basket_id"), doc.get("medication_id") + if not basket or not med: + continue + nid = f"{basket}:{med}" + if nid in seen: + continue + seen.add(nid) + doc["_id"] = nid + ops.append(ReplaceOne({"_id": nid}, doc, upsert=True)) + if ops: + db.iwrs_destruction.bulk_write(ops, ordered=False) + print(f" -> {len(ops)} destrukci") + + +# ── main ───────────────────────────────────────────────────────────────────── + +def main(): + print(f"Cilova DB: {MONGO_DB}") + ensure_indexes() + backfill_imports() + backfill_subject_summary() + backfill_per_import("iwrs_subject_visits", "iwrs_visits", "iwrs_visits_snapshots", visit_id) + backfill_per_import("iwrs_shipments", "iwrs_shipments", "iwrs_shipments_snapshots", shipment_id_) + backfill_per_import("iwrs_shipment_items", "iwrs_shipment_items", "iwrs_shipment_items_snapshots", shipment_item_id) + backfill_per_import("iwrs_inventory", "iwrs_inventory", "iwrs_inventory_snapshots", inventory_id) + backfill_destruction() + + # finalni statistika + db = get_db() + print("\nFINALNI STAV V MONGO:") + for coll in ["iwrs_imports","iwrs_subject_summary","iwrs_visits","iwrs_notifications", + "iwrs_shipments","iwrs_shipment_items","iwrs_inventory","iwrs_destruction", + "iwrs_subject_summary_snapshots","iwrs_visits_snapshots", + "iwrs_shipments_snapshots","iwrs_shipment_items_snapshots","iwrs_inventory_snapshots"]: + n = db[coll].count_documents({}) + print(f" {coll:42s} {n}") + + +if __name__ == "__main__": + main() diff --git a/IWRS/Trash/reports.json b/IWRS/Trash/reports.json new file mode 100644 index 0000000..c66e6eb --- /dev/null +++ b/IWRS/Trash/reports.json @@ -0,0 +1,23 @@ +[ + {"name": "Drug Accountability Form - Multiple Subjects", "href": "/report/drug_accountability_form_multiple_subjects"}, + {"name": "Drug Accountability Form - Single Subject", "href": "/report/drug_accountability_form_single_subject"}, + {"name": "Janssen Pharmaceuticals IP Destruction Form", "href": "/report/ip_destruction_form"}, + {"name": "On-Site Drug Inventory and Accountability Details Form", "href": "/report/onsite_inventory_detail"}, + {"name": "On-Site Drug Inventory Form", "href": "/report/onsite_drug_inventory_form"}, + {"name": "Location Summary Report", "href": "/report/country_summary_report"}, + {"name": "Site Detail Report", "href": "/report/site_detail_report"}, + {"name": "Study Sites Report", "href": "/report/study_sites_report"}, + {"name": "Site Inventory Detail Report", "href": "/report/site_inventory_detail"}, + {"name": "Site Inventory Summary Report", "href": "/report/site_inventory_summary"}, + {"name": "Subject Data Changes Report", "href": "/report/patient_data_changes_report"}, + {"name": "Subject Detail Report", "href": "/report/patient_detail_report"}, + {"name": "Subject Summary Report", "href": "/report/patient_summary_report"}, + {"name": "Subject Visit Summary Report", "href": "/report/patient_visit_summary"}, + {"name": "Shipment Details Report", "href": "/report/shipment_details_report"}, + {"name": "Shipments Report", "href": "/report/shipments_report"}, + {"name": "Cohort History Report", "href": "/report/cohort_history_report"}, + {"name": "Cohort Summary Report", "href": "/report/cohort_summary_report"}, + {"name": "Site Activations Report", "href": "/report/site_activation_pivot"}, + {"name": "User Login History", "href": "/report/user_logins"}, + {"name": "Users List", "href": "/report/users"} +] diff --git a/IWRS/Trash/run_all_v1.0.md b/IWRS/Trash/run_all_v1.0.md new file mode 100644 index 0000000..d73253b --- /dev/null +++ b/IWRS/Trash/run_all_v1.0.md @@ -0,0 +1,112 @@ +# run_all_v1.0.py — IWRS: kompletní pipeline Pacienti + Léky + +**Verze:** 1.0 | **Datum:** 2026-06-10 + +Jeden vstupní skript na úrovni `IWRS/`, který stáhne z janssen.4gclinical.com +a naimportuje do MongoDB (db `studie`) data pacientů i léků pro obě studie +(77242113UCO3001, 42847922MDD3003). Nahrazuje dřívější `Drugs/run_all.py` +a `Patients/download_all.py` + `Patients/import_all.py` (přesunuty do `Trash/`). + +## Tok souborů + +``` +IWRS/Incoming/ ← sem padá vše stažené (pacienti i léky, datumované názvy) +IWRS/Incoming/Processed/ ← sem se přesouvá po úspěšném importu +``` + +- Při chybě importu soubor **zůstává v Incoming/** a zpracuje se při příštím běhu. +- Import jde vždy **nejstarší soubor napřed** (mtime) — chronologická správnost snapshotů. +- Kolize jména v Processed/ → přepíše se (Mongo už data má, soubor je jen archiv). +- Adresář `IWRS/Incoming/` je v `.gitignore` (stejně jako dříve `Patients/Incoming/`). +- Původní adresáře `Drugs/xls_*` zůstávají zmrazené na místě jako archiv — nový kód je nepoužívá. + +## Názvy souborů v Incoming/ + +| Typ | Vzor | +|---|---| +| Subject Summary | `YYYY-MM-DD {study} Subject Summary Report.xlsx` | +| Subject Detail | `YYYY-MM-DD {study} {subject} Subject Detail.xlsx` | +| Notifikace | `{datum}_{study}_{subject}_{label}.pdf` + `.json` | +| Onsite Inventory | `YYYY-MM-DD {study} Onsite Inventory {site}.xlsx` | +| IP Destruction | `YYYY-MM-DD {study} IP Destruction {basket}.xlsx` | +| Shipments Report | `YYYY-MM-DD {study} Shipments Report.xlsx` | +| Shipment Details | `YYYY-MM-DD {study} Shipment Details {shipment_id}.xlsx` | + +Při kolizi (druhý běh ve stejný den) se před příponu přidá ` HHMM`. +Metadata (site, basket, study) se při importu čtou primárně z **obsahu** souboru; +z názvu se bere jen `shipment_id` u Shipment Details. + +## Průběh + +### Fáze 1 — stahování (2 přihlášení, per studie jedna browser session) + +1. Login + výběr studie (`common/iwrs_portal.py`) +2. **Pacienti** (`Patients/download_patients.py`): + - Subject Summary Report + - per subjekt: Subject Detail XLSX + notifikace PDF+JSON (stahují se jen + notifikace, jejichž `pk` ještě není v Mongo `iwrs_notifications`) +3. **Léky** (`Drugs/download_drugs.py`): + - Onsite Inventory — všechna centra, vždy znovu + - IP Destruction — přeskočí košíky už importované v `iwrs_destruction` + (destrukce je immutable); dříve se přeskakovalo podle existence souboru + - Shipments Report — vždy znovu + - Shipment Details — jen CZ zásilky; přeskočí zásilky, jejichž položky + jsou v `iwrs_shipment_items` se statusem RECEIVED (finální stav); + dříve „soubor existuje a status RECEIVED“. CANCELLED zásilky se stahují + při každém běhu (záměrně zachováno z původní verze). + +### Fáze 2 — import (po stažení obou studií) + +1. `ensure_indexes()` (jednou) +2. **Pacienti** (`Patients/import_patients.py`): summary → detaily → notifikace; + per soubor, po úspěchu přesun do Processed/ +3. **Léky** (`Drugs/import_drugs.py`): jeden `import_id` per studie a běh; + parsuje všechny čekající soubory (nejstarší napřed, poslední vyhrává per `_id`), + pak hromadný zápis: + - `iwrs_shipments`, `iwrs_shipment_items`, `iwrs_inventory` — upsert + snapshot + - `iwrs_destruction` — upsert bez snapshotu + Po úspěšném zápisu se zparsované soubory přesunou do Processed/; + soubor s chybou parsování zůstává v Incoming/. + Prázdný inventory report (centrum bez zásob — jen meta řádky `Site:` atd., + bez tabulky léků) se bere jako 0 položek a normálně se archivuje. + +## Použití + +``` +python run_all_v1.0.py # vše (download + import, obě studie) +python run_all_v1.0.py --download-only # jen stažení do Incoming/ +python run_all_v1.0.py --import-only # jen import čekajících souborů +python run_all_v1.0.py --only-patients # jen pacientská část +python run_all_v1.0.py --only-drugs # jen léková část +python run_all_v1.0.py --study 42847922MDD3003 # jen jedna studie +``` + +Prohlížeč běží s `headless=False` (viditelné okno) jako dosud. +Moduly `import_patients.py` a `import_drugs.py` lze spustit i samostatně. + +## Mapa modulů + +``` +IWRS/ + run_all_v1.0.py ← vstupní skript (CLI, orchestrace) + common/ + iwrs_portal.py ← BASE_URL, credentials, login(page, study) + paths.py ← INCOMING/PROCESSED, unique_path, move_done, sorted_by_mtime + mongo_writer.py ← beze změny (konvertory, upserty, snapshoty, import log) + Patients/ + download_patients.py ← summary + delegace na download_subject_details.run() + import_patients.py ← logika z bývalého import_all.py, nové cesty + download_subject_details.py, import_to_mongo.py, + import_notifications_to_mongo.py, parse_notifications_to_mongo.py ← beze změny + Trash/download_all.py, Trash/import_all.py ← nahrazeno + Drugs/ + download_drugs.py ← 4 typy reportů → Incoming/, skip-logika přes Mongo + import_drugs.py ← parsery z bývalého import_to_mongo.py, čte Incoming/ + Trash/run_all.py, Trash/import_to_mongo.py ← nahrazeno +``` + +## Jednorázová migrace (provedeno 2026-06-10) + +- `Patients/Incoming/Zpracováno/` (1343 souborů) → `IWRS/Incoming/Processed/` +- `.gitignore`: `IWRS/Patients/Incoming/` → `IWRS/Incoming/` +- Staré vstupní skripty → `Trash/` (viz mapa výše) diff --git a/IWRS/Trash/run_all_v1.0.py b/IWRS/Trash/run_all_v1.0.py new file mode 100644 index 0000000..c17a3ce --- /dev/null +++ b/IWRS/Trash/run_all_v1.0.py @@ -0,0 +1,147 @@ +""" +================================================================================ + run_all_v1.0.py — IWRS: kompletní pipeline Pacienti + Léky (obě studie) + Verze: 1.0 + Datum: 2026-06-10 +================================================================================ + +Stáhne z janssen.4gclinical.com a naimportuje do MongoDB (db `studie`): + + Pacienti: Subject Summary, Subject Details, notifikace (PDF+JSON) + Léky: Onsite Inventory, IP Destruction, Shipments Report, Shipment Details + +Tok souborů: vše se stahuje do IWRS/Incoming/, po úspěšném importu se přesouvá +do IWRS/Incoming/Processed/. Při chybě soubor zůstává v Incoming/ a zpracuje +se při příštím běhu. + +Přihlášení: 2× (jednou per studie) — studie se vybírá až po přihlášení, takže +jedna browser session stáhne pacienty i léky pro jednu studii. + +Použití: + python run_all_v1.0.py # vše (download + import, obě studie) + python run_all_v1.0.py --download-only # jen stažení do Incoming/ + python run_all_v1.0.py --import-only # jen import čekajících souborů + python run_all_v1.0.py --only-patients # jen pacientská část + python run_all_v1.0.py --only-drugs # jen léková část + python run_all_v1.0.py --study 42847922MDD3003 # jen jedna studie + +Detaily v run_all_v1.0.md. +""" + +import os +import sys +import argparse +import traceback + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +for _p in (os.path.join(BASE_DIR, "Drugs"), os.path.join(BASE_DIR, "Patients"), BASE_DIR): + if _p not in sys.path: + sys.path.insert(0, _p) + +from playwright.sync_api import sync_playwright + +from common.iwrs_portal import login +from common.paths import STUDIES, INCOMING_DIR, PROCESSED_DIR, ensure_dirs +from common.mongo_writer import ensure_indexes + +import download_patients +import import_patients +import download_drugs +import import_drugs + + +def download_phase(studies, do_patients, do_drugs): + with sync_playwright() as p: + for study in studies: + print(f"\n{'='*60}") + print(f"[{study}] STAHOVÁNÍ") + print(f"{'='*60}") + + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + try: + print(" Přihlášení...") + login(page, study) + + if do_patients: + print(f"\n ── PACIENTI [{study}] ──") + try: + download_patients.run(page, study) + except Exception as e: + print(f" CHYBA při stahování pacientů: {e}") + traceback.print_exc() + + if do_drugs: + print(f"\n ── LÉKY [{study}] ──") + try: + download_drugs.run(page, study) + except Exception as e: + print(f" CHYBA při stahování léků: {e}") + traceback.print_exc() + + except Exception as e: + print(f" CHYBA (login/session): {e}") + traceback.print_exc() + finally: + browser.close() + + +def import_phase(studies, do_patients, do_drugs): + print(f"\n{'='*60}") + print("IMPORT DO MongoDB") + print(f"{'='*60}") + ensure_indexes() + + if do_patients: + try: + import_patients.run(studies) + except Exception as e: + print(f" CHYBA při importu pacientů: {e}") + traceback.print_exc() + + if do_drugs: + try: + import_drugs.run(studies) + except Exception as e: + print(f" CHYBA při importu léků: {e}") + traceback.print_exc() + + +def main(): + ap = argparse.ArgumentParser( + description="IWRS pipeline: stažení + import pacientů a léků (obě studie)") + ap.add_argument("--download-only", action="store_true", help="jen stažení do Incoming/") + ap.add_argument("--import-only", action="store_true", help="jen import čekajících souborů") + ap.add_argument("--only-patients", action="store_true", help="jen pacientská část") + ap.add_argument("--only-drugs", action="store_true", help="jen léková část") + ap.add_argument("--study", choices=STUDIES, help="jen jedna studie") + args = ap.parse_args() + + if args.download_only and args.import_only: + ap.error("--download-only a --import-only nelze kombinovat") + if args.only_patients and args.only_drugs: + ap.error("--only-patients a --only-drugs nelze kombinovat") + + studies = [args.study] if args.study else STUDIES + do_patients = not args.only_drugs + do_drugs = not args.only_patients + + ensure_dirs() + + if not args.import_only: + download_phase(studies, do_patients, do_drugs) + + if not args.download_only: + import_phase(studies, do_patients, do_drugs) + + print(f"\n{'='*60}") + print("Vše hotovo.") + print(f" Incoming: {INCOMING_DIR}") + print(f" Processed: {PROCESSED_DIR}") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/IWRS/create_report_v1.0.md b/IWRS/create_report_v1.0.md new file mode 100644 index 0000000..ad97761 --- /dev/null +++ b/IWRS/create_report_v1.0.md @@ -0,0 +1,56 @@ +# create_report_v1.0.py — IWRS report (pacienti + léky v jednom Excelu) + +**Verze:** 1.0 | **Datum:** 2026-06-10 + +Sjednocuje bývalé `Patients/create_subject_report.py` a `Drugs/create_report.py` +(oba přesunuty do `Trash/`). Postaveno na drugs verzi, která už pacientské listy +obsahovala — pacientský report z XLSX byl její překonaná podmnožina (četl mrtvý +adresář `IncomingSourceReports/`, neměl UCO flag sloupce ani I-0 dopočet). + +## Zdroj a výstup + +- **Zdroj:** výhradně MongoDB `studie` — kolekce `iwrs_subject_summary`, + `iwrs_visits`, `iwrs_inventory`, `iwrs_destruction`, `iwrs_shipments`, + `iwrs_shipment_items`. Žádné čtení XLSX → nezávisí na adresářích + `Patients/` a `Drugs/`. Data plní `run_all_v1.1.py`. +- **Výstup:** `IWRS/Reports/YYYY-MM-DD {study} IWRS report.xlsx` + — kolize ve stejný den → ` HHMM` před příponou (konvence `common.paths.unique_path`, + nahrazuje dřívější číslování ` v{n}`). + +## Listy (v pořadí) + +| # | List | Obsah | +|---|---|---| +| 1 | Přehled | stav pacientů; škrtnutí Screen Failed/Discontinued, bold Randomized + Adolescent; UCO3001 navíc flagy Rescreened / ADT-IR / ≥3 Adv.Th. / 5-ASA only / Uste. / Isol.Proct. | +| 2 | Next Visits | očekávané visity aktivních subjektů, řazeno podle data; visit I-0 = screening + 42 dní | +| 3 | Patient Visits | proběhlé visity s medikací (group by visit, Med IDs + Qty) | +| 4 | CountryMedicationOverview | sklad per kit, zeleně Destroyed + Basket No. | +| 5 | Expired as of … | expirované nepřiřazené kity na centrech (červená hlavička) | +| 6 | Assigned not dispensed | přiřazené, nevydané | +| 7 | Not returned | nevrácené kity z minulých visit (vs. Max Visit Date) | +| 8 | Kits for destruction | vrácené/nevydané kity čekající na destrukci | +| 9 | Shipments | zásilky + položky, dvoubarevná hlavička (zásilka modrá / detail zelená) | +| 10 | Site Summary | počty kitů per centrum a status (Available/Assigned/Dispensed/Returned/Total) | + +Formátování beze změny převzato z obou původních skriptů (pruhované řádky, +autofit, autofilter, freeze panes, data DD-MMM-YYYY). + +## Použití + +``` +python create_report_v1.0.py # obě studie (2 soubory) +python create_report_v1.0.py --study 42847922MDD3003 +``` + +Typický postup: `python run_all_v1.1.py` (stáhne+naimportuje čerstvá data) +→ `python create_report_v1.0.py`. + +## Změny proti původním skriptům + +- jméno výstupu `… IWRS report.xlsx` (dříve `… CZ IWRS overview v{n}.xlsx`, + resp. `… Subject Summary.xlsx`) +- výstupní adresář `IWRS/Reports/` (dříve `Drugs/output/`, resp. `Patients/CreatedReports/`) +- pořadí listů: Přehled první (dříve Patient Visits první) +- list ZDROJ ze starého subject reportu vypuštěn (surová data jsou v Mongu + vč. snapshot historie) +- přidán přepínač `--study` diff --git a/IWRS/create_report_v1.0.py b/IWRS/create_report_v1.0.py new file mode 100644 index 0000000..175c3f5 --- /dev/null +++ b/IWRS/create_report_v1.0.py @@ -0,0 +1,678 @@ +""" +================================================================================ + create_report_v1.0.py — IWRS report: pacienti + léky v jednom Excelu + Verze: 1.0 + Datum: 2026-06-10 +================================================================================ + +Sjednocuje bývalé Patients/create_subject_report.py a Drugs/create_report.py +(oba v Trash/). Čte výhradně MongoDB (db `studie`, kolekce iwrs_*) — žádné XLSX +zdroje, takže nezávisí na adresářích Patients/ a Drugs/. + +Výstup: IWRS/Reports/YYYY-MM-DD {study} IWRS report.xlsx + (kolize ve stejný den → ' HHMM' před příponou) + +Listy (v pořadí): + 1. Přehled — stav pacientů (škrtnutí SF/DC, bold Randomized, + UCO3001 navíc flag sloupce Rescreened/ADT-IR/…) + 2. Next Visits — očekávané visity aktivních subjektů (I-0 = screening+42d) + 3. Patient Visits — proběhlé visity s medikací (group by visit) + 4. CountryMedicationOverview — sklad per kit + Destroyed/Basket No. + 5. Expired as of … — expirované nepřiřazené kity na centrech + 6. Assigned not dispensed — přiřazené, nevydané + 7. Not returned — nevrácené z minulých visit + 8. Kits for destruction — vrácené/nevydané kity čekající na destrukci + 9. Shipments — zásilky + položky (dvoubarevná hlavička) + 10. Site Summary — počty kitů per centrum a status + +Použití: + python create_report_v1.0.py # obě studie + python create_report_v1.0.py --study 42847922MDD3003 +""" + +import os +import sys +import argparse +import pandas as pd +from datetime import date +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from common.mongo_writer import get_db +from common.paths import STUDIES, unique_path + +REPORTS_DIR = os.path.join(BASE_DIR, "Reports") + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", "Max Visit Date", + "Visit Date", "Scheduled Date", +} + +N_SHIP_COLS = 9 # počet shipment sloupců před detail sloupci + + +# ── Načítání dat z MongoDB ──────────────────────────────────────────────────── + +INVENTORY_COLS = [ + ("site", "Site"), + ("medication_id", "Med ID"), + ("packaged_lot_no", "Lot No."), + ("original_expiration_date", "Orig Exp Date"), + ("expiration_date", "Exp Date"), + ("received_date", "Rcv Date"), + ("receipt_user", "Rcpt User"), + ("subject_identifier", "Subject ID"), + ("quantity_assigned", "Qty Asgn"), + ("irt_transaction", "IRT Tx"), + ("date_assigned", "Date Asgn"), + ("assignment_user", "Asgn User"), + ("dispensation_status", "Disp Status"), + ("dispensing_date", "Disp Date"), + ("quantity_dispensed", "Qty Disp"), + ("dispensing_user", "Disp User"), + ("quantity_returned", "Qty Ret"), + ("date_returned", "Date Ret"), + ("return_user", "Ret User"), +] + + +def load_inventory(study): + db = get_db() + inv = list(db.iwrs_inventory.find({"study": study})) + destr = list(db.iwrs_destruction.find({"study": study})) + # map medication_id -> first basket+date + destr_map = {} + for d in destr: + mid = d.get("medication_id") + if mid and mid not in destr_map: + destr_map[mid] = (d.get("basket_id"), d.get("destruction_date")) + + records = [] + for doc in inv: + row = {label: doc.get(key) for key, label in INVENTORY_COLS} + b, dt = destr_map.get(doc.get("medication_id"), (None, None)) + row["Destroyed"] = dt + row["Basket No."] = b + records.append(row) + + df = pd.DataFrame(records) + if df.empty: + print(" Inventory: 0 kitu") + return df + + df = df.sort_values(["Site", "Rcv Date", "Med ID"], na_position="last").reset_index(drop=True) + for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + print(f" Inventory: {len(df)} kitu") + return df + + +SHIP_COLS = [ + ("shipment_id", "Shipment ID"), + ("status", "IRT Shipment Status"), + ("type", "Type"), + ("ship_from", "Shipment From"), + ("ship_to_site", "Ship To:"), + ("request_date", "Request Date"), + ("received_date", "Received Date"), + ("received_by", "Received by"), + ("expected_arrival", "Expected Arrival"), +] + +ITEM_COLS = [ + ("investigator", "Investigator"), + ("medication_description", "Medication Description"), + ("medication_id", "Medication ID"), + ("packaged_lot_no", "Packaged Lot number"), + ("expiration_date", "Expiration Date"), + ("item_status", "Status"), +] + + +def load_shipments(study): + db = get_db() + ships = list(db.iwrs_shipments.find({"study": study})) + items = list(db.iwrs_shipment_items.find({"study": study})) + + # index items by shipment_id + items_by_ship = {} + for it in items: + items_by_ship.setdefault(it.get("shipment_id"), []).append(it) + + records = [] + for s in ships: + base = {label: s.get(key) for key, label in SHIP_COLS} + for it in items_by_ship.get(s.get("shipment_id"), []): + row = dict(base) + for key, label in ITEM_COLS: + row[label] = it.get(key) + records.append(row) + + df = pd.DataFrame(records) + if df.empty: + print(" Shipments: 0 zásilek, 0 kitu") + return df + + df = df.sort_values(["Ship To:", "Shipment ID", "Medication ID"], na_position="last").reset_index(drop=True) + for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"): + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + n_ship = df["Shipment ID"].nunique() + print(f" Shipments: {n_ship} zásilek, {len(df)} kitu") + return df + + +def load_visits(study): + db = get_db() + cur = db.iwrs_visits.find({ + "study": study, + "visit_type": "Past", + "irt_transaction_no": {"$ne": None}, + }) + rows = [] + for v in cur: + rows.append({ + "Subject": v.get("subject"), + "Visit Date": v.get("actual_date") or v.get("scheduled_date"), + "Scheduled Date": v.get("scheduled_date"), + "IRT Tx No": v.get("irt_transaction_no"), + "Visit": v.get("irt_transaction_description"), + "Medication": v.get("medication_assignment"), + "medication_id": v.get("medication_id"), + "quantity_assigned": v.get("quantity_assigned"), + }) + df = pd.DataFrame(rows) + if df.empty: + print(" Visits: 0 radku") + return df + + # GROUP BY subject/actual/scheduled/irt_no/desc/medication + grouped = ( + df.groupby(["Subject", "Visit Date", "Scheduled Date", "IRT Tx No", "Visit", "Medication"], + dropna=False, as_index=False) + .agg(**{ + "Med IDs": ("medication_id", lambda s: ", ".join(sorted([str(x) for x in s if pd.notna(x)]))), + "Qty": ("quantity_assigned", "sum"), + }) + ) + grouped = grouped.sort_values(["Subject", "Visit Date"]).reset_index(drop=True) + for col in ("Visit Date", "Scheduled Date"): + if col in grouped.columns: + grouped[col] = pd.to_datetime(grouped[col], errors="coerce") + if study == "77242113UCO3001": + grouped["Visit"] = grouped["Visit"].replace("Subject Number Creation", "Screening") + print(f" Visits: {len(grouped)} řádků") + return grouped + + +# ── Odvozené sheety ─────────────────────────────────────────────────────────── + +def build_site_summary(shipments_df): + STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"] + pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0) + for s in STATUS_COLS: + if s not in pivot.columns: + pivot[s] = 0 + pivot = ( + pivot[STATUS_COLS] + .reset_index() + .rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"}) + .sort_values("Site") + .reset_index(drop=True) + ) + pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1) + print(f" Site Summary: {len(pivot)} center") + return pivot + + +def build_expired(df): + today = date.today() + mask = ( + df["Basket No."].isna() & + df["Subject ID"].isna() & + (df["Exp Date"] < pd.Timestamp(today)) + ) + filtered = df[mask].copy().reset_index(drop=True) + sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + print(f" Expired: {len(filtered)}") + return filtered, sheet_name + + +def build_assigned_not_dispensed(df): + mask = df["Subject ID"].notna() & df["Disp Date"].isna() + filtered = df[mask].copy().reset_index(drop=True) + print(f" Assigned not dispensed: {len(filtered)}") + return filtered + + +def build_not_returned(df): + no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].fillna("").str.upper() != "NOT DISPENSED") + ].copy() + max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") + no_ret = no_ret.join(max_asgn, on="Subject ID") + filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) + filtered = filtered.reset_index(drop=True) + print(f" Not returned: {len(filtered)}") + return filtered + + +def build_kits_for_destruction(df): + mask = ( + df["Basket No."].isna() & + (df["Date Ret"].notna() | (df["Disp Status"].fillna("").str.upper() == "NOT DISPENSED")) + ) + filtered = ( + df[mask] + .copy() + .sort_values(["Site", "Date Ret"], ascending=[True, True]) + .drop(columns=["Destroyed", "Basket No."]) + .reset_index(drop=True) + ) + print(f" Kits for destruction: {len(filtered)}") + return filtered + + +# ── Formátování ─────────────────────────────────────────────────────────────── + +STRIPE_GRAY = PatternFill("solid", start_color="F2F2F2") +STRIPE_WHITE = PatternFill("solid", start_color="FFFFFF") + +# pacienti — styly zachovány z create_subject_report.py +_PAT_HEADER_FILL = PatternFill("solid", start_color="1F4E79") +_PAT_HEADER_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10) +_PAT_NORMAL_FONT = Font(name="Arial", size=10) +_PAT_BOLD_FONT = Font(name="Arial", bold=True, size=10) +_PAT_STRIKE_FONT = Font(name="Arial", size=10, strike=True, color="999999") +_PAT_ADOLESC_FONT = Font(name="Arial", bold=True, size=10) +_PAT_THIN = Side(style="thin", color="CCCCCC") +_PAT_BORDER = Border(left=_PAT_THIN, right=_PAT_THIN, top=_PAT_THIN, bottom=_PAT_THIN) +_PAT_EVEN_FILL = PatternFill("solid", start_color="EBF3FB") +_PAT_ODD_FILL = PatternFill("solid", start_color="FFFFFF") +_PAT_CENTER = Alignment(horizontal="center", vertical="center") +_PAT_LEFT = Alignment(horizontal="left", vertical="center") + + +def _autofit(ws): + for col_cells in ws.columns: + max_len = 0 + col_letter = get_column_letter(col_cells[0].column) + for cell in col_cells: + if cell.value is None: + continue + # datum se zobrazí jako DD-MMM-YYYY = 11 znaků + if hasattr(cell.value, "strftime") or cell.number_format == "DD-MMM-YYYY": + length = 11 + else: + length = len(str(cell.value)) + if length > max_len: + max_len = length + ws.column_dimensions[col_letter].width = min(max_len + 3, 50) + + +def format_sheet(ws, header_color, highlight_col=None, highlight_color=None): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + header_fill = PatternFill("solid", start_color=header_color) + header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) + row_font = Font(name="Arial", size=10) + hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None + + headers = [cell.value for cell in ws[1]] + + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + stripe = STRIPE_GRAY if row[0].row % 2 == 0 else STRIPE_WHITE + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if hi_fill and col_name == highlight_col: + cell.fill = hi_fill + else: + cell.fill = stripe + + _autofit(ws) + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +def format_shipment_sheet(ws, header_color_ship, header_color_detail, n_ship_cols): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10) + dfont = Font(name="Arial", size=10) + fill_ship = PatternFill("solid", start_color=header_color_ship) + fill_detail = PatternFill("solid", start_color=header_color_detail) + + for cell in ws[1]: + cell.fill = fill_ship if cell.column <= n_ship_cols else fill_detail + cell.font = hfont + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = border + ws.row_dimensions[1].height = 30 + + for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + stripe = STRIPE_GRAY if row[0].row % 2 == 0 else STRIPE_WHITE + for cell in row: + cell.font = dfont + cell.border = border + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.fill = stripe + if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"): + cell.number_format = "DD-MMM-YYYY" + + _autofit(ws) + ws.auto_filter.ref = ws.dimensions + ws.freeze_panes = "A2" + + +# ── Pacienti ───────────────────────────────────────────────────────────────── + +def load_patients(study): + db = get_db() + docs = list(db.iwrs_subject_summary.find({"study": study})) + if not docs: + raise RuntimeError(f"Žádná data v Mongo pro pacienty {study}") + + base_cols = [ + ("subject", "Subject"), + ("investigator", "Investigator"), + ("age", "Subject's age collection"), + ("cohort_per_irt", "Cohort per IRT"), + ("irt_subject_status", "IRT Subject Status"), + ("last_irt_transaction", "Last Recorded IRT Transaction"), + ("next_irt_transaction", "Next Expected IRT Transaction"), + ("next_irt_transaction_date_local", "Next Expected IRT Transaction Date [Local]"), + ] + uco_extra = [ + ("rescreened_subject", "Rescreened Subject"), + ("adt_ir", "ADT-IR"), + ("three_or_more_advanced_therapies", "3+ Adv. Therapies"), + ("only_oral_5asa_compounds", "Only 5-ASA"), + ("ustekinumab", "Ustekinumab"), + ("isolated_proctitis", "Isolated Proctitis"), + ] + cols = list(base_cols) + if study == "77242113UCO3001": + cols += uco_extra + + rows = [{label: d.get(key) for key, label in cols} for d in docs] + df = pd.DataFrame(rows).sort_values("Subject").reset_index(drop=True) + + if "Next Expected IRT Transaction Date [Local]" in df.columns: + df["Next Expected IRT Transaction Date [Local]"] = pd.to_datetime( + df["Next Expected IRT Transaction Date [Local]"], errors="coerce" + ) + print(f" Pacienti: {len(df)} subjektů") + return df + + +def _simplify_cohort(val): + if pd.isna(val): + return "" + val = str(val) + if "dolescent" in val: + return "Adolescent" + if val.startswith("Adult"): + return "Adult" + return val + + +def _fmt_date(val): + if pd.isna(val): + return "" + if hasattr(val, "strftime"): + return val.strftime("%Y-%m-%d") + return str(val)[:10] + + +def _write_prehled(wb, df_raw, study): + ws = wb.create_sheet("Přehled", 0) + ws.sheet_view.showGridLines = False + + is_uco = (study == "77242113UCO3001") + + if is_uco: + display_headers = ["Subject", "Investigator", "Věk", "Cohort", + "Rescreened", "ADT-IR", "≥3 Adv.Th.", "5-ASA only", + "Uste.", "Isol.Proct.", + "Status", "Last IRT", "Next Visit", "Next Date"] + col_widths = [14, 22, 6, 12, 11, 8, 11, 10, 8, 12, 14, 12, 12, 13] + status_col = 11 + flag_cols = set(range(5, 11)) # 1-indexed sloupce s Yes/No hodnotami + else: + display_headers = ["Subject", "Investigator", "Věk", "Cohort", "Status", "Last IRT", "Next Visit", "Next Date"] + col_widths = [14, 22, 6, 12, 14, 12, 12, 13] + status_col = 5 + flag_cols = set() + + last_col = get_column_letter(len(display_headers)) + ws.merge_cells(f"A1:{last_col}1") + title = ws["A1"] + title.value = f"Subject Summary — {study} ({date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + for c, (h, w) in enumerate(zip(display_headers, col_widths), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = _PAT_HEADER_FONT + cell.fill = _PAT_HEADER_FILL + cell.alignment = _PAT_CENTER + cell.border = _PAT_BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + base = { + "Subject": df_raw["Subject"].fillna(""), + "Investigator": df_raw["Investigator"].fillna(""), + "Věk": df_raw["Subject's age collection"].apply(lambda v: "" if pd.isna(v) else int(v)), + "Cohort": df_raw["Cohort per IRT"].apply(_simplify_cohort), + } + if is_uco: + base.update({ + "Rescreened": df_raw["Rescreened Subject"].fillna(""), + "ADT-IR": df_raw["ADT-IR"].fillna(""), + "≥3 Adv.Th.": df_raw["3+ Adv. Therapies"].fillna(""), + "5-ASA only": df_raw["Only 5-ASA"].fillna(""), + "Uste.": df_raw["Ustekinumab"].fillna(""), + "Isol.Proct.": df_raw["Isolated Proctitis"].fillna(""), + }) + base.update({ + "Status": df_raw["IRT Subject Status"].fillna(""), + "Last IRT": df_raw["Last Recorded IRT Transaction"].fillna("—"), + "Next Visit": df_raw["Next Expected IRT Transaction"].fillna("—"), + "Next Date": df_raw["Next Expected IRT Transaction Date [Local]"].apply(_fmt_date), + }) + display = pd.DataFrame(base).sort_values("Subject").reset_index(drop=True) + + for r_idx, row in display.iterrows(): + excel_row = r_idx + 3 + status = str(row["Status"]) + is_failed = "Screen Failed" in status or "Discontinued" in status + is_randomized = "Randomized" in status + is_adolescent = row["Cohort"] == "Adolescent" + fill = _PAT_EVEN_FILL if r_idx % 2 == 0 else _PAT_ODD_FILL + + for c_idx, val in enumerate(row, 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = _PAT_BORDER + cell.alignment = _PAT_CENTER if (c_idx == 3 or c_idx in flag_cols) else _PAT_LEFT + if is_failed: + cell.font = _PAT_STRIKE_FONT + elif c_idx == status_col and is_randomized: + cell.font = _PAT_BOLD_FONT + elif c_idx == 4 and is_adolescent: + cell.font = _PAT_ADOLESC_FONT + else: + cell.font = _PAT_NORMAL_FONT + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + ws.auto_filter.ref = f"A2:{last_col}{len(display) + 2}" + + +def _write_next_visits(wb, df_raw, study, visits_df=None): + ws = wb.create_sheet("Next Visits", 1) + ws.sheet_view.showGridLines = False + + ws.merge_cells("A1:D1") + title = ws["A1"] + title.value = f"Next Expected Visits — {study} ({date.today().strftime('%d-%b-%Y')})" + title.font = Font(name="Arial", bold=True, size=12, color="1F4E79") + title.alignment = Alignment(horizontal="left", vertical="center") + ws.row_dimensions[1].height = 22 + + nv_headers = ["Subject", "Investigator", "Next Visit", "Datum"] + nv_widths = [14, 22, 26, 13] + for c, (h, w) in enumerate(zip(nv_headers, nv_widths), 1): + cell = ws.cell(row=2, column=c, value=h) + cell.font = _PAT_HEADER_FONT + cell.fill = _PAT_HEADER_FILL + cell.alignment = _PAT_CENTER + cell.border = _PAT_BORDER + ws.column_dimensions[get_column_letter(c)].width = w + ws.row_dimensions[2].height = 18 + + df = pd.DataFrame({ + "Subject": df_raw["Subject"].fillna(""), + "Investigator": df_raw["Investigator"].fillna(""), + "Next Visit": df_raw["Next Expected IRT Transaction"].fillna(""), + "Datum": df_raw["Next Expected IRT Transaction Date [Local]"], + "Status": df_raw["IRT Subject Status"].fillna(""), + }) + + # I-0: datum = screening date + 42 dní + if visits_df is not None and not visits_df.empty: + screen = ( + visits_df[visits_df["Visit"].str.contains("Screen", case=False, na=False)] + .groupby("Subject")["Visit Date"].min() + .rename("Screening Date") + ) + df = df.join(screen, on="Subject") + mask_i0 = df["Next Visit"].str.contains("I-0", na=False) + df.loc[mask_i0, "Datum"] = df.loc[mask_i0, "Screening Date"] + pd.Timedelta(days=42) + df = df.drop(columns=["Screening Date"]) + + df = df[df["Datum"].notna()] + df = df[~df["Status"].str.contains("Screen Failed|Discontinued", na=False)] + df = df.sort_values("Datum").reset_index(drop=True) + + for r_idx, row in df.iterrows(): + excel_row = r_idx + 3 + fill = _PAT_EVEN_FILL if r_idx % 2 == 0 else _PAT_ODD_FILL + datum_val = row["Datum"] + datum_str = datum_val.strftime("%Y-%m-%d") if hasattr(datum_val, "strftime") else str(datum_val)[:10] + for c_idx, val in enumerate([row["Subject"], row["Investigator"], row["Next Visit"], datum_str], 1): + cell = ws.cell(row=excel_row, column=c_idx, value=val if val != "" else None) + cell.fill = fill + cell.border = _PAT_BORDER + cell.font = _PAT_NORMAL_FONT + cell.alignment = _PAT_LEFT + ws.row_dimensions[excel_row].height = 16 + + ws.freeze_panes = "A3" + ws.auto_filter.ref = f"A2:D{len(df) + 2}" + + +# ── Jeden report pro jednu studii ───────────────────────────────────────────── + +def create_study_report(study): + today = date.today() + output_file = unique_path(REPORTS_DIR, f"{today} {study} IWRS report") + + print(f"\n[{study}] Nacitam z MongoDB...") + df = load_inventory(study) + shipments_df = load_shipments(study) + df_patients = load_patients(study) + visits_df = load_visits(study) + + expired_df, expired_sheet = build_expired(df) + assigned_df = build_assigned_not_dispensed(df) + not_returned_df = build_not_returned(df) + destruction_df = build_kits_for_destruction(df) + site_summary_df = build_site_summary(shipments_df) + + with pd.ExcelWriter(output_file, engine="openpyxl") as writer: + df.to_excel( writer, index=False, sheet_name="CountryMedicationOverview") + expired_df.to_excel( writer, index=False, sheet_name=expired_sheet) + assigned_df.to_excel( writer, index=False, sheet_name="Assigned not dispensed") + not_returned_df.to_excel( writer, index=False, sheet_name="Not returned") + destruction_df.to_excel( writer, index=False, sheet_name="Kits for destruction") + shipments_df.to_excel( writer, index=False, sheet_name="Shipments") + site_summary_df.to_excel( writer, index=False, sheet_name="Site Summary") + visits_df.to_excel( writer, index=False, sheet_name="Patient Visits") + + wb = load_workbook(output_file) + + ws_main = wb["CountryMedicationOverview"] + format_sheet(ws_main, header_color="1F4E79") + green_fill = PatternFill("solid", start_color="E2EFDA") + headers_main = [c.value for c in ws_main[1]] + for row in ws_main.iter_rows(min_row=2, max_row=ws_main.max_row): + for cell in row: + col_name = headers_main[cell.column - 1] if cell.column <= len(headers_main) else None + if col_name in ("Destroyed", "Basket No."): + cell.fill = green_fill + + format_sheet(wb[expired_sheet], header_color="C00000", highlight_col="Exp Date", highlight_color="FFE0E0") + format_sheet(wb["Assigned not dispensed"], header_color="833C00", highlight_col="Subject ID", highlight_color="FFF2CC") + format_sheet(wb["Not returned"], header_color="375623", highlight_col="Max Visit Date", highlight_color="E2EFDA") + format_sheet(wb["Kits for destruction"], header_color="595959") + format_shipment_sheet(wb["Shipments"], "1F4E79", "375623", N_SHIP_COLS) + format_sheet(wb["Site Summary"], header_color="1F4E79") + format_sheet(wb["Patient Visits"], header_color="1F4E79") + + # ── pacienti (Přehled + Next Visits) na začátek ────────────────────────── + _write_prehled(wb, df_patients, study) + _write_next_visits(wb, df_patients, study, visits_df) + + # ── pořadí listů: Přehled → Next Visits → Patient Visits → léky ────────── + first = ["Přehled", "Next Visits", "Patient Visits"] + rest = [s for s in wb.sheetnames if s not in first] + wb._sheets = [wb[s] for s in first] + [wb[s] for s in rest] + + wb.save(output_file) + print(f" Uloženo: {os.path.basename(output_file)} ({len(df)} řádků skladu)") + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + ap = argparse.ArgumentParser(description="IWRS report: pacienti + léky v jednom Excelu (z MongoDB)") + ap.add_argument("--study", choices=STUDIES, help="jen jedna studie") + args = ap.parse_args() + + os.makedirs(REPORTS_DIR, exist_ok=True) + for study in ([args.study] if args.study else STUDIES): + try: + create_study_report(study) + except Exception as e: + import traceback + print(f"\n[{study}] CHYBA: {e}") + traceback.print_exc() + print("\nHotovo.") + + +if __name__ == "__main__": + main() diff --git a/IWRS/download_drugs.py b/IWRS/download_drugs.py new file mode 100644 index 0000000..c1db9fb --- /dev/null +++ b/IWRS/download_drugs.py @@ -0,0 +1,219 @@ +""" +download_drugs.py — stažení Drugs reportů pro jednu studii do IWRS/Incoming/. +Verze: 1.1 | Datum: 2026-06-10 + v1.1: přesun na úroveň IWRS/ + +Volá se z IWRS/run_all_v1.1.py s již přihlášenou Playwright page (login + +výběr studie zajišťuje common.iwrs_portal.login). + + 1. Onsite inventory detail (per site, stahuje se vždy) + 2. IP destruction (per košík; přeskočí košíky už importované + v Mongo iwrs_destruction — destrukce se nemění) + 3. Shipments report (jeden soubor na studii, stahuje se vždy) + 4. Shipment details (per CZ zásilka; přeskočí zásilky, jejichž + položky jsou v Mongo iwrs_shipment_items se + statusem RECEIVED — finální stav) + +Názvy souborů (datumované, aby zapadly do Incoming/ flow): + YYYY-MM-DD {study} Onsite Inventory {site}.xlsx + YYYY-MM-DD {study} IP Destruction {basket}.xlsx + YYYY-MM-DD {study} Shipments Report.xlsx + YYYY-MM-DD {study} Shipment Details {shipment_id}.xlsx +""" + +import os +import sys +import datetime + +import pandas as pd + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from common.iwrs_portal import BASE_URL +from common.paths import INCOMING_DIR, unique_path +from common.mongo_writer import get_db + +SITES = { + "77242113UCO3001": [ + "DD5-CZ10001", "DD5-CZ10003", "DD5-CZ10006", "DD5-CZ10009", + "DD5-CZ10010", "DD5-CZ10012", "DD5-CZ10013", "DD5-CZ10015", + "DD5-CZ10016", "DD5-CZ10020", "DD5-CZ10021", "DD5-CZ10022", + ], + "42847922MDD3003": [ + "S10-CZ10002", "S10-CZ10004", "S10-CZ10005", + "S10-CZ10008", "S10-CZ10011", "S10-CZ10012", + ], +} + + +def _today(): + return datetime.date.today().strftime("%Y-%m-%d") + + +# ── skip-logika přes Mongo (náhrada za dřívější "soubor existuje") ─────────── + +def get_existing_baskets(study): + """Košíky už importované v iwrs_destruction — destrukce je immutable.""" + try: + db = get_db() + return set(db.iwrs_destruction.distinct("basket_id", {"study": study})) + except Exception as e: + print(f" UPOZORNĚNÍ: nelze načíst košíky z Mongo ({e}), stahuji vše") + return set() + + +def get_received_shipments(study): + """Zásilky, jejichž položky už jsou v Mongo se statusem RECEIVED (finální stav).""" + try: + db = get_db() + return set(db.iwrs_shipment_items.distinct( + "shipment_id", + {"study": study, "shipment_status": {"$regex": "^received$", "$options": "i"}}, + )) + except Exception as e: + print(f" UPOZORNĚNÍ: nelze načíst zásilky z Mongo ({e}), stahuji vše") + return set() + + +# ── download funkce ────────────────────────────────────────────────────────── + +def download_inventory(page, study): + today = _today() + page.goto(f"{BASE_URL}/report/onsite_inventory_detail") + page.wait_for_load_state("networkidle", timeout=120000) + + for site_id in SITES[study]: + print(f" [{site_id}] inventory...") + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.get_by_role("option", name=site_id).click() + page.wait_for_load_state("networkidle", timeout=120000) + + filename = unique_path(INCOMING_DIR, f"{today} {study} Onsite Inventory {site_id}") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + print(f" Inventory OK ({len(SITES[study])} center)") + + +def download_destruction(page, study): + today = _today() + page.goto(f"{BASE_URL}/report/ip_destruction_form") + page.wait_for_load_state("networkidle", timeout=120000) + + page.locator('input[placeholder="search"], input[type="text"]').first.click() + page.wait_for_timeout(1000) + baskets = [b.strip() for b in page.locator("mat-option").all_inner_texts() + if b.strip() and b.strip() != "No results found"] + page.keyboard.press("Escape") + page.wait_for_timeout(500) + + if not baskets: + print(" Žádné destruction košíky") + return + + existing = get_existing_baskets(study) + new_count = 0 + for basket in baskets: + if basket in existing: + continue # destrukce se nemění — přeskočit + print(f" [košík {basket}] stahování...") + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(basket) + page.wait_for_timeout(500) + page.locator("mat-option").first.dispatch_event("click") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = unique_path(INCOMING_DIR, f"{today} {study} IP Destruction {basket}") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + new_count += 1 + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(f" Destruction OK ({new_count} nových, {len(baskets) - new_count} přeskočeno)") + + +def download_shipments_report(page, study): + today = _today() + page.goto(f"{BASE_URL}/report/shipments_report") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = unique_path(INCOMING_DIR, f"{today} {study} Shipments Report") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" Shipments report OK -> {os.path.basename(filename)}") + return filename + + +def download_shipment_details(page, study, shipments_report_path): + today = _today() + + # načti CZ shipment IDs z právě staženého shipments reportu + raw = pd.read_excel(shipments_report_path, header=None) + header_row = None + for i, row in raw.iterrows(): + if "Shipment ID" in [str(v).strip() for v in row]: + header_row = i + break + df = pd.read_excel(shipments_report_path, header=header_row) + df = df.dropna(how="all") + df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)] + cz_shipments = list(zip( + df["Shipment ID"].astype(str).str.strip(), + df["IRT Shipment Status"].astype(str).str.strip() if "IRT Shipment Status" in df.columns else [""] * len(df), + )) + print(f" CZ zásilek celkem: {len(cz_shipments)}") + + received = get_received_shipments(study) + + page.goto(f"{BASE_URL}/report/shipment_details_report") + page.wait_for_load_state("networkidle", timeout=120000) + + skipped = 0 + for shipment, status in cz_shipments: + if shipment in received: + skipped += 1 + continue # položky v Mongo už mají finální stav RECEIVED + input_field = page.locator('input[placeholder="search"], input[type="text"]').first + input_field.click() + input_field.fill(shipment) + page.wait_for_timeout(500) + page.locator("mat-option").first.dispatch_event("click") + page.wait_for_load_state("networkidle", timeout=120000) + + filename = unique_path(INCOMING_DIR, f"{today} {study} Shipment Details {shipment}") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{shipment}] ({status}) OK") + + page.get_by_role("button", name="Clear").click() + page.wait_for_load_state("networkidle", timeout=120000) + + print(f" Přeskočeno (RECEIVED v Mongo): {skipped}") + + +def run(page, study): + """Stáhne všechny 4 typy Drugs reportů pro studii do IWRS/Incoming/.""" + os.makedirs(INCOMING_DIR, exist_ok=True) + + print("\n [1/4] Onsite inventory...") + download_inventory(page, study) + + print("\n [2/4] IP destruction...") + download_destruction(page, study) + + print("\n [3/4] Shipments report...") + report_path = download_shipments_report(page, study) + + print("\n [4/4] Shipment details (CZ)...") + download_shipment_details(page, study, report_path) diff --git a/IWRS/download_patients.py b/IWRS/download_patients.py new file mode 100644 index 0000000..3279257 --- /dev/null +++ b/IWRS/download_patients.py @@ -0,0 +1,47 @@ +""" +download_patients.py — stažení pacientských reportů pro jednu studii. +Verze: 1.1 | Datum: 2026-06-10 + v1.1: přesun na úroveň IWRS/ + +Volá se z IWRS/run_all_v1.1.py s již přihlášenou Playwright page (login + +výběr studie zajišťuje common.iwrs_portal.login). + + 1. Subject Summary Report + 2. Subject Detail Reports + notifikace PDF+JSON (per subjekt, jen nové dle pk v Mongo) + +Vše se ukládá ploše do IWRS/Incoming/ s datumovanými názvy. +""" + +import os +import sys +import datetime + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from common.iwrs_portal import BASE_URL +from common.paths import INCOMING_DIR, unique_path + +import download_subject_details as dsd + + +def download_summary(page, study, today): + print(f" [{study}] Stahuji Subject Summary Report...") + page.goto(f"{BASE_URL}/report/patient_summary_report") + page.wait_for_load_state("networkidle", timeout=120000) + filename = unique_path(INCOMING_DIR, f"{today} {study} Subject Summary Report") + with page.expect_download(timeout=120000) as dl: + page.get_by_role("button", name="Download XLS").click() + dl.value.save_as(filename) + print(f" [{study}] Summary OK -> {os.path.basename(filename)}") + return filename + + +def run(page, study): + """Stáhne summary + detaily + notifikace pro studii do IWRS/Incoming/.""" + os.makedirs(INCOMING_DIR, exist_ok=True) + today = datetime.date.today().strftime("%Y-%m-%d") + download_summary(page, study, today) + # detail XLSX + notifikace přímo do Incoming/ (flat názvy se study+subject) + dsd.run(page, study, out_dir=INCOMING_DIR, subjects_source_dir=INCOMING_DIR) diff --git a/IWRS/import_drugs.py b/IWRS/import_drugs.py new file mode 100644 index 0000000..ddbf91e --- /dev/null +++ b/IWRS/import_drugs.py @@ -0,0 +1,309 @@ +""" +import_drugs.py — import Drugs reportů z IWRS/Incoming/ do MongoDB. +Verze: 1.2 | Datum: 2026-06-10 + v1.1: prázdný inventory report (centrum bez zásob — jen meta řádky, bez + tabulky léků) se bere jako 0 položek a archivuje se do Processed/ + v1.2: přesun na úroveň IWRS/ + +Nahrazuje Drugs/import_to_mongo.py (ten parsoval pevné adresáře xls_*; +nyní se parsují datumované soubory z IWRS/Incoming/). + +Per studie a běh: jeden import_id. Soubory se zpracují nejstarší napřed, +při více souborech stejného záznamu vyhrává poslední (poslední stav). +Po úspěšném zápisu do Monga se zparsované soubory přesunou do +IWRS/Incoming/Processed/; soubor s chybou parsování zůstává v Incoming/. + +Cílové kolekce (db `studie`): + iwrs_shipments / iwrs_shipment_items / iwrs_inventory (upsert + snapshot) + iwrs_destruction (upsert only, immutable) + +Volá se z IWRS/run_all_v1.0.py (ensure_indexes volá orchestrátor); +lze spustit i samostatně: python import_drugs.py +""" + +import os +import re +import sys +import glob + +import pandas as pd + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from common.paths import INCOMING_DIR, STUDIES, move_done, sorted_by_mtime +from common.mongo_writer import ( + to_str, to_int, to_date, + ensure_indexes, log_import, + bulk_upsert_with_snapshot, bulk_upsert_only, +) + + +def _pending(pattern): + return sorted_by_mtime(glob.glob(os.path.join(INCOMING_DIR, pattern))) + + +def _find_header_row(raw, marker): + for i, row in raw.iterrows(): + if marker in [str(v).strip() for v in row]: + return i + return None + + +# ── XLSX parsery (per soubor) ──────────────────────────────────────────────── + +def parse_shipments_file(path, study): + raw = pd.read_excel(path, header=None) + header_row = _find_header_row(raw, "Shipment ID") + if header_row is None: + raise ValueError("hlavičkový řádek 'Shipment ID' nenalezen") + df = pd.read_excel(path, header=header_row).dropna(how="all") + df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)] + col = df.columns.tolist() + rows = [] + for _, r in df.iterrows(): + sid = to_str(r["Shipment ID"]) + if not sid: + continue + rows.append({ + "_id": sid, + "shipment_id": sid, + "study": study, + "status": to_str(r["IRT Shipment Status"]), + "type": to_str(r["Type"]), + "ship_from": to_str(r["Shipment From"]), + "ship_to_site": to_str(r["Ship To:"]), + "location": to_str(r["Location"]), + "request_date": to_date(r["Request Date"]), + "shipped_date": to_date(r["Shipped Date"]), + "received_date": to_date(r["Received Date"]) if "Received Date" in col else None, + "received_by": to_str(r["Received by"]) if "Received by" in col else None, + "delivered_date_utc": to_date(r["Delivered Date [UTC]"]) if "Delivered Date [UTC]" in col else None, + "delivery_recipient": to_str(r["Delivery Recipient"]) if "Delivery Recipient" in col else None, + "delivery_details": to_str(r["Delivery Details"]) if "Delivery Details" in col else None, + "cancelled_date": to_date(r["Cancelled Date"]) if "Cancelled Date" in col else None, + "total_medication_ids": to_int(r["Total Medication IDs"]) if "Total Medication IDs" in col else None, + "tracking_no": to_str(r["Tracking #"]) if "Tracking #" in col else None, + "shipping_category": to_str(r["Shipping Category"]) if "Shipping Category" in col else None, + "expected_arrival": to_date(r["Expected Arrival"]) if "Expected Arrival" in col else None, + }) + return rows + + +def parse_shipment_details_file(path, study): + # shipment_id z názvu: "... Shipment Details {id}[ HHMM].xlsx" + m = re.search(r"Shipment Details (\S+?)(?: \d{4})?\.xlsx$", os.path.basename(path)) + shipment_id = m.group(1) if m else "UNKNOWN" + raw = pd.read_excel(path, header=None) + header_row = _find_header_row(raw, "Medication ID") + if header_row is None: + raise ValueError("hlavičkový řádek 'Medication ID' nenalezen") + df = pd.read_excel(path, header=header_row).dropna(how="all") + rows = [] + for _, r in df.iterrows(): + med_desc = (to_str(r.get("Medication Description")) + or to_str(r.get("Medication ID Description"))) + med_type = (to_str(r.get("Medication type")) + or to_str(r.get("Medication ID type"))) + med_id = to_str(r.get("Medication ID")) + if not med_id: + continue + rows.append({ + "_id": f"{shipment_id}:{med_id}", + "study": study, + "shipment_id": shipment_id, + "destination_location": to_str(r.get("Destination Location")), + "shipment_status": to_str(r.get("IRT Shipment Status")), + "shipment_type": to_str(r.get("Type")), + "destination_site": to_str(r.get("Destination Site")), + "investigator": to_str(r.get("Investigator")), + "medication_description": med_desc, + "medication_type": med_type, + "medication_id": med_id, + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "container_id": to_str(r.get("Container ID")), + "quantity": to_int(r.get("Quantity of Medication IDs")), + "expiration_date": to_date(r.get("Expiration Date")), + "item_status": to_str(r.get("Status")), + }) + return rows + + +def parse_inventory_file(path, study): + raw = pd.read_excel(path, header=None) + site = investigator = location = None + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + if first.startswith("Site:"): + site = first.replace("Site:", "").strip() + elif first.startswith("Investigator:"): + investigator = first.replace("Investigator:", "").strip() + elif first.startswith("Location:"): + location = first.replace("Location:", "").strip() + if first in ("Medication", "Medication ID") and header_row is None: + header_row = i + if header_row is None: + if site: + return [] # centrum bez zásob — report má jen meta řádky, žádnou tabulku + raise ValueError("hlavičkový řádek 'Medication' nenalezen") + df = pd.read_excel(path, header=header_row).dropna(how="all") + df = df.rename(columns={df.columns[0]: "medication_id"}) + rows = [] + for _, r in df.iterrows(): + med_id = to_str(r["medication_id"]) + if not med_id or not site: + continue + rows.append({ + "_id": f"{site}:{med_id}", + "study": study, + "site": site, + "investigator": investigator, + "location": location, + "medication_id": med_id, + "packaged_lot_no": to_str(r.get("Packaged Lot number")), + "original_expiration_date": to_date(r.get("Original Expiration Date when Packaged Lot was Added")), + "expiration_date": to_date(r.get("Expiration date")), + "received_date": to_date(r.get("Received Date")), + "receipt_user": to_str(r.get("Shipment Receipt User")), + "subject_identifier": to_str(r.get("Subject Identifier")), + "quantity_assigned": to_int(r.get("Quantity Assigned")), + "irt_transaction": to_str(r.get("IRT Transaction")), + "date_assigned": to_date(r.get("Date Assigned")), + "assignment_user": to_str(r.get("Assignment User")), + "dispensation_status": to_str(r.get("Dispensation Status")), + "dispensing_date": to_date(r.get("Dispensing date") or r.get("Dispensing Date")), + "quantity_dispensed": to_int(r.get("Quantity Dispensed")), + "dispensing_user": to_str(r.get("Dispensing User")), + "quantity_returned": to_int(r.get("Quantity Returned")), + "date_returned": to_date(r.get("Date Returned")), + "return_user": to_str(r.get("Return User")), + }) + return rows + + +def parse_destruction_file(path, study): + raw = pd.read_excel(path, header=None) + meta = {} + header_row = None + for i, row in raw.iterrows(): + first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else "" + for key, attr in [ + ("Investigator Name:", "investigator"), + ("Site ID:", "site_id"), + ("Location:", "location"), + ("Basket ID:", "basket_id"), + ("Drug Destruction Created Date:", "destruction_date"), + ]: + if first.startswith(key): + meta[attr] = first.replace(key, "").strip() + if first == "Medication ID Description" and header_row is None: + header_row = i + if header_row is None: + raise ValueError("hlavičkový řádek 'Medication ID Description' nenalezen") + df = pd.read_excel(path, header=header_row).dropna(how="all") + basket_id = meta.get("basket_id") + rows = [] + for _, r in df.iterrows(): + med_id = to_str(r.get("Medication ID")) + if not med_id or not basket_id: + continue + rows.append({ + "_id": f"{basket_id}:{med_id}", + "study": study, + "site_id": meta.get("site_id"), + "investigator": meta.get("investigator"), + "location": meta.get("location"), + "basket_id": basket_id, + "destruction_date": to_date(meta.get("destruction_date")), + "medication_description": to_str(r.get("Medication ID Description")), + "medication_id": med_id, + "packaged_lot_description": to_str(r.get("Packaged Lot description")), + "comments": to_str(r.get("Comments")), + }) + return rows + + +# ── zpracování souborů ─────────────────────────────────────────────────────── + +def _parse_files(files, parser, study, label): + """Zparsuje soubory (nejstarší napřed, poslední vyhrává per _id). + + Vrací (docs, ok_paths, failed_paths). + """ + docs, ok, failed = {}, [], [] + for path in files: + try: + for d in parser(path, study): + docs[d["_id"]] = d + ok.append(path) + except Exception as e: + failed.append(path) + print(f" [{study}] CHYBA parsování {label} {os.path.basename(path)}: {e}") + return list(docs.values()), ok, failed + + +def import_study(study): + ship_files = _pending(f"* {study} Shipments Report*.xlsx") + item_files = _pending(f"* {study} Shipment Details *.xlsx") + inv_files = _pending(f"* {study} Onsite Inventory *.xlsx") + dest_files = _pending(f"* {study} IP Destruction *.xlsx") + + if not (ship_files or item_files or inv_files or dest_files): + print(f" [{study}] drugs: nic ke zpracování") + return + + shipments, ok_ship, _ = _parse_files(ship_files, parse_shipments_file, study, "shipments") + items, ok_item, _ = _parse_files(item_files, parse_shipment_details_file, study, "details") + inventory, ok_inv, _ = _parse_files(inv_files, parse_inventory_file, study, "inventory") + destruct, ok_dest, _ = _parse_files(dest_files, parse_destruction_file, study, "destruction") + + ok_files = ok_ship + ok_item + ok_inv + ok_dest + if not ok_files: + print(f" [{study}] drugs: žádný soubor se nepodařilo zparsovat") + return + + print(f" [{study}] Zásilky: {len(shipments)} | Položky: {len(items)} | " + f"Sklad: {len(inventory)} | Destrukce: {len(destruct)}") + + import_id = log_import(study, f"drugs_{study}", "drugs", { + "shipments": len(shipments), + "shipment_items": len(items), + "inventory": len(inventory), + "destruction": len(destruct), + }) + print(f" [{study}] import_id = {import_id}") + + bulk_upsert_with_snapshot("iwrs_shipments", "iwrs_shipments_snapshots", shipments, import_id) + bulk_upsert_with_snapshot("iwrs_shipment_items", "iwrs_shipment_items_snapshots", items, import_id) + bulk_upsert_with_snapshot("iwrs_inventory", "iwrs_inventory_snapshots", inventory, import_id) + bulk_upsert_only("iwrs_destruction", destruct, import_id) + + # zápis do Monga prošel → archivovat zdrojové soubory + for path in ok_files: + move_done(path) + print(f" [{study}] drugs: {len(ok_files)} soubor(ů) přesunuto do Processed") + + +def run(studies=None): + studies = studies or STUDIES + if not os.path.isdir(INCOMING_DIR): + print(f"Adresář neexistuje: {INCOMING_DIR}") + return + print("=" * 60) + print("Import Drugs (shipments / items / inventory / destruction)") + print("=" * 60) + for study in studies: + try: + import_study(study) + except Exception as e: + import traceback + print(f" [{study}] CHYBA importu drugs: {e}") + traceback.print_exc() + + +if __name__ == "__main__": + ensure_indexes() + run(sys.argv[1:] or None) diff --git a/IWRS/import_patients.py b/IWRS/import_patients.py new file mode 100644 index 0000000..776498d --- /dev/null +++ b/IWRS/import_patients.py @@ -0,0 +1,89 @@ +""" +import_patients.py — import pacientských reportů z IWRS/Incoming/ do MongoDB. +Verze: 1.1 | Datum: 2026-06-10 + v1.1: přesun na úroveň IWRS/, worker přejmenován na import_patients_mongo + +Pořadí zpracování per typ + studie: nejstarší soubor podle mtime první +(důležité pro chronologickou správnost snapshotů). + +Po úspěšném importu se soubor přesune do IWRS/Incoming/Processed/. +Při chybě zůstane soubor v Incoming/. + +Volá se z IWRS/run_all_v1.1.py (ensure_indexes volá orchestrátor); +lze spustit i samostatně: python import_patients.py +""" + +import os +import sys +import glob + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from common.paths import INCOMING_DIR, PROCESSED_DIR, STUDIES, move_done, sorted_by_mtime +from common.mongo_writer import ensure_indexes + +import import_patients_mongo +import import_notifications_to_mongo + + +def import_summaries(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} Subject Summary Report*.xlsx") + files = sorted_by_mtime(glob.glob(pattern)) + if not files: + print(f" [{study}] summary: nic ke zpracování") + return + print(f" [{study}] summary: {len(files)} soubor(ů) (oldest first)") + for path in files: + try: + import_patients_mongo.import_subject_summary(study, path) + move_done(path) + except Exception as e: + print(f" [{study}] CHYBA summary {os.path.basename(path)}: {e}") + + +def import_details(study): + pattern = os.path.join(INCOMING_DIR, f"* {study} * Subject Detail.xlsx") + files = sorted_by_mtime(glob.glob(pattern)) + if not files: + print(f" [{study}] detail: nic ke zpracování") + return + print(f" [{study}] detail: {len(files)} soubor(ů) (oldest first)") + for path in files: + parsed = import_patients_mongo.parse_detail_filename(path) + if not parsed: + print(f" [{study}] PŘESKAKUJI (nelze parsovat název): {os.path.basename(path)}") + continue + _, parsed_study, subject = parsed + if parsed_study != study: + continue # patří jiné studii + try: + import_patients_mongo.import_visits_single_file(study, subject, path) + move_done(path) + except Exception as e: + print(f" [{study}] CHYBA detail {os.path.basename(path)}: {e}") + + +def run(studies=None): + studies = studies or STUDIES + if not os.path.isdir(INCOMING_DIR): + print(f"Adresář neexistuje: {INCOMING_DIR}") + return + + print("=" * 60) + print("Import Subject Summary + Visits") + print("=" * 60) + for study in studies: + import_summaries(study) + import_details(study) + + print("\n" + "=" * 60) + print("Import notifikací") + print("=" * 60) + import_notifications_to_mongo.import_from_dir(INCOMING_DIR, PROCESSED_DIR, studies) + + +if __name__ == "__main__": + ensure_indexes() + run() diff --git a/IWRS/run_all_v1.1.md b/IWRS/run_all_v1.1.md new file mode 100644 index 0000000..0936103 --- /dev/null +++ b/IWRS/run_all_v1.1.md @@ -0,0 +1,132 @@ +# run_all_v1.1.py — IWRS: kompletní pipeline Pacienti + Léky + +**Verze:** 1.1 | **Datum:** 2026-06-10 + +- **v1.1:** všechny moduly pipeline přesunuty na úroveň `IWRS/` — adresáře + `Patients/` a `Drugs/` už pipeline nepotřebuje. Worker přejmenován: + `Patients/import_to_mongo.py` → `import_patients_mongo.py`; parsery notifikací + vyčleněny do `notification_parsers.py` (bez závislosti na MySQL). +- **v1.0:** první verze sjednocené pipeline (nahradila `Drugs/run_all.py` + a `Patients/download_all.py`+`import_all.py`). + +Jeden vstupní skript na úrovni `IWRS/`, který stáhne z janssen.4gclinical.com +a naimportuje do MongoDB (db `studie`) data pacientů i léků pro obě studie +(77242113UCO3001, 42847922MDD3003). + +## Tok souborů + +``` +IWRS/Incoming/ ← sem padá vše stažené (pacienti i léky, datumované názvy) +IWRS/Incoming/Processed/ ← sem se přesouvá po úspěšném importu +``` + +- Při chybě importu soubor **zůstává v Incoming/** a zpracuje se při příštím běhu. +- Import jde vždy **nejstarší soubor napřed** (mtime) — chronologická správnost snapshotů. +- Kolize jména v Processed/ → přepíše se (Mongo už data má, soubor je jen archiv). +- Adresář `IWRS/Incoming/` je v `.gitignore`. +- Původní adresáře `Drugs/xls_*` zůstávají zmrazené na místě jako archiv — nový kód je nepoužívá. + +## Názvy souborů v Incoming/ + +| Typ | Vzor | +|---|---| +| Subject Summary | `YYYY-MM-DD {study} Subject Summary Report.xlsx` | +| Subject Detail | `YYYY-MM-DD {study} {subject} Subject Detail.xlsx` | +| Notifikace | `{datum}_{study}_{subject}_{label}.pdf` + `.json` | +| Onsite Inventory | `YYYY-MM-DD {study} Onsite Inventory {site}.xlsx` | +| IP Destruction | `YYYY-MM-DD {study} IP Destruction {basket}.xlsx` | +| Shipments Report | `YYYY-MM-DD {study} Shipments Report.xlsx` | +| Shipment Details | `YYYY-MM-DD {study} Shipment Details {shipment_id}.xlsx` | + +Při kolizi (druhý běh ve stejný den) se před příponu přidá ` HHMM`. +Metadata (site, basket, study) se při importu čtou primárně z **obsahu** souboru; +z názvu se bere jen `shipment_id` u Shipment Details. + +## Průběh + +### Fáze 1 — stahování (2 přihlášení, per studie jedna browser session) + +1. Login + výběr studie (`common/iwrs_portal.py`) +2. **Pacienti** (`download_patients.py`): + - Subject Summary Report + - per subjekt: Subject Detail XLSX + notifikace PDF+JSON (stahují se jen + notifikace, jejichž `pk` ještě není v Mongo `iwrs_notifications`) +3. **Léky** (`download_drugs.py`): + - Onsite Inventory — všechna centra, vždy znovu + - IP Destruction — přeskočí košíky už importované v `iwrs_destruction` + (destrukce je immutable) + - Shipments Report — vždy znovu + - Shipment Details — jen CZ zásilky; přeskočí zásilky, jejichž položky + jsou v `iwrs_shipment_items` se statusem RECEIVED (finální stav). + CANCELLED zásilky se stahují při každém běhu (záměrně zachováno). + +### Fáze 2 — import (po stažení obou studií) + +1. `ensure_indexes()` (jednou) +2. **Pacienti** (`import_patients.py`): summary → detaily → notifikace; + per soubor, po úspěchu přesun do Processed/ +3. **Léky** (`import_drugs.py`): jeden `import_id` per studie a běh; + parsuje všechny čekající soubory (nejstarší napřed, poslední vyhrává per `_id`), + pak hromadný zápis: + - `iwrs_shipments`, `iwrs_shipment_items`, `iwrs_inventory` — upsert + snapshot + - `iwrs_destruction` — upsert bez snapshotu + Po úspěšném zápisu se zparsované soubory přesunou do Processed/; + soubor s chybou parsování zůstává v Incoming/. + Prázdný inventory report (centrum bez zásob — jen meta řádky `Site:` atd., + bez tabulky léků) se bere jako 0 položek a normálně se archivuje. + +## Použití + +``` +python run_all_v1.1.py # vše (download + import, obě studie) +python run_all_v1.1.py --download-only # jen stažení do Incoming/ +python run_all_v1.1.py --import-only # jen import čekajících souborů +python run_all_v1.1.py --only-patients # jen pacientská část +python run_all_v1.1.py --only-drugs # jen léková část +python run_all_v1.1.py --study 42847922MDD3003 # jen jedna studie +``` + +Prohlížeč běží s `headless=False` (viditelné okno) jako dosud. +Moduly `import_patients.py` a `import_drugs.py` lze spustit i samostatně. + +## Mapa modulů (vše na úrovni IWRS/) + +``` +IWRS/ + run_all_v1.1.py ← vstupní skript (CLI, orchestrace) + download_patients.py ← summary + delegace na download_subject_details.run() + download_subject_details.py ← detaily subjektů + notifikace (worker) + import_patients.py ← orchestrace importu pacientů + import_patients_mongo.py ← worker: summary + visits → Mongo (ex Patients/import_to_mongo.py) + import_notifications_to_mongo.py ← worker: notifikace PDF+JSON → Mongo + notification_parsers.py ← parsery textů notifikací (bez MySQL) + download_drugs.py ← 4 typy reportů → Incoming/, skip-logika přes Mongo + import_drugs.py ← parsery + import léků z Incoming/ + common/ + iwrs_portal.py ← BASE_URL, credentials, login(page, study) + paths.py ← INCOMING/PROCESSED, unique_path, move_done, sorted_by_mtime + mongo_writer.py ← konvertory, upserty, snapshoty, import log + Trash/ ← run_all_v1.0.py + .md +``` + +## Úklid IWRS (provedeno 2026-06-10) + +Na úrovni `IWRS/` zůstává jen aktivní kód a data: +`run_all_v1.1.py`, `create_report_v1.0.py` (+ oba `.md`), 8 modulů pipeline, +`common/`, `Incoming/` (+ `Processed/`) a `Reports/`. + +Vše ostatní je v `IWRS/Trash/`: celé adresáře `Patients/` a `Drugs/` +(vč. zmrazených archivů `xls_*`, starých výstupů `output/` a `CreatedReports/` +a jejich vlastních `Trash/`), `Testing/`, `backfill_mysql_to_mongo.py` +(jednorázový legacy nástroj; importoval `db_config` z Patients, v Trash zůstává +funkční vedle sebe), `reports.json` a `run_all_v1.0.*`. + +Reportovací skripty byly sjednoceny do `IWRS/create_report_v1.0.py` — původní +`Drugs/create_report.py` a `Patients/create_subject_report.py` jsou v Trash. + +## Jednorázová migrace (provedeno 2026-06-10) + +- `Patients/Incoming/Zpracováno/` (1343 souborů) → `IWRS/Incoming/Processed/` +- `.gitignore`: `IWRS/Patients/Incoming/` → `IWRS/Incoming/` +- Staré vstupní skripty → `Patients/Trash/`, `Drugs/Trash/` +- v1.1: moduly pipeline → úroveň `IWRS/` (git mv, historie zachována) diff --git a/IWRS/run_all_v1.1.py b/IWRS/run_all_v1.1.py new file mode 100644 index 0000000..109b9c6 --- /dev/null +++ b/IWRS/run_all_v1.1.py @@ -0,0 +1,151 @@ +""" +================================================================================ + run_all_v1.1.py — IWRS: kompletní pipeline Pacienti + Léky (obě studie) + Verze: 1.1 + Datum: 2026-06-10 +================================================================================ + + v1.1: všechny moduly pipeline přesunuty na úroveň IWRS/ — adresáře + Patients/ a Drugs/ už pipeline nepotřebuje (zbývají v nich jen + reportovací skripty, archivy a Trash) + v1.0: první verze sjednocené pipeline + +Stáhne z janssen.4gclinical.com a naimportuje do MongoDB (db `studie`): + + Pacienti: Subject Summary, Subject Details, notifikace (PDF+JSON) + Léky: Onsite Inventory, IP Destruction, Shipments Report, Shipment Details + +Tok souborů: vše se stahuje do IWRS/Incoming/, po úspěšném importu se přesouvá +do IWRS/Incoming/Processed/. Při chybě soubor zůstává v Incoming/ a zpracuje +se při příštím běhu. + +Přihlášení: 2× (jednou per studie) — studie se vybírá až po přihlášení, takže +jedna browser session stáhne pacienty i léky pro jednu studii. + +Použití: + python run_all_v1.1.py # vše (download + import, obě studie) + python run_all_v1.1.py --download-only # jen stažení do Incoming/ + python run_all_v1.1.py --import-only # jen import čekajících souborů + python run_all_v1.1.py --only-patients # jen pacientská část + python run_all_v1.1.py --only-drugs # jen léková část + python run_all_v1.1.py --study 42847922MDD3003 # jen jedna studie + +Detaily v run_all_v1.1.md. +""" + +import os +import sys +import argparse +import traceback + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from playwright.sync_api import sync_playwright + +from common.iwrs_portal import login +from common.paths import STUDIES, INCOMING_DIR, PROCESSED_DIR, ensure_dirs +from common.mongo_writer import ensure_indexes + +import download_patients +import import_patients +import download_drugs +import import_drugs + + +def download_phase(studies, do_patients, do_drugs): + with sync_playwright() as p: + for study in studies: + print(f"\n{'='*60}") + print(f"[{study}] STAHOVÁNÍ") + print(f"{'='*60}") + + browser = p.chromium.launch(headless=False) + context = browser.new_context(accept_downloads=True) + page = context.new_page() + + try: + print(" Přihlášení...") + login(page, study) + + if do_patients: + print(f"\n ── PACIENTI [{study}] ──") + try: + download_patients.run(page, study) + except Exception as e: + print(f" CHYBA při stahování pacientů: {e}") + traceback.print_exc() + + if do_drugs: + print(f"\n ── LÉKY [{study}] ──") + try: + download_drugs.run(page, study) + except Exception as e: + print(f" CHYBA při stahování léků: {e}") + traceback.print_exc() + + except Exception as e: + print(f" CHYBA (login/session): {e}") + traceback.print_exc() + finally: + browser.close() + + +def import_phase(studies, do_patients, do_drugs): + print(f"\n{'='*60}") + print("IMPORT DO MongoDB") + print(f"{'='*60}") + ensure_indexes() + + if do_patients: + try: + import_patients.run(studies) + except Exception as e: + print(f" CHYBA při importu pacientů: {e}") + traceback.print_exc() + + if do_drugs: + try: + import_drugs.run(studies) + except Exception as e: + print(f" CHYBA při importu léků: {e}") + traceback.print_exc() + + +def main(): + ap = argparse.ArgumentParser( + description="IWRS pipeline: stažení + import pacientů a léků (obě studie)") + ap.add_argument("--download-only", action="store_true", help="jen stažení do Incoming/") + ap.add_argument("--import-only", action="store_true", help="jen import čekajících souborů") + ap.add_argument("--only-patients", action="store_true", help="jen pacientská část") + ap.add_argument("--only-drugs", action="store_true", help="jen léková část") + ap.add_argument("--study", choices=STUDIES, help="jen jedna studie") + args = ap.parse_args() + + if args.download_only and args.import_only: + ap.error("--download-only a --import-only nelze kombinovat") + if args.only_patients and args.only_drugs: + ap.error("--only-patients a --only-drugs nelze kombinovat") + + studies = [args.study] if args.study else STUDIES + do_patients = not args.only_drugs + do_drugs = not args.only_patients + + ensure_dirs() + + if not args.import_only: + download_phase(studies, do_patients, do_drugs) + + if not args.download_only: + import_phase(studies, do_patients, do_drugs) + + print(f"\n{'='*60}") + print("Vše hotovo.") + print(f" Incoming: {INCOMING_DIR}") + print(f" Processed: {PROCESSED_DIR}") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.md b/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.md new file mode 100644 index 0000000..ef14c74 --- /dev/null +++ b/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.md @@ -0,0 +1,86 @@ +# jnj_emails_to_fulltext_v1.0 + +**Verze:** 1.0 +**Datum:** 2026-06-10 +**Autor:** vladimir.buzalka +**Umístění:** `/mnt/user/Scripts/Janssen_emails_to_fulltext/` (v kontejneru `/scripts/Janssen_emails_to_fulltext/`) + +## Účel + +Inkrementální zpracování JNJ e-mailů. Ke schránce `vbuzalka@its.jnj.com` není jiný +přístup než průběžný export `.msg` souborů do `/mnt/JNJEMAILS` (~70 tis. souborů, +nové přibývají denně). Skript nové soubory: + +1. **KROK 1 — IMPORT:** naparsuje a uloží do MongoDB `emaily."vbuzalka@its.jnj.com"` + (stejné schéma a `_id` logika jako bulk import `parse_emails_tower_v1.3.py`) +2. **KROK 2 — ENRICH:** fulltext do PostgreSQL `MongoEmaily.emails` — deleguje na + existující `/scripts/5_enrich_fulltext_emails_v1.4.py --mailbox "vbuzalka@its.jnj.com"`, + takže PG schéma, `extractor_version` i skip-logika zůstávají identické s hlavní + Graph pipeline (krok 5 v `0_run_pipeline`). Hlavní pipeline pak tyto záznamy + pouze skipuje (ext_v shodná, ok=true) — žádná dvojí práce. + +## Vztah k existujícím skriptům + +| Skript | Role | +|--------|------| +| `parse_emails_tower_v1.3.py` | jednorázový bulk import (70k, ~48 h) — zdroj parsovací logiky | +| `1b_parse_emails_graph_delta_v1.0.py` | Graph delta pro schránky buzalka.cz — JNJ záměrně vynechána | +| `5_enrich_fulltext_emails_v1.4.py` | enrich do PG — **tento skript ho volá s `--mailbox`** | +| **`jnj_emails_to_fulltext_v1.0.py`** | **inkrementální .msg → Mongo → PG pro JNJ** | + +Po ověření provozu je plán sloučit do hlavní pipeline `0_run_pipeline` jako další krok +(např. „1c: JNJ msg import"). + +## Inkrementalita (co se přeskakuje) + +- soubory zapsané ve `state.json` → klíč `done` `{filename: message_id}` +- pojistka: `distinct("filename")` z Mongo kolekce (state se při 1. běhu sám naplní); + `state.json` zároveň řeší duplicitní Message-ID (2 soubory → 1 dokument), které by + se přes samotný Mongo distinct reimportovaly donekonečna +- soubory mladší než `--min-age` s (default 300) — mohou se ještě zapisovat exportem +- soubory které 3× selhaly (`MAX_FAIL`) — `--retry-failed` je zkusí znovu +- flock (`.lock`) — souběžný start se ukončí bez práce + +## Spouštění + +```bash +# Náhled bez zápisu (Mongo, PG i state zůstanou nedotčené): +docker exec -it python-runner python /scripts/Janssen_emails_to_fulltext/jnj_emails_to_fulltext_v1.0.py --dry-run --limit 10 + +# Ostrý inkrementální běh: +docker exec -it python-runner python /scripts/Janssen_emails_to_fulltext/jnj_emails_to_fulltext_v1.0.py + +# Wrapper s datovaným logem (pro User Scripts / cron): +/mnt/user/Scripts/Janssen_emails_to_fulltext/run_jnj_emails_to_fulltext.sh +``` + +| Parametr | Význam | +|----------|--------| +| `--dry-run` | parsuje, ale nic nezapisuje (Mongo, PG, state.json) | +| `--limit N` | max N nových souborů (test) | +| `--min-age S` | přeskoč soubory mladší než S sekund (default 300) | +| `--no-enrich` | jen import do Mongo, bez PG | +| `--retry-failed` | znovu zkusit trvale selhávající soubory | +| `--msgs-dir DIR` | jiný zdrojový adresář (default `/mnt/JNJEMAILS`) | + +## Výstupy a logy (vše v adresáři skriptu) + +- `logs/run_YYYYMMDD_HHMM.log` — stdout běhu (přes wrapper; `run_latest.log` symlink) +- `logs/errors.log` — chyby parsování jednotlivých souborů +- `state.json` — `{done: {filename: message_id}, failed: {filename: počet}}` + +## Exit kódy + +- `0` — OK (včetně „nic nového") +- `1` — chyby parsování / enrich selhal / Mongo nedostupná + +## Závislosti + +V image `python-runner` už jsou: `extract-msg==0.55.0`, `olefile`, `pymongo`, +`python-dateutil`. KROK 2 navíc potřebuje `psycopg`, `bs4`, `lxml` — používá je +denně pipeline krok 5, takže jsou k dispozici. + +## Historie verzí + +- **1.0** (2026-06-10) — iniciální verze; parsovací logika 1:1 z + `parse_emails_tower_v1.3.py`, enrich delegován na `5_enrich_fulltext_emails_v1.4.py` diff --git a/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.py b/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.py new file mode 100644 index 0000000..4c2ef8c --- /dev/null +++ b/Janssen_emails_to_fulltext/Trash/jnj_emails_to_fulltext_v1.0.py @@ -0,0 +1,890 @@ +""" +jnj_emails_to_fulltext_v1.0.py +Nazev: jnj_emails_to_fulltext_v1.0.py +Verze: 1.0 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Popis: + Inkrementalni pipeline pro JNJ e-maily exportovane jako .msg soubory. + Nove .msg pribyvaji do /mnt/JNJEMAILS (export z Outlooku, jiny pristup + ke schrance vbuzalka@its.jnj.com neni). Skript je dvoukrokovy: + + KROK 1 (IMPORT): nove .msg -> MongoDB emaily."vbuzalka@its.jnj.com" + KROK 2 (ENRICH): fulltext -> PostgreSQL MongoEmaily.emails + (vola existujici /scripts/5_enrich_fulltext_emails_v1.4.py + s parametrem --mailbox, takze PG schema i extractor_version + zustavaji 100% konzistentni s hlavni Graph pipeline) + + Parsovaci logika KROKU 1 je prevzata 1:1 z parse_emails_tower_v1.3.py: + - kaskadove otevirani (normal -> SUPPRESS_ALL -> overrideEncoding) + - raw-OLE fallback pro degradovana textova pole (kodovani se neveri) + - to_bson ochrana proti >int64 MAPI hodnotam + - stejne schema dokumentu, stejna kolekce, stejny zpusob _id + (Internet Message-ID, fallback "filename:") + + Inkrementalita (co se preskakuje): + - soubory ve stavovem souboru state.json (klic "done") + - + pojistka: distinct("filename") z Mongo kolekce + (state.json se pri prvnim behu z Monga sam naplni) + - soubory mladsi nez --min-age sekund (jeste se mohou zapisovat) + - soubory ktere MAX_FAIL-krat selhaly (--retry-failed je zkusi znovu) + + Stavovy soubor state.json resi i edge-case duplicitnich Message-ID + (2 ruzne .msg se stejnym _id by se pres Mongo distinct("filename") + donekonecna stridave reimportovaly). + +Prostredi: + Bezi v Docker containeru "python-runner" na Unraid Tower. + /mnt/user/JNJEMAILS -> /mnt/JNJEMAILS (zdrojove .msg) + /mnt/user/Scripts -> /scripts (tento skript + enrich skript) + MongoDB 192.168.1.76:27017 db=emaily kolekce=vbuzalka@its.jnj.com + PostgreSQL 192.168.1.76:5432 db=MongoEmaily tabulka=emails (pres enrich) + +Spousteni (z Unraid terminalu): + # Nahled bez zapisu (parsuje, ale nezapisuje do Mongo ani PG): + docker exec -it python-runner python /scripts/Janssen_emails_to_fulltext/jnj_emails_to_fulltext_v1.0.py --dry-run --limit 10 + + # Ostry inkrementalni beh (import + enrich): + docker exec -it python-runner python /scripts/Janssen_emails_to_fulltext/jnj_emails_to_fulltext_v1.0.py + + # Pres wrapper s datovanym logem (pro cron): + /mnt/user/Scripts/Janssen_emails_to_fulltext/run_jnj_emails_to_fulltext.sh + +Parametry: + --dry-run parsovani probehne, ale NIC se nezapise (Mongo, PG, state) + --limit N zpracovat max N novych souboru (test) + --min-age S preskoc soubory mladsi nez S sekund (default 300) + --no-enrich preskocit KROK 2 (jen import do Mongo) + --retry-failed zkusit znovu i soubory ktere uz MAX_FAIL-krat selhaly + --msgs-dir DIR jiny zdrojovy adresar (default /mnt/JNJEMAILS) + +Vystup / logy (vse v adresari skriptu): + stdout prubeh (wrapper presmeruje do logs/run_*.log) + logs/errors.log chyby parsovani jednotlivych souboru + state.json stav: done={filename: message_id}, failed={filename: pocet} + +Exit kody: + 0 = OK (vcetne "nic noveho") + 1 = chyba (parsovani s chybami / enrich selhal / Mongo nedostupna) + +Historie verzi: + 1.0 2026-06-10 Inicialni verze (fork parsovaci logiky parse_emails_tower_v1.3, + enrich delegovan na 5_enrich_fulltext_emails_v1.4 --mailbox) +""" + +import sys +import os +import re +import json +import time +import logging +import argparse +import base64 +import struct +import subprocess +from pathlib import Path +from datetime import datetime, timezone +from typing import Optional + +import extract_msg +from extract_msg.enums import ErrorBehavior +import olefile +from dateutil import parser as dtparser +from pymongo import MongoClient, UpdateOne + +try: + import fcntl # jen Linux (v containeru vzdy) +except ImportError: # lokalni vyvoj na Windows + fcntl = None + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +# ─── KONFIGURACE ────────────────────────────────────────────────────────────── +SCRIPT_DIR = Path(__file__).resolve().parent +MSGS_DIR = Path("/mnt/JNJEMAILS") +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" +MONGO_COL = "vbuzalka@its.jnj.com" +ENRICH_SCRIPT = Path("/scripts/5_enrich_fulltext_emails_v1.4.py") +BATCH_SIZE = 200 +MAX_FAIL = 3 # po tolika selhanich soubor preskakovat +DEFAULT_MIN_AGE = 300 # s — mladsi soubory se jeste mohou zapisovat +STATE_FILE = SCRIPT_DIR / "state.json" +LOCK_FILE = SCRIPT_DIR / ".lock" +LOG_DIR = SCRIPT_DIR / "logs" +ERR_LOG = LOG_DIR / "errors.log" +SCRIPT_VERSION = "1.0" +# ────────────────────────────────────────────────────────────────────────────── + +LOG_DIR.mkdir(exist_ok=True) +logging.basicConfig( + filename=str(ERR_LOG), + level=logging.ERROR, + format="%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + encoding="utf-8", +) + + +# ─── Pomocné funkce (1:1 z parse_emails_tower_v1.3) ────────────────────────── + +def safe(obj, *attrs, default=None): + """Bezpecne cteni atributu — vrati prvni non-None hodnotu.""" + for attr in attrs: + try: + val = getattr(obj, attr, None) + if val is None: + continue + if isinstance(val, str) and not val.strip(): + continue + return val + except Exception: + continue + return default + + +def parse_date(raw) -> Optional[datetime]: + """Libovolny datum -> UTC datetime bez tzinfo (pro MongoDB).""" + if raw is None: + return None + if isinstance(raw, datetime): + if raw.tzinfo: + return raw.astimezone(timezone.utc).replace(tzinfo=None) + return raw + try: + dt = dtparser.parse(str(raw)) + if dt.tzinfo: + return dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt + except Exception: + return None + + +_INT64_MIN, _INT64_MAX = -(2 ** 63), 2 ** 63 - 1 + + +def to_bson(val): + """Konvertuje hodnotu na BSON-serializovatelny typ. + + BSON umi jen signed int64 — velke MAPI hodnoty (PR_CHANGE_KEY, FILETIME) + mimo rozsah prevadime na string, jinak cely bulk_write spadne. + """ + if isinstance(val, bool): # bool PRED int (isinstance(True, int)) + return val + if isinstance(val, bytes): + return val.hex() if len(val) <= 128 else f"" + if isinstance(val, datetime): + return parse_date(val) + if isinstance(val, int): + return val if _INT64_MIN <= val <= _INT64_MAX else str(val) + if isinstance(val, (str, float, type(None))): + return val + if isinstance(val, list): + return [to_bson(v) for v in val] + try: + iv = int(val) + return iv if _INT64_MIN <= iv <= _INT64_MAX else str(iv) + except Exception: + pass + return str(val) + + +def extract_headers(msg) -> dict: + headers = {} + try: + hdr = msg.header + if not hdr: + return {} + from email.header import decode_header as _dh + + def _decode(v: str) -> str: + try: + parts = _dh(v) + out = "" + for part, enc in parts: + out += part.decode(enc or "utf-8", errors="replace") if isinstance(part, bytes) else part + return out + except Exception: + return v + + for key in set(hdr.keys()): + k = key.lower().replace("-", "_") + vals = [_decode(v) for v in hdr.get_all(key, [])] + headers[k] = vals if len(vals) > 1 else (vals[0] if vals else "") + except Exception as e: + logging.error("extract_headers: %s", e) + return headers + + +def extract_recipients(msg) -> list: + result = [] + type_map = {1: "to", 2: "cc", 3: "bcc"} + try: + for r in msg.recipients: + rtype = getattr(r, "type", 1) + try: + rtype = int(rtype) + except Exception: + try: + rtype = int(rtype.value) + except Exception: + rtype = 1 + rec = { + "type": type_map.get(rtype, "to"), + "email": safe(r, "email", default=""), + "name": safe(r, "name", default=""), + } + result.append(rec) + except Exception as e: + logging.error("extract_recipients: %s", e) + return result + + +def extract_attachments(msg) -> list: + result = [] + try: + for att in msg.attachments: + fname = safe(att, "longFilename", "shortFilename", default="") + if not fname: + continue + size = 0 + try: + d = att.data + size = len(d) if d else 0 + except Exception: + pass + result.append({ + "filename": fname, + "size_bytes": size, + "mime_type": safe(att, "mimetype", "mimeType", default="application/octet-stream"), + "content_id": safe(att, "cid", default=None), + "is_inline": bool(safe(att, "isInline", default=False)), + }) + except Exception as e: + logging.error("extract_attachments: %s", e) + return result + + +def extract_mapi_props(msg) -> dict: + """Vsechny raw MAPI properties jako {0xXXXX: value}.""" + result = {} + try: + props = msg.props + if not hasattr(props, "items"): + return {} + for key, prop in props.items(): + try: + val = to_bson(prop.value) + prop_id = f"0x{key[:4].upper()}" if len(key) >= 4 else f"0x{key.upper()}" + result[prop_id] = val + except Exception: + pass + except Exception as e: + logging.error("extract_mapi_props: %s", e) + return result + + +# ─── Tolerantní otevírání a raw-OLE fallback (1:1 z v1.3) ──────────────────── + +_CPID_TO_CODEC = { + 1250: "cp1250", 1251: "cp1251", 1252: "cp1252", 1253: "cp1253", + 1254: "cp1254", 1255: "cp1255", 1256: "cp1256", 1257: "cp1257", + 1258: "cp1258", 874: "cp874", 932: "shift_jis", 936: "gb2312", + 949: "euc_kr", 950: "big5", 65001: "utf-8", 28591: "iso-8859-1", + 28592: "iso-8859-2", 20127: "ascii", +} + + +def _read_u32_prop(ole, propid): + """Precte 32-bit hodnotu MAPI property z top-level __properties_version1.0.""" + try: + data = ole.openstream("__properties_version1.0").read() + except Exception: + return None + body = data[32:] + for i in range(0, len(body) - 16 + 1, 16): + rec = body[i:i + 16] + tag = struct.unpack("> 16) & 0xFFFF) == propid: + return struct.unpack(" Optional[str]: + """Codec dle PR_INTERNET_CPID / PR_MESSAGE_CODEPAGE (napoveda, ne dogma).""" + for pid in (0x3FDE, 0x3FFD): + codec = _CPID_TO_CODEC.get(_read_u32_prop(ole, pid)) + if codec and codec not in ("utf-8", "ascii"): + return codec + return None + + +def _cascade_decode(raw: bytes, is_unicode: bool, cpid_codec: Optional[str]) -> str: + """Dekoduje bajty MAPI stringu — hlavickam se neveri, kaskada strict pokusu.""" + if not raw: + return "" + if is_unicode: + try: + return raw.decode("utf-16-le") + except Exception: + return raw.decode("utf-16-le", errors="replace") + order = ["utf-8"] + if cpid_codec: + order.append(cpid_codec) + order += ["cp1250", "cp1252", "gb2312", "big5"] + for enc in order: + try: + return raw.decode(enc, errors="strict") + except Exception: + continue + return raw.decode("latin-1", errors="replace") + + +def _raw_mapi_strings(msg_path: Path) -> dict: + """Cte klicova textova MAPI pole PRIMO z OLE (mimo extract_msg).""" + out = {"subject": "", "normalized_subject": "", "sender_name": "", + "sender_email": "", "sender_smtp": "", "body_text": "", "body_html": ""} + try: + ole = olefile.OleFileIO(str(msg_path)) + except Exception: + return out + try: + cpid = _detect_cpid(ole) + wanted = { + "0037": "subject", "0E1D": "normalized_subject", + "0C1A": "sender_name", "5D01": "sender_smtp", + "0C1F": "sender_email", "1000": "body_text", "1013": "body_html", + } + prefix = "__substg1.0_" + found = {} + for entry in ole.listdir(): + if len(entry) != 1: + continue + name = entry[0] + if not name.startswith(prefix): + continue + tag = name[len(prefix):len(prefix) + 4].upper() + key = wanted.get(tag) + if not key: + continue + typ = name[-4:].upper() + prio = {"001F": 3, "001E": 2, "0102": 1}.get(typ, 0) + if prio == 0: + continue + prev = found.get(key) + if prev and prev[0] >= prio: + continue + try: + raw = ole.openstream(entry).read() + val = _cascade_decode(raw, typ == "001F", cpid) + except Exception: + continue + found[key] = (prio, val) + for key, (_, val) in found.items(): + out[key] = val + finally: + ole.close() + return out + + +def _degraded(s) -> bool: + """Pole je degradovane: prazdne nebo obsahuje U+FFFD.""" + return (not s) or ("�" in s) + + +def open_message(msg_path: Path): + """Kaskadove otevreni .msg -> (msg, mode) nebo (None, None).""" + try: + return extract_msg.Message(str(msg_path)), "normal" + except Exception: + pass + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL), "suppress_all" + except Exception: + pass + encs = [] + try: + ole = olefile.OleFileIO(str(msg_path)) + c = _detect_cpid(ole) + ole.close() + if c: + encs.append(c) + except Exception: + pass + for e in encs + ["cp1250", "cp1252"]: + try: + return extract_msg.Message( + str(msg_path), errorBehavior=ErrorBehavior.SUPPRESS_ALL, + overrideEncoding=e), f"override:{e}" + except Exception: + continue + return None, None + + +# ─── Hlavní extrakce (1:1 z v1.3) ──────────────────────────────────────────── + +def extract_message(msg_path: Path) -> Optional[dict]: + """Parsuje jeden .msg soubor -> MongoDB dokument.""" + msg, parse_mode = open_message(msg_path) + if msg is None: + logging.error("open failed [%s]: vsechny pokusy o otevreni selhaly", msg_path.name) + return None + + try: + mid = None + for attr in ("messageId", "message_id", "internetMessageId"): + mid = safe(msg, attr) + if mid: + break + if not mid: + mid = f"filename:{msg_path.stem}" + mid = str(mid).strip() + + try: + subject = msg.subject or "" + except Exception: + subject = "" + + normalized_subject = safe(msg, "normalizedSubject", "normalized_subject", default="") + + try: + body_text = msg.body or "" + except Exception: + body_text = "" + + body_html = None + try: + bh = msg.htmlBody + if isinstance(bh, bytes): + bh = bh.decode("utf-8", errors="replace") + if bh: + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + except Exception: + pass + + try: + sender_email = msg.sender or "" + except Exception: + sender_email = "" + + sender_name = safe(msg, "senderName", "sender_name", default="") + sender_smtp = safe(msg, "senderSmtpAddress", "sent_representing_smtp_address", default="") + + recipients = extract_recipients(msg) + + try: + to_raw = msg.to or "" + except Exception: + to_raw = "" + try: + cc_raw = msg.cc or "" + except Exception: + cc_raw = "" + try: + bcc_raw = getattr(msg, "bcc", None) or "" + except Exception: + bcc_raw = "" + + display_to = safe(msg, "displayTo", "display_to", default="") + display_cc = safe(msg, "displayCc", "display_cc", default="") + + try: + received_at = parse_date(msg.date) + except Exception: + received_at = None + + sent_at = None + for attr in ("clientSubmitTime", "client_submit_time", "sentOn"): + v = safe(msg, attr) + if v: + sent_at = parse_date(v) + break + + importance = 1 + try: + v = msg.importance + if v is not None: + importance = int(v) + except Exception: + pass + + sensitivity = 0 + try: + v = getattr(msg, "sensitivity", None) + if v is not None: + sensitivity = int(v) + except Exception: + pass + + flag_status = 0 + try: + v = safe(msg, "flagStatus", "flag_status") + if v is not None: + flag_status = int(v) + except Exception: + pass + + conversation_topic = safe(msg, "conversationTopic", "conversation_topic", default="") + + conversation_index = "" + try: + ci = safe(msg, "conversationIndex", "conversation_index") + if isinstance(ci, bytes): + conversation_index = base64.b64encode(ci).decode() + elif ci: + conversation_index = str(ci) + except Exception: + pass + + in_reply_to = safe(msg, "inReplyTo", "in_reply_to", default="") + + internet_refs = [] + try: + refs = safe(msg, "internetReferences", "internet_references") + if isinstance(refs, list): + internet_refs = refs + elif isinstance(refs, str) and refs: + internet_refs = [r.strip() for r in refs.split() if r.strip()] + except Exception: + pass + + categories = [] + try: + cats = safe(msg, "categories") + if isinstance(cats, list): + categories = [str(c) for c in cats if c] + elif isinstance(cats, str) and cats: + categories = [c.strip() for c in re.split(r"[;,]", cats) if c.strip()] + except Exception: + pass + + read_receipt = bool(safe(msg, "readReceiptRequested", "read_receipt_requested", default=False)) + delivery_receipt = bool(safe(msg, "deliveryReceiptRequested", "delivery_receipt_requested", default=False)) + + headers = extract_headers(msg) + + if not in_reply_to: + in_reply_to = headers.get("in_reply_to", "") + if not internet_refs: + refs_str = headers.get("references", "") + if isinstance(refs_str, str) and refs_str: + internet_refs = [r.strip() for r in refs_str.split() if r.strip()] + + attachments = extract_attachments(msg) + mapi_raw = extract_mapi_props(msg) + + msg.close() + + # Raw-OLE fallback pro degradovana textova pole + parse_degraded = parse_mode != "normal" + forced = parse_mode != "normal" + if (forced or _degraded(subject) or _degraded(body_text) + or _degraded(sender_email) or (body_html and "�" in body_html)): + raw = _raw_mapi_strings(msg_path) + if raw["subject"] and (forced or _degraded(subject)): + subject = raw["subject"] + if raw["normalized_subject"] and (forced or _degraded(normalized_subject)): + normalized_subject = raw["normalized_subject"] + if raw["body_text"] and (forced or _degraded(body_text)): + body_text = raw["body_text"] + if raw["body_html"] and (forced or not body_html or "�" in body_html): + bh = raw["body_html"] + body_html = bh if len(bh) <= 2 * 1024 * 1024 else bh[:2 * 1024 * 1024] + if (raw["sender_smtp"] or raw["sender_email"]) and (forced or _degraded(sender_email)): + sender_email = raw["sender_smtp"] or raw["sender_email"] + if raw["sender_name"] and (forced or _degraded(sender_name)): + sender_name = raw["sender_name"] + if raw["sender_smtp"] and not sender_smtp: + sender_smtp = raw["sender_smtp"] + + return { + "_id": mid, + "filename": msg_path.name, + + "subject": subject, + "normalized_subject": normalized_subject, + "importance": importance, + "sensitivity": sensitivity, + "flag_status": flag_status, + "read_receipt_requested": read_receipt, + "delivery_receipt_requested": delivery_receipt, + "has_attachments": len(attachments) > 0, + "attachment_count": len(attachments), + "message_size_bytes": msg_path.stat().st_size, + + "conversation_topic": conversation_topic, + "conversation_index": conversation_index, + "in_reply_to": in_reply_to, + "internet_references": internet_refs, + "categories": categories, + + "received_at": received_at, + "sent_at": sent_at, + + "sender": { + "email": sender_email, + "name": sender_name, + "smtp": sender_smtp, + }, + "to": to_raw, + "cc": cc_raw, + "bcc": bcc_raw, + "display_to": display_to, + "display_cc": display_cc, + "recipients": recipients, + + "body_text": body_text, + "body_html": body_html, + + "attachments": attachments, + "headers": headers, + "mapi": mapi_raw, + + "parse_mode": parse_mode, + "parse_degraded": parse_degraded, + + "parsed_at": datetime.now(timezone.utc).replace(tzinfo=None), + } + + except Exception as e: + logging.error("extract_message failed [%s]: %s", msg_path.name, e) + return None + + +# ─── Stav (state.json) ─────────────────────────────────────────────────────── + +def load_state() -> dict: + if STATE_FILE.exists(): + try: + st = json.loads(STATE_FILE.read_text(encoding="utf-8")) + if isinstance(st, dict) and "done" in st and "failed" in st: + return st + except Exception as e: + print(f" VAROVANI: state.json nesel nacist ({e}) -- zacinam s prazdnym") + return {"done": {}, "failed": {}} + + +def save_state(state: dict) -> None: + tmp = STATE_FILE.with_suffix(".json.tmp") + tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8") + os.replace(tmp, STATE_FILE) + + +# ─── Lock proti soubehu ────────────────────────────────────────────────────── + +def acquire_lock(): + """Vrati otevreny lock file handle, nebo ukonci skript pokud uz bezi jiny.""" + if fcntl is None: + return None + lf = open(LOCK_FILE, "w") + try: + fcntl.flock(lf, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + print("Jiny beh jnj_emails_to_fulltext jeste probiha (lock) -- koncim.") + sys.exit(0) + return lf + + +# ─── KROK 2: enrich (delegovano na existujici skript pipeline) ─────────────── + +def run_enrich() -> int: + """Spusti 5_enrich_fulltext_emails --mailbox . Vraci exit code.""" + if not ENRICH_SCRIPT.exists(): + print(f" CHYBA: enrich skript nenalezen: {ENRICH_SCRIPT}") + return 1 + cmd = [sys.executable, str(ENRICH_SCRIPT), "--mailbox", MONGO_COL] + print(f"\n=== KROK 2: ENRICH (PG fulltext) ===") + print(f" {' '.join(cmd)}") + sys.stdout.flush() + r = subprocess.run(cmd) + print(f" enrich exit code: {r.returncode}") + return r.returncode + + +# ─── MAIN ───────────────────────────────────────────────────────────────────── + +def main() -> int: + ap = argparse.ArgumentParser(description=f"jnj_emails_to_fulltext v{SCRIPT_VERSION}") + ap.add_argument("--msgs-dir", default=str(MSGS_DIR), + help="Cesta k .msg souborum") + ap.add_argument("--limit", type=int, default=0, + help="Zpracovat max N novych souboru (0 = vse)") + ap.add_argument("--min-age", type=int, default=DEFAULT_MIN_AGE, + help=f"Preskoc soubory mladsi nez S sekund (default {DEFAULT_MIN_AGE})") + ap.add_argument("--dry-run", action="store_true", + help="Parsuje, ale NEZAPISUJE (Mongo, PG, state.json)") + ap.add_argument("--no-enrich", action="store_true", + help="Preskocit KROK 2 (PG fulltext)") + ap.add_argument("--retry-failed", action="store_true", + help=f"Zkusit znovu i soubory s {MAX_FAIL}+ selhanimi") + args = ap.parse_args() + + msgs_dir = Path(args.msgs_dir) + start = datetime.now() + + print(f"=== jnj_emails_to_fulltext v{SCRIPT_VERSION} ===") + print(f"Start: {start.strftime('%Y-%m-%d %H:%M:%S')}{' [DRY-RUN]' if args.dry_run else ''}") + print(f"Zdroj: {msgs_dir}") + print(f"MongoDB: {MONGO_URI} -> {MONGO_DB}.{MONGO_COL}") + + lock = acquire_lock() # noqa: F841 — drzime handle do konce behu + + # MongoDB + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + try: + client.admin.command("ping") + print(" MongoDB OK") + except Exception as e: + print(f" CHYBA: MongoDB neni dostupna -- {e}") + return 1 + + col = client[MONGO_DB][MONGO_COL] + + # Stav + seed z Mongo (filename je v kazdem dokumentu z tower importu) + state = load_state() + print(" Nacitam seznam jiz importovanych souboru z MongoDB...") + mongo_filenames = set(col.distinct("filename")) + print(f" v Mongo: {len(mongo_filenames)} souboru, ve state.json: {len(state['done'])}") + known = set(state["done"]) | mongo_filenames + + failed_skip = set() + if not args.retry_failed: + failed_skip = {fn for fn, cnt in state["failed"].items() if cnt >= MAX_FAIL} + + # Scan + print(f"\nSkenuji {msgs_dir} ...") + all_files = sorted(msgs_dir.glob("*.msg")) + now_ts = time.time() + + too_young = 0 + candidates = [] + for f in all_files: + if f.name in known or f.name in failed_skip: + continue + try: + if now_ts - f.stat().st_mtime < args.min_age: + too_young += 1 + continue + except OSError: + continue + candidates.append(f) + + if args.limit: + candidates = candidates[:args.limit] + + total = len(candidates) + print(f" Celkem .msg na disku: {len(all_files)}") + print(f" Jiz importovano (skip): {len(known & {x.name for x in all_files})}") + print(f" Trvale selhavajici: {len(failed_skip)}") + print(f" Mladsi nez {args.min_age}s: {too_young}") + print(f" Ke zpracovani: {total}{' (limit)' if args.limit else ''}\n") + + imported = 0 + err_count = 0 + + if total == 0: + print("Nic noveho k importu.") + else: + batch: list[tuple[str, str, UpdateOne]] = [] # (filename, _id, op) + + def flush(): + nonlocal imported, err_count + if not batch: + return + if args.dry_run: + batch.clear() + return + ops = [op for _, _, op in batch] + try: + col.bulk_write(ops, ordered=False) + for fn, mid, _ in batch: + state["done"][fn] = mid + state["failed"].pop(fn, None) + except Exception as e: + # Per-dokument fallback — chyba zahodi jen vadny zaznam + logging.error("bulk_write spadl (%s) -- prepinam na per-dokument", e) + print(f" CHYBA bulk_write: {e} -- zkousim per-dokument") + for fn, mid, op in batch: + try: + col.bulk_write([op], ordered=False) + state["done"][fn] = mid + state["failed"].pop(fn, None) + except Exception as e2: + logging.error("per-dokument selhal [%s, _id=%s]: %s", fn, mid, e2) + print(f" ZAHOZEN {fn} (_id={mid}): {e2}") + state["failed"][fn] = state["failed"].get(fn, 0) + 1 + imported -= 1 + err_count += 1 + save_state(state) + batch.clear() + + for i, msg_path in enumerate(candidates, 1): + doc = extract_message(msg_path) + + if doc is None: + err_count += 1 + if not args.dry_run: + state["failed"][msg_path.name] = state["failed"].get(msg_path.name, 0) + 1 + else: + batch.append((msg_path.name, doc["_id"], + UpdateOne({"_id": doc["_id"]}, {"$set": doc}, upsert=True))) + imported += 1 + + if len(batch) >= BATCH_SIZE: + flush() + + status = "ERR " if doc is None else "OK " + subject_str = (doc.get("subject") or "")[:60] if doc else "?" + sender_str = (doc.get("sender", {}).get("email") or "")[:40] if doc else "?" + print(f" {i:>5}/{total} {status} {subject_str:<60} {sender_str}") + + if i % 500 == 0: + elapsed = (datetime.now() - start).total_seconds() + rate = i / elapsed if elapsed > 0 else 0 + eta_s = int((total - i) / rate) if rate > 0 else 0 + print(f" {'-'*80}") + print(f" Prubeh: ok={imported} err={err_count} " + f"{rate:.1f} msg/s ETA {eta_s//3600}h{(eta_s%3600)//60}m") + print(f" {'-'*80}") + + flush() + if not args.dry_run: + save_state(state) + + elapsed_total = (datetime.now() - start).total_seconds() + print(f"\n{'='*52}") + print(f"KROK 1 (import): ok={imported} err={err_count}" + f"{' [DRY-RUN — nic nezapsano]' if args.dry_run else ''}") + print(f"Cas importu: {int(elapsed_total//60)}m {int(elapsed_total%60)}s") + if not args.dry_run: + print(f"Dokumentu v kolekci: {col.estimated_document_count()}") + + client.close() + + # KROK 2 — enrich do PG (jen pokud je co a neni dry-run) + enrich_rc = 0 + if args.dry_run: + print("\nKROK 2 (enrich): preskocen [DRY-RUN]") + elif args.no_enrich: + print("\nKROK 2 (enrich): preskocen [--no-enrich]") + elif imported == 0: + print("\nKROK 2 (enrich): preskocen (zadne nove e-maily)") + else: + enrich_rc = run_enrich() + + print(f"\nKonec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + if err_count: + print(f"Chyby parsovani logovany do: {ERR_LOG}") + + return 1 if (err_count > 0 or enrich_rc != 0) else 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print("\nPreruseno uzivatelem") + sys.exit(130) diff --git a/Janssen_emails_to_fulltext/Trash/run_jnj_emails_to_fulltext.sh b/Janssen_emails_to_fulltext/Trash/run_jnj_emails_to_fulltext.sh new file mode 100644 index 0000000..edf97c0 --- /dev/null +++ b/Janssen_emails_to_fulltext/Trash/run_jnj_emails_to_fulltext.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# ============================================================================ +# Wrapper pro jnj_emails_to_fulltext_v1.0.py (JNJ .msg -> Mongo -> PG fulltext). +# Bezi na Unraid hostu, skript spousti uvnitr containeru python-runner. +# Datovane logy + run_latest.log symlink, uklid logu po 30 dnech. +# +# Instalace pres User Scripts plugin nebo /etc/cron.d (zatim NEINSTALOVANO): +# 30 6,18 * * * /mnt/user/Scripts/Janssen_emails_to_fulltext/run_jnj_emails_to_fulltext.sh +# ============================================================================ + +set -u + +BASE_DIR="/mnt/user/Scripts/Janssen_emails_to_fulltext" +LOG_DIR="${BASE_DIR}/logs" +TIMESTAMP=$(date +%Y%m%d_%H%M) +LOG_FILE="${LOG_DIR}/run_${TIMESTAMP}.log" +LATEST_LINK="${LOG_DIR}/run_latest.log" +RETENTION_DAYS=30 + +mkdir -p "$LOG_DIR" + +echo "=== jnj_emails_to_fulltext run @ $(date '+%Y-%m-%d %H:%M:%S') ===" >> "$LOG_FILE" + +if ! docker inspect -f '{{.State.Running}}' python-runner 2>/dev/null | grep -q true; then + echo "ERROR: python-runner container is not running" >> "$LOG_FILE" + docker start python-runner >> "$LOG_FILE" 2>&1 || exit 1 + sleep 5 +fi + +docker exec python-runner python /scripts/Janssen_emails_to_fulltext/jnj_emails_to_fulltext_v1.0.py "$@" >> "$LOG_FILE" 2>&1 +RET=$? + +echo "" >> "$LOG_FILE" +echo "=== Wrapper finished @ $(date '+%Y-%m-%d %H:%M:%S') exit=$RET ===" >> "$LOG_FILE" + +ln -sf "$LOG_FILE" "$LATEST_LINK" + +find "$LOG_DIR" -name 'run_*.log' -type f -mtime +${RETENTION_DAYS} -delete + +exit $RET diff --git a/Python-runner/5_enrich_fulltext_emails_v1.4.py b/Python-runner/5_enrich_fulltext_emails_v1.4.py new file mode 100644 index 0000000..0ae7b05 --- /dev/null +++ b/Python-runner/5_enrich_fulltext_emails_v1.4.py @@ -0,0 +1,587 @@ +""" +============================================================================== +Skript: enrich_fulltext_emails_v1.4.py +Verze: 1.4 +Datum: 2026-06-10 +Autor: vladimir.buzalka + +Zmeny v1.4 (2026-06-10): + - Bugfix: NON_MAILBOX_COLLECTIONS rozsireno o "jnj_messages" a + "jnj_sync_state" (pomocne kolekce JNJ folder trackingu). Predtim je + discover_mailboxes bral jako schranky (jiny schema dokumentu) -> + errors=1 -> cely krok 5 FAIL(1) pri kazdem behu pipeline. + +Popis: + Vytahne plny text z emailu ulozenych v MongoDB (db: emaily) a ulozi ho do + PostgreSQL (db: MongoEmaily, tabulka: emails) s GIN tsvector indexem. + + Emaily se NESTAHUJI znovu - tela uz jsou v Mongo z parse_emails_graph_v1.4 + (a refetch_text_bodies_v1.0 pro stare plain-text emaily). + Tento skript jen vybere prvni dostupne telo a posle text do PG na fulltext. + +Zmeny v1.3.1 (2026-06-09): + - Bugfix: _clean_for_pg nahrazuje osamocene surrogate (\\ud800-\\udfff) za U+FFFD. + Drive jeden mail se surrogaty (napr. JNJ .msg) shodil celou davku a krok 5 + skoncil FAIL. EXTRACTOR_VERSION zustava 1.2 (neni zmena fallback logiky). + +Zmeny v1.3 vs v1.2: + - Bugfix: NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state"} + (sync_state pribyla v delta syncu, predtim ji v1.2 brala jako mailbox). + - --index-reset: pred zpracovanim schranky vymaze vsechny jeji emaily z PG + (force re-extract; pouzij kdyz povysis EXTRACTOR_VERSION nebo chces ciste). + - Vylepseny header per-mailbox: ukaze pocet v Mongu, v PG a k zpracovani. + +Zmeny v1.2 vs v1.1: + - S/MIME emaily: pokud unwrap_smime_v1.0 ulozil smime_body_text/smime_body_html, + pouzije se PREFEROVANE pred bezvyznamnym wrapper telem. + - body_source: nova hodnota "smime". + - EXTRACTOR_VERSION=1.2 -> vsechny existujici emaily v PG se preparsuji. + +Zmeny v1.1 vs v1.0: + - Fallback poradi rozsireno o body_text. + - body_source umi novou hodnotu "text" (plne plain-text telo, max 2 MB). + +Zdroj: + MongoDB 192.168.1.76 db=emaily kolekce= + (krome NON_MAILBOX_COLLECTIONS) + +Cil: + PostgreSQL 192.168.1.76 db=MongoEmaily tabulka=emails + tsvector config 'soubory' (sdileny - simple + unaccent) + +Inkrementalita: + Pokud (mailbox, message_id) jiz existuje a extractor_version je aktualni + a modified_at v Mongo neni novejsi -> skip. Pri zmene verze extractoru + se vse preparsuje. --index-reset to obejde a smaze PG pred behom. + +Spusteni: + python enrich_fulltext_emails_v1.4.py # vsechny schranky + python enrich_fulltext_emails_v1.4.py --mailbox ordinace@buzalkova.cz + python enrich_fulltext_emails_v1.4.py --limit 500 # test + python enrich_fulltext_emails_v1.4.py --mailbox X --index-reset # smaze PG schranky a re-extrahuje vsechno + python enrich_fulltext_emails_v1.4.py --index-reset # smaze CELY index a postavi znovu (POMALE!) +============================================================================== +""" + +from __future__ import annotations + +import argparse +import re +import sys +import time +import traceback +from datetime import datetime, timezone +from typing import Optional + +import psycopg +from bs4 import BeautifulSoup +from pymongo import MongoClient + +# --- konfigurace ------------------------------------------------------------ +MONGO_URI = "mongodb://192.168.1.76:27017" +MONGO_DB = "emaily" + +PG_DSN = ("host=192.168.1.76 port=5432 dbname=MongoEmaily " + "user=vladimir.buzalka password=Vlado7309208104++") + +EXTRACTOR_VERSION = "1.2" # NEMENIT pokud nemenis fallback logiku! + +MAX_TEXT_BYTES = 5 * 1024 * 1024 # plain text max 5 MB + +# Kolekce v `emaily` ktere NEJSOU mailboxy (nezpracovavame) +# (jnj_messages + jnj_sync_state = pomocne kolekce JNJ folder trackingu) +NON_MAILBOX_COLLECTIONS = {"attachments_index", "sync_state", + "jnj_messages", "jnj_sync_state"} + +BATCH_SIZE = 100 + + +# --- SCHEMA ----------------------------------------------------------------- + +SCHEMA_SQL = """ +CREATE EXTENSION IF NOT EXISTS unaccent; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = 'soubory') THEN + CREATE TEXT SEARCH CONFIGURATION soubory ( COPY = simple ); + ALTER TEXT SEARCH CONFIGURATION soubory + ALTER MAPPING FOR hword, hword_part, word + WITH unaccent, simple; + END IF; +END$$; + +CREATE TABLE IF NOT EXISTS emails ( + id BIGSERIAL PRIMARY KEY, + mailbox TEXT NOT NULL, + message_id TEXT NOT NULL, + graph_id TEXT, + conversation_id TEXT, + folder_path TEXT, + subject TEXT, + sender_email TEXT, + sender_name TEXT, + to_addrs TEXT, + cc_addrs TEXT, + bcc_addrs TEXT, + sent_at TIMESTAMPTZ, + received_at TIMESTAMPTZ, + modified_at TIMESTAMPTZ, + is_read BOOLEAN, + is_draft BOOLEAN, + has_attachments BOOLEAN, + attachment_count INT, + attachments_summary TEXT, + body TEXT, + body_length INT, + body_source TEXT, -- 'html' | 'preview' | 'empty' + tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('soubory'::regconfig, + left( + coalesce(subject, '') || ' ' || + coalesce(sender_email, '') || ' ' || + coalesce(sender_name, '') || ' ' || + coalesce(to_addrs, '') || ' ' || + coalesce(cc_addrs, '') || ' ' || + coalesce(attachments_summary, '') || ' ' || + coalesce(body, ''), + 800000) + ) + ) STORED, + extracted_at TIMESTAMPTZ DEFAULT now(), + extractor_version TEXT, + ok BOOLEAN, + error TEXT, + UNIQUE (mailbox, message_id) +); + +CREATE INDEX IF NOT EXISTS emails_tsv_gin ON emails USING gin(tsv); +CREATE INDEX IF NOT EXISTS emails_subject_trgm ON emails USING gin(subject gin_trgm_ops); +CREATE INDEX IF NOT EXISTS emails_sender_email_idx ON emails(sender_email); +CREATE INDEX IF NOT EXISTS emails_mailbox_idx ON emails(mailbox); +CREATE INDEX IF NOT EXISTS emails_received_idx ON emails(received_at DESC); +CREATE INDEX IF NOT EXISTS emails_conv_idx ON emails(conversation_id); +""" + + +# --- HELPERY ---------------------------------------------------------------- + +_CTRL_RX = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]") +_WS_RX = re.compile(r"[ \t]+") +_NL_RX = re.compile(r"\n{3,}") +# Osamocene surrogate (\ud800-\udfff) jsou neplatne v UTF-8 -> psycopg pri zapisu +# vyhodi UnicodeEncodeError ("surrogates not allowed") a shodi celou davku. +# Vznikaji ze spatne dekodovanych tel (napr. nektere JNJ .msg). Nahradime je U+FFFD. +_SURROGATE_RX = re.compile(r"[\ud800-\udfff]") + + +def _clean_for_pg(s: str) -> str: + if not s: + return "" + s = _CTRL_RX.sub("", s) + if _SURROGATE_RX.search(s): + s = _SURROGATE_RX.sub("�", s) + return s + + +def _truncate(s: str) -> str: + s = _clean_for_pg(s or "") + if not s: + return "" + b = s.encode("utf-8", errors="replace") + if len(b) <= MAX_TEXT_BYTES: + return s + return b[:MAX_TEXT_BYTES].decode("utf-8", errors="ignore") + + +def html_to_text(html: str) -> str: + if not html: + return "" + try: + soup = BeautifulSoup(html, "lxml") + except Exception: + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "head"]): + tag.decompose() + text = soup.get_text(separator="\n") + lines = [_WS_RX.sub(" ", ln).strip() for ln in text.split("\n")] + text = "\n".join(ln for ln in lines if ln) + text = _NL_RX.sub("\n\n", text) + return text + + +def fmt_recipients(recipients: list, kind: str) -> str: + if not recipients: + return "" + out = [] + for r in recipients: + if not isinstance(r, dict): + continue + if r.get("type") != kind: + continue + name = (r.get("name") or "").strip() + email = (r.get("email") or "").strip() + if name and email: + out.append(f"{name} <{email}>") + elif email: + out.append(email) + elif name: + out.append(name) + return "; ".join(out) + + +def fmt_attachments(attachments: list) -> str: + if not attachments: + return "" + out = [] + for a in attachments[:20]: + if not isinstance(a, dict): + continue + name = a.get("name") or a.get("filename") or "" + if name: + out.append(name) + return " | ".join(out) + + +def _short(s, n=60): + if not s: + return "" + s = str(s).replace("\n", " ").strip() + return s if len(s) <= n else s[:n] + "..." + + +def _now() -> datetime: + return datetime.now(tz=timezone.utc) + + +def _aware_utc(dt: Optional[datetime]) -> Optional[datetime]: + """Sjednoceni: PG TIMESTAMPTZ -> tz-aware UTC; Mongo datetime -> naive (UTC). + Vrati tz-aware UTC datetime nebo None.""" + if dt is None: + return None + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +# --- HLAVNI SMYCKA ---------------------------------------------------------- + +def process_mailbox(pg: psycopg.Connection, mongo_coll, mailbox: str, + limit: Optional[int] = None, + index_reset: bool = False) -> dict: + # --index-reset: smaz vse pro tuto schranku v PG + if index_reset: + with pg.cursor() as cur: + cur.execute("DELETE FROM emails WHERE mailbox = %s", (mailbox,)) + deleted = cur.rowcount + pg.commit() + print(f"[{mailbox}] --index-reset: smazano {deleted} radku v PG") + + # existujici zaznamy v PG (rychly inkrementalni lookup) + # tuple = (extractor_version, ok, body_source) + with pg.cursor() as cur: + cur.execute( + "SELECT message_id, extractor_version, ok, body_source " + "FROM emails WHERE mailbox = %s", + (mailbox,), + ) + existing = {row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()} + + mongo_total = mongo_coll.estimated_document_count() + pg_total = len(existing) + pg_uptodate = sum(1 for v in existing.values() + if v[0] == EXTRACTOR_VERSION and v[1]) + to_process_estimate = mongo_total - pg_uptodate + print(f"\n========== {mailbox} ==========") + print(f" v Mongu: {mongo_total}") + print(f" v PG: {pg_total} (z toho ext_v={EXTRACTOR_VERSION} & ok=true: {pg_uptodate})") + print(f" k zpracovani: ~{to_process_estimate}{' (limit=' + str(limit) + ')' if limit else ''}") + + if to_process_estimate <= 0 and not index_reset and not limit: + print(" Nic noveho ke zpracovani.") + return {"mailbox": mailbox, "processed": 0, "ok": 0, "errors": 0, + "skipped": pg_uptodate, "empty_body": 0} + + proj = { + "_id": 1, "graph_id": 1, "conversation_id": 1, "folder_path": 1, + "subject": 1, "sender": 1, "recipients": 1, + "sent_at": 1, "received_at": 1, "modified_at": 1, + "is_read": 1, "is_draft": 1, + "has_attachments": 1, "attachment_count": 1, "attachments": 1, + "body_html": 1, "body_text": 1, "body_preview": 1, + "smime_unwrapped": 1, "smime_body_text": 1, "smime_body_html": 1, + "smime_subject": 1, "smime_inner_attachments": 1, + } + cursor = mongo_coll.find({}, proj, no_cursor_timeout=True) + if limit: + cursor = cursor.limit(limit) + + processed = ok = errors = skipped = empty_body = 0 + queue: list[dict] = [] + n = 0 + + try: + for doc in cursor: + n += 1 + msg_id = doc.get("_id") or "" + prev = existing.get(msg_id) # (extractor_version, ok, body_source) + mongo_mtime = doc.get("modified_at") + + # Skip kdyz PG ma stejnou EV a ok=true. + # Vyjimka: smime_unwrapped v Mongu, ale PG body_source != 'smime' + # -> unwrap_smime pridal rozbaleny text az po enrichu -> re-enrich. + if prev and prev[0] == EXTRACTOR_VERSION and prev[1]: + needs_smime_reindex = ( + bool(doc.get("smime_unwrapped")) + and prev[2] != "smime" + ) + if not needs_smime_reindex: + skipped += 1 + continue + + sender = doc.get("sender") or {} + recipients = doc.get("recipients") or [] + attachments = doc.get("attachments") or [] + inner = doc.get("smime_inner_attachments") or [] + if inner: + attachments = list(attachments) + [ + {"filename": (a.get("filename") or "") + " [smime]"} + for a in inner if a.get("filename") + ] + + row = { + "mailbox": mailbox, + "message_id": msg_id, + "graph_id": doc.get("graph_id"), + "conversation_id": doc.get("conversation_id"), + "folder_path": doc.get("folder_path"), + "subject": doc.get("subject") or "", + "sender_email": sender.get("email"), + "sender_name": sender.get("name"), + "to_addrs": fmt_recipients(recipients, "to"), + "cc_addrs": fmt_recipients(recipients, "cc"), + "bcc_addrs": fmt_recipients(recipients, "bcc"), + # Vsechny timestampy z Monga jsou naive ale interpretovany jako UTC. + # Tagneme je tz-aware aby PG TIMESTAMPTZ ulozil spravnou UTC hodnotu + # a nepocital posun podle session timezone. + "sent_at": _aware_utc(doc.get("sent_at")), + "received_at": _aware_utc(doc.get("received_at")), + "modified_at": _aware_utc(mongo_mtime), + "is_read": doc.get("is_read"), + "is_draft": doc.get("is_draft"), + "has_attachments": doc.get("has_attachments"), + "attachment_count": doc.get("attachment_count"), + "attachments_summary": fmt_attachments(attachments), + "body": None, + "body_length": 0, + "body_source": "empty", + "extracted_at": _now(), + "extractor_version": EXTRACTOR_VERSION, + "ok": False, + "error": None, + } + + status = "OK "; detail = "" + try: + text = "" + if doc.get("smime_unwrapped"): + s_text = doc.get("smime_body_text") or "" + s_html = doc.get("smime_body_html") or "" + s_html_text = html_to_text(s_html) if s_html else "" + combined = "\n\n".join(p for p in (s_text, s_html_text) if p) + s_subject = doc.get("smime_subject") or "" + if s_subject: + combined = f"Subject: {s_subject}\n\n{combined}" + if combined: + text = combined + row["body_source"] = "smime" + if not text: + html = doc.get("body_html") or "" + h_text = html_to_text(html) if html else "" + if h_text: + text = h_text + row["body_source"] = "html" + if not text: + plain = doc.get("body_text") or "" + if plain: + text = plain + row["body_source"] = "text" + if not text: + preview = doc.get("body_preview") or "" + if preview: + text = preview + row["body_source"] = "preview" + if not text: + row["body_source"] = "empty" + empty_body += 1 + body = _truncate(text) + row["body"] = body if body else None + row["body_length"] = len(body) + row["ok"] = True + ok += 1 + detail = f"{len(body)} znaku {_short(body, 60)!r}" + except Exception as e: + row["error"] = f"{type(e).__name__}: {e}"[:500] + status = "ERR"; detail = row["error"][:80]; errors += 1 + + queue.append(row) + processed += 1 + + if processed % 200 == 0 or processed == 1: + subj = _short(row["subject"], 50) + print(f" [{n:>6}|p={processed:>5}] {status} {row['body_source']:<7} " + f"{row['body_length']:>7}ch | {subj}", flush=True) + + if len(queue) >= BATCH_SIZE: + _flush(pg, queue); queue.clear() + finally: + cursor.close() + + if queue: + _flush(pg, queue) + + return {"mailbox": mailbox, "processed": processed, "ok": ok, + "errors": errors, "skipped": skipped, "empty_body": empty_body} + + +UPSERT_SQL = """ +INSERT INTO emails + (mailbox, message_id, graph_id, conversation_id, folder_path, + subject, sender_email, sender_name, to_addrs, cc_addrs, bcc_addrs, + sent_at, received_at, modified_at, is_read, is_draft, + has_attachments, attachment_count, attachments_summary, + body, body_length, body_source, + extracted_at, extractor_version, ok, error) +VALUES + (%(mailbox)s, %(message_id)s, %(graph_id)s, %(conversation_id)s, %(folder_path)s, + %(subject)s, %(sender_email)s, %(sender_name)s, %(to_addrs)s, %(cc_addrs)s, %(bcc_addrs)s, + %(sent_at)s, %(received_at)s, %(modified_at)s, %(is_read)s, %(is_draft)s, + %(has_attachments)s, %(attachment_count)s, %(attachments_summary)s, + %(body)s, %(body_length)s, %(body_source)s, + %(extracted_at)s, %(extractor_version)s, %(ok)s, %(error)s) +ON CONFLICT (mailbox, message_id) DO UPDATE SET + graph_id = EXCLUDED.graph_id, + conversation_id = EXCLUDED.conversation_id, + folder_path = EXCLUDED.folder_path, + subject = EXCLUDED.subject, + sender_email = EXCLUDED.sender_email, + sender_name = EXCLUDED.sender_name, + to_addrs = EXCLUDED.to_addrs, + cc_addrs = EXCLUDED.cc_addrs, + bcc_addrs = EXCLUDED.bcc_addrs, + sent_at = EXCLUDED.sent_at, + received_at = EXCLUDED.received_at, + modified_at = EXCLUDED.modified_at, + is_read = EXCLUDED.is_read, + is_draft = EXCLUDED.is_draft, + has_attachments = EXCLUDED.has_attachments, + attachment_count = EXCLUDED.attachment_count, + attachments_summary = EXCLUDED.attachments_summary, + body = EXCLUDED.body, + body_length = EXCLUDED.body_length, + body_source = EXCLUDED.body_source, + extracted_at = EXCLUDED.extracted_at, + extractor_version = EXCLUDED.extractor_version, + ok = EXCLUDED.ok, + error = EXCLUDED.error +""" + + +def _flush(pg: psycopg.Connection, rows: list[dict]) -> None: + for r in rows: + for k in ("subject", "sender_email", "sender_name", "to_addrs", "cc_addrs", + "bcc_addrs", "attachments_summary", "body", "error", "folder_path"): + if r.get(k): + r[k] = _clean_for_pg(r[k]) + with pg.cursor() as cur: + cur.executemany(UPSERT_SQL, rows) + pg.commit() + + +def discover_mailboxes(db) -> list[str]: + out = [] + for name in sorted(db.list_collection_names()): + if name in NON_MAILBOX_COLLECTIONS: + continue + out.append(name) + return out + + +def main() -> int: + ap = argparse.ArgumentParser(description="enrich_fulltext_emails v1.4") + ap.add_argument("--mailbox", default="", + help="Jedna konkretni schranka. Bez argumentu projede vsechny.") + ap.add_argument("--limit", type=int, + help="Limit emailu na schranku (test)") + ap.add_argument("--index-reset", action="store_true", + help="Pred zpracovanim schranky vymaze vsechny jeji emaily z PG " + "(force re-extract). Bez --mailbox SMAZE CELY index.") + args = ap.parse_args() + + t0 = time.time() + print(f"=== enrich_fulltext_emails v1.4 ===") + print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + print("\nPripojuji se k PostgreSQL...") + pg = psycopg.connect(PG_DSN, connect_timeout=10) + with pg.cursor() as cur: + cur.execute(SCHEMA_SQL) + pg.commit() + print(" Schema OK.") + + print("Pripojuji se k MongoDB...") + mongo = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + mongo.admin.command("ping") + db = mongo[MONGO_DB] + print(" MongoDB OK.") + + if args.mailbox: + mailboxes = [args.mailbox] + else: + mailboxes = discover_mailboxes(db) + print(f"\nSchranky ke zpracovani ({len(mailboxes)}):") + for mb in mailboxes: + print(f" - {mb}") + + if args.index_reset and not args.mailbox: + print(f"\n!!! --index-reset bez --mailbox => SMAZE CELY INDEX ({len(mailboxes)} schranek) !!!") + + results = [] + for mb in mailboxes: + try: + results.append(process_mailbox(pg, db[mb], mb, + limit=args.limit, + index_reset=args.index_reset)) + except Exception as e: + traceback.print_exc() + print(f" FATAL pri zpracovani {mb}: {e}") + results.append({"mailbox": mb, "processed": 0, "ok": 0, + "errors": 1, "skipped": 0, "empty_body": 0}) + + pg.close() + + print("\n" + "="*60) + print("=== SHRNUTI ===") + grand = {"processed": 0, "ok": 0, "errors": 0, "skipped": 0, "empty_body": 0} + for r in results: + print(f" {r['mailbox']:40} processed={r['processed']:>5} ok={r['ok']:>5} " + f"errors={r['errors']:>3} skipped={r['skipped']:>6} empty={r['empty_body']:>4}") + for k in grand: + grand[k] += r.get(k, 0) + print(f" {'TOTAL':40} processed={grand['processed']:>5} ok={grand['ok']:>5} " + f"errors={grand['errors']:>3} skipped={grand['skipped']:>6} empty={grand['empty_body']:>4}") + print(f"\nCelkem trvalo: {time.time() - t0:.1f} s") + print(f"Konec: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + # exit code: 0 jen kdyz vsechny schranky probehly bez chyby + return 1 if grand["errors"] > 0 else 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print("\nPreruseno uzivatelem") + except Exception: + traceback.print_exc() + sys.exit(1)