Dhom1 commited on
Commit
f3956eb
·
verified ·
1 Parent(s): 6afc103

Update src/ukg/schedule.py

Browse files
Files changed (1) hide show
  1. src/ukg/schedule.py +197 -107
src/ukg/schedule.py CHANGED
@@ -1,172 +1,262 @@
 
 
 
 
 
 
 
 
 
1
  import os
2
- import requests
3
- import pandas as pd
4
  import json
5
- import streamlit as st # إضافة هذا السطر
 
 
 
 
6
 
7
- def fetch_open_shifts(start_date="2000-01-01", end_date="3000-01-01", location_ids=None):
8
- if location_ids is None:
9
- location_ids = ["2401","2402","2953","2955","2927","2928","2401","2955"]
10
 
11
- url = "https://partnerdemo-019.cfn.mykronos.com/api/v1/scheduling/schedule/multi_read"
12
- headers = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  "Content-Type": "application/json",
14
- "appkey": os.environ.get("UKG_APP_KEY"),
15
- "Authorization": os.environ.get("UKG_AUTH_TOKEN")
16
  }
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  payload = {
19
  "select": ["OPENSHIFTS"],
20
  "where": {
21
  "locations": {
22
  "dateRange": {
23
  "startDate": start_date,
24
- "endDate": end_date
25
  },
26
  "includeEmployeeTransfer": False,
27
- "locations": {
28
- "ids": location_ids
29
- }
30
  }
31
- }
32
  }
33
 
34
  try:
35
- print("Payload being sent:")
36
- print(json.dumps(payload, indent=2))
37
- print("Headers:")
38
- print(headers)
39
-
40
  response = requests.post(url, headers=headers, json=payload)
41
- print("Status Code:", response.status_code)
42
- print("Raw Response:", response.text)
43
-
44
  response.raise_for_status()
45
  data = response.json()
46
-
47
  open_shifts = data.get("openShifts", [])
48
- print(f"Found {len(open_shifts)} open shifts")
49
-
50
- rows = []
51
  for shift in open_shifts:
52
- rows.append({
53
- "ID": shift.get("id"),
54
- "Start": shift.get("startDateTime"),
55
- "End": shift.get("endDateTime"),
56
- "Label": shift.get("label"),
57
- "Org Job": shift.get("segments", [{}])[0].get("orgJobRef", {}).get("qualifier", "") if shift.get("segments") else "",
58
- "Posted": shift.get("posted"),
59
- "Self Serviced": shift.get("selfServiced"),
60
- "Locked": shift.get("locked")
61
- })
62
-
 
 
 
 
 
63
  return pd.DataFrame(rows)
64
-
65
  except Exception as e:
66
- print(f"❌ UKG Open Shifts API call failed: {e}")
67
  return pd.DataFrame()
68
-
69
 
70
- # ---- UKG Location API FETCH FUNCTION ----
71
- def fetch_location_data():
72
- url = "https://partnerdemo-019.cfn.mykronos.com/api/v1/commons/locations/multi_read"
73
- headers = {
74
- "Content-Type": "application/json",
75
- "appkey": os.environ.get("UKG_APP_KEY"),
76
- "Authorization": os.environ.get("UKG_AUTH_TOKEN")
77
- }
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  payload = {
80
- "multiReadOptions": {
81
- "includeOrgPathDetails": True
82
- },
83
  "where": {
84
- "query": {
85
- "context": "ORG",
86
- "date": "2025-07-13",
87
- "q": "Medsurg"
88
- }
89
- }
90
  }
91
-
92
  try:
93
  response = requests.post(url, headers=headers, json=payload)
94
  response.raise_for_status()
95
  data = response.json()
96
-
97
- # استخراج المواقع
98
  rows = []
99
- for item in data:
100
- rows.append({
101
- "Node ID": item.get("nodeId", ""),
102
- "Name": item.get("name", ""),
103
- "Full Name": item.get("fullName", ""),
104
- "Description": item.get("description", ""),
105
- "Org Path": item.get("orgPath", ""),
106
- "Persistent ID": item.get("persistentId", "")
107
- })
108
-
 
109
  return pd.DataFrame(rows)
110
-
111
  except Exception as e:
112
  st.error(f"❌ UKG Location API call failed: {e}")
113
  return pd.DataFrame()
114
 
115
 
116
- import os
117
- import requests
118
- import pandas as pd
119
 
120
- # ---- Fetch employee data from UKG API ----
121
- def fetch_employees(employee_ids):
122
- UKG_BASE_URL = "https://partnerdemo-019.cfn.mykronos.com/api/v1/commons/persons/"
123
- headers = {
124
- "Content-Type": "application/json",
125
- "appkey": os.environ.get("UKG_APP_KEY"),
126
- "Authorization": f"Bearer {os.environ.get('UKG_AUTH_TOKEN')}"
127
- }
128
 
129
- def fetch_employee_data(emp_id):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  try:
131
- response = requests.get(f"{UKG_BASE_URL}{emp_id}", headers=headers)
132
- if response.status_code == 200:
133
- data = response.json()
134
- person_number = data.get("personInformation", {}).get("person", {}).get("personNumber", None)
135
- org_path = ""
136
- phone = ""
137
- full_name = ""
138
 
139
- primary_accounts = data.get("jobAssignment", {}).get("primaryLaborAccounts", [])
 
 
 
 
140
  if primary_accounts:
141
  org_path = primary_accounts[0].get("organizationPath", "")
142
 
 
 
143
  phones = data.get("personInformation", {}).get("telephoneNumbers", [])
144
  if phones:
145
  phone = phones[0].get("phoneNumber", "")
146
 
147
- full_name = data.get("personInformation", {}).get("person", {}).get("fullName", "")
148
-
149
  return {
150
  "personNumber": person_number,
151
  "organizationPath": org_path,
152
  "phoneNumber": phone,
153
- "fullName": full_name
154
  }
155
  else:
156
- print(f"⚠️ Could not fetch ID {emp_id}: {response.status_code}")
157
- return None
 
158
  except Exception as e:
159
- print(f"❌ Error for ID {emp_id}: {e}")
160
- return None
161
 
162
- employee_records = [emp for emp_id in employee_ids if (emp := fetch_employee_data(emp_id))]
163
- df_employees = pd.DataFrame(employee_records)
 
 
 
164
 
165
- # Extract job role from organizationPath
166
- df_employees["JobRole"] = df_employees["organizationPath"].apply(
167
- lambda x: x.split("/")[-1] if isinstance(x, str) else ""
168
- )
169
-
170
- return df_employees
 
 
 
 
 
171
 
 
 
 
 
 
172
 
 
 
 
1
+ """Helper functions for interacting with UKG (Kronos) APIs.
2
+
3
+ This module centralizes the logic for fetching open shifts, locations and
4
+ employee data from the UKG demo API. It also ensures that return values are
5
+ safe to consume within a Streamlit application by handling missing keys
6
+ gracefully and avoiding common exceptions such as ``KeyError`` when
7
+ expected columns are missing from returned data frames.
8
+ """
9
+
10
  import os
 
 
11
  import json
12
+ from typing import List, Optional, Iterable, Dict, Any
13
+
14
+ import pandas as pd
15
+ import requests
16
+ import streamlit as st
17
 
 
 
 
18
 
19
+ def _get_auth_header() -> Dict[str, str]:
20
+ """Construct a common authorization header for UKG API calls.
21
+
22
+ The UKG API uses two headers for authentication: ``appkey`` and
23
+ ``Authorization``. The latter expects the string ``Bearer `` followed by
24
+ the token. Both values are read from environment variables ``UKG_APP_KEY``
25
+ and ``UKG_AUTH_TOKEN``. If either variable is missing, a warning is
26
+ emitted via Streamlit.
27
+
28
+ Returns
29
+ -------
30
+ dict
31
+ Header dictionary suitable for passing to ``requests`` calls.
32
+ """
33
+ app_key = os.environ.get("UKG_APP_KEY")
34
+ token = os.environ.get("UKG_AUTH_TOKEN")
35
+ if not app_key or not token:
36
+ st.warning(
37
+ "UKG authentication variables (UKG_APP_KEY and/or UKG_AUTH_TOKEN) are"
38
+ " not set. API calls may fail."
39
+ )
40
+ return {
41
  "Content-Type": "application/json",
42
+ "appkey": app_key or "",
43
+ "Authorization": f"Bearer {token}" if token else "",
44
  }
45
 
46
+
47
+ def fetch_open_shifts(
48
+ start_date: str = "2000-01-01",
49
+ end_date: str = "3000-01-01",
50
+ location_ids: Optional[Iterable[str]] = None,
51
+ ) -> pd.DataFrame:
52
+ """Fetch open shift instances from the UKG demo API.
53
+
54
+ Parameters
55
+ ----------
56
+ start_date : str
57
+ ISO date (YYYY-MM-DD) for the beginning of the date range.
58
+ end_date : str
59
+ ISO date (YYYY-MM-DD) for the end of the date range.
60
+ location_ids : iterable of str, optional
61
+ A collection of location identifiers to filter the search. If not
62
+ provided, a default list of IDs is used.
63
+
64
+ Returns
65
+ -------
66
+ pandas.DataFrame
67
+ A DataFrame containing information about open shifts. If the API
68
+ returns no shifts or encounters an error, an empty DataFrame is
69
+ returned.
70
+ """
71
+ if location_ids is None:
72
+ location_ids = ["2401", "2402", "2953", "2955", "2927", "2928", "2401", "2955"]
73
+
74
+ url = (
75
+ "https://partnerdemo-019.cfn.mykronos.com/api/v1/"
76
+ "scheduling/schedule/multi_read"
77
+ )
78
+ headers = _get_auth_header()
79
+
80
  payload = {
81
  "select": ["OPENSHIFTS"],
82
  "where": {
83
  "locations": {
84
  "dateRange": {
85
  "startDate": start_date,
86
+ "endDate": end_date,
87
  },
88
  "includeEmployeeTransfer": False,
89
+ "locations": {"ids": list(location_ids)},
 
 
90
  }
91
+ },
92
  }
93
 
94
  try:
 
 
 
 
 
95
  response = requests.post(url, headers=headers, json=payload)
 
 
 
96
  response.raise_for_status()
97
  data = response.json()
 
98
  open_shifts = data.get("openShifts", [])
99
+ rows: List[Dict[str, Any]] = []
 
 
100
  for shift in open_shifts:
101
+ rows.append(
102
+ {
103
+ "ID": shift.get("id"),
104
+ "Start": shift.get("startDateTime"),
105
+ "End": shift.get("endDateTime"),
106
+ "Label": shift.get("label"),
107
+ "Org Job": shift.get("segments", [{}])[0]
108
+ .get("orgJobRef", {})
109
+ .get("qualifier", "")
110
+ if shift.get("segments")
111
+ else "",
112
+ "Posted": shift.get("posted"),
113
+ "Self Serviced": shift.get("selfServiced"),
114
+ "Locked": shift.get("locked"),
115
+ }
116
+ )
117
  return pd.DataFrame(rows)
 
118
  except Exception as e:
119
+ st.error(f"❌ UKG Open Shifts API call failed: {e}")
120
  return pd.DataFrame()
 
121
 
 
 
 
 
 
 
 
 
122
 
123
+ def fetch_location_data(date: str = "2025-07-13", query: str = "Medsurg") -> pd.DataFrame:
124
+ """Fetch location information from the UKG demo API.
125
+
126
+ Parameters
127
+ ----------
128
+ date : str
129
+ The effective date for the location context in ISO format.
130
+ query : str
131
+ A search string to filter locations.
132
+
133
+ Returns
134
+ -------
135
+ pandas.DataFrame
136
+ A DataFrame containing location attributes, or an empty DataFrame
137
+ if the call fails.
138
+ """
139
+ url = (
140
+ "https://partnerdemo-019.cfn.mykronos.com/api/v1/"
141
+ "commons/locations/multi_read"
142
+ )
143
+ headers = _get_auth_header()
144
  payload = {
145
+ "multiReadOptions": {"includeOrgPathDetails": True},
 
 
146
  "where": {
147
+ "query": {"context": "ORG", "date": date, "q": query}
148
+ },
 
 
 
 
149
  }
 
150
  try:
151
  response = requests.post(url, headers=headers, json=payload)
152
  response.raise_for_status()
153
  data = response.json()
154
+ # The API returns a list of location objects
 
155
  rows = []
156
+ for item in data if isinstance(data, list) else data.get("locations", []):
157
+ rows.append(
158
+ {
159
+ "Node ID": item.get("nodeId", ""),
160
+ "Name": item.get("name", ""),
161
+ "Full Name": item.get("fullName", ""),
162
+ "Description": item.get("description", ""),
163
+ "Org Path": item.get("orgPath", ""),
164
+ "Persistent ID": item.get("persistentId", ""),
165
+ }
166
+ )
167
  return pd.DataFrame(rows)
 
168
  except Exception as e:
169
  st.error(f"❌ UKG Location API call failed: {e}")
170
  return pd.DataFrame()
171
 
172
 
173
+ def fetch_employees(employee_ids: Iterable[int]) -> pd.DataFrame:
174
+ """Fetch employee information from the UKG demo API.
 
175
 
176
+ For each employee ID provided, this function queries the person information
177
+ endpoint and extracts basic details such as the person number, full name,
178
+ phone number and organizational path. It also derives a ``JobRole`` from
179
+ the last segment of the ``organizationPath``. When no records are returned
180
+ or a particular key is missing, the returned DataFrame still contains
181
+ the expected columns to avoid ``KeyError`` exceptions downstream.
 
 
182
 
183
+ Parameters
184
+ ----------
185
+ employee_ids : iterable of int
186
+ A list of employee identifiers.
187
+
188
+ Returns
189
+ -------
190
+ pandas.DataFrame
191
+ A DataFrame with one row per successfully fetched employee. If no
192
+ data is available, a DataFrame with the appropriate columns but no
193
+ rows is returned.
194
+ """
195
+ base_url = (
196
+ "https://partnerdemo-019.cfn.mykronos.com/api/v1/commons/persons/"
197
+ )
198
+ headers = _get_auth_header()
199
+
200
+ def fetch_employee_data(emp_id: int) -> Optional[Dict[str, Any]]:
201
  try:
202
+ resp = requests.get(f"{base_url}{emp_id}", headers=headers)
203
+ if resp.status_code == 200:
204
+ data = resp.json()
205
+ person_info = data.get("personInformation", {}).get("person", {})
206
+ person_number = person_info.get("personNumber")
207
+ full_name = person_info.get("fullName", "")
 
208
 
209
+ # Extract organization path from primary labor accounts
210
+ org_path = ""
211
+ primary_accounts = (
212
+ data.get("jobAssignment", {}).get("primaryLaborAccounts", [])
213
+ )
214
  if primary_accounts:
215
  org_path = primary_accounts[0].get("organizationPath", "")
216
 
217
+ # Extract first phone number if present
218
+ phone = ""
219
  phones = data.get("personInformation", {}).get("telephoneNumbers", [])
220
  if phones:
221
  phone = phones[0].get("phoneNumber", "")
222
 
 
 
223
  return {
224
  "personNumber": person_number,
225
  "organizationPath": org_path,
226
  "phoneNumber": phone,
227
+ "fullName": full_name,
228
  }
229
  else:
230
+ st.warning(
231
+ f"⚠️ Could not fetch employee {emp_id}: {resp.status_code}"
232
+ )
233
  except Exception as e:
234
+ st.error(f"❌ Error fetching employee {emp_id}: {e}")
235
+ return None
236
 
237
+ records: List[Dict[str, Any]] = []
238
+ for emp_id in employee_ids:
239
+ data = fetch_employee_data(emp_id)
240
+ if data:
241
+ records.append(data)
242
 
243
+ # Build DataFrame with guaranteed columns
244
+ df_employees = pd.DataFrame(records)
245
+ # Ensure mandatory columns exist even if DataFrame is empty
246
+ for col in [
247
+ "personNumber",
248
+ "organizationPath",
249
+ "phoneNumber",
250
+ "fullName",
251
+ ]:
252
+ if col not in df_employees.columns:
253
+ df_employees[col] = []
254
 
255
+ # Derive JobRole from organizationPath safely
256
+ def derive_role(path: Any) -> str:
257
+ if isinstance(path, str) and path:
258
+ return path.split("/")[-1]
259
+ return ""
260
 
261
+ df_employees["JobRole"] = df_employees["organizationPath"].apply(derive_role)
262
+ return df_employees