keyinnovations commited on
Commit
d2db761
Β·
verified Β·
1 Parent(s): 7be3c40

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +288 -0
  2. requirements.txt +0 -0
app.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import json
3
+ import requests
4
+ import streamlit.components.v1 as components
5
+ import datetime
6
+
7
+
8
+ st.set_page_config(
9
+ page_title="Key Innovations Inc.",
10
+ page_icon="https://tscstatic.keyinnovations.ca/logo/logo_1T895ONEIG.png",
11
+ layout="wide"
12
+ )
13
+ st.image("https://tscstatic.keyinnovations.ca/logo/logo_1T895ONEIG.png", width=150)
14
+ #st.title('Syncore Calendar')
15
+ st.subheader('Syncore CalendarπŸ“…')
16
+
17
+
18
+ # API Endpoint
19
+ ALL_JOBS_URL = "https://api.syncore.app/v2/orders/jobs?date_from={}&date_to={}&limit=1000"
20
+ SALES_ORDERS_URL = "https://api.syncore.app/v2/orders/jobs/{}/salesorders"
21
+
22
+ # API Headers
23
+ HEADERS = {
24
+ "x-api-key": "69c23811-1073-4c1c-9902-8d552703c460",
25
+ "Accept": "application/json"
26
+ }
27
+
28
+ # Function to fetch jobs for the selected month with pagination
29
+ def fetch_jobs(selected_year, selected_month):
30
+ first_day = datetime.date(selected_year, selected_month, 1)
31
+ last_day = (first_day + datetime.timedelta(days=32)).replace(day=1) - datetime.timedelta(days=1)
32
+
33
+ date_from, date_to = first_day.strftime("%Y-%m-%d"), last_day.strftime("%Y-%m-%d")
34
+
35
+ all_jobs = []
36
+ page = 1
37
+
38
+ while True:
39
+ url = f"https://api.syncore.app/v2/orders/jobs?date_from={date_from}&date_to={date_to}&limit=100&page={page}"
40
+ response = requests.get(url, headers=HEADERS)
41
+
42
+ if response.status_code == 200:
43
+ data = response.json()
44
+ new_jobs = data.get("jobs", [])
45
+
46
+ if not new_jobs:
47
+ break
48
+
49
+ all_jobs.extend(new_jobs)
50
+ page += 1
51
+
52
+ else:
53
+ st.error(f"API Error (Fetching Jobs): {response.status_code} - {response.text}")
54
+ break
55
+
56
+ return [job["id"] for job in all_jobs]
57
+
58
+ # Function to fetch sales orders for jobs
59
+ def fetch_sales_orders_for_jobs(job_ids):
60
+ orders = []
61
+ priority_colors = {"High": "#dc2626", "Medium": "#2563eb", "Low": "#22c55e"}
62
+ today = datetime.date.today()
63
+
64
+ processed_alerts = set()
65
+
66
+ for job_id in job_ids:
67
+ url = SALES_ORDERS_URL.format(job_id)
68
+ response = requests.get(url, headers=HEADERS)
69
+
70
+ if response.status_code == 200:
71
+ data = response.json()
72
+ if "salesorders" in data:
73
+ for order in data["salesorders"]:
74
+ job_created = order.get("date", "Not Available")
75
+ estimated_delivery_date = order.get("estimated_delivery_date", "Not Available")
76
+ job_number = order.get("job_number", "NA")
77
+
78
+ # Extract supplier names
79
+ supplier_names = list({
80
+ item["supplier"]["name"]
81
+ for item in order.get("line_items", []) if item.get("supplier")
82
+ })
83
+ if len(supplier_names) < 2:
84
+ #print(len(supplier_names))
85
+ continue
86
+
87
+ # Assign priority based on estimated delivery date
88
+ priority = "Low"
89
+ if estimated_delivery_date != "Not Available":
90
+ estimated_date = datetime.datetime.strptime(estimated_delivery_date, "%Y-%m-%d").date()
91
+ days_until_delivery = (estimated_date - today).days
92
+ if days_until_delivery <= 15:
93
+ priority = "High"
94
+ elif 16 <= days_until_delivery <= 30:
95
+ priority = "Medium"
96
+
97
+ # Add job event
98
+ if job_created != "Not Available":
99
+ job_event = {
100
+ "id": f"{job_number}-job",
101
+ "title": f"{job_number}<br><strong>πŸ›  Suppliers:</strong> {', '.join(supplier_names)}<br>πŸ“… {job_created} β†’ 🚚 {estimated_delivery_date}",
102
+ "start": job_created,
103
+ "color": priority_colors.get(priority, "#2563eb")
104
+ }
105
+ orders.append(job_event)
106
+
107
+ # Add delivery alert
108
+ if estimated_delivery_date != "Not Available" and job_id not in processed_alerts:
109
+ delivery_alert_event = {
110
+ "id": f"{job_number}-alert",
111
+ "title": f"{job_number}<br>🚨 Delivery Day",
112
+ "start": estimated_delivery_date,
113
+ "color": "#ff9800"
114
+ }
115
+ orders.append(delivery_alert_event)
116
+ processed_alerts.add(job_id)
117
+
118
+ else:
119
+ st.error(f"API Fetch Error (Sales Orders for Job {job_id}): {response.status_code}")
120
+
121
+ return orders
122
+
123
+
124
+
125
+ # Handle Calendar Month Switching
126
+ if "selected_month" not in st.session_state:
127
+ today = datetime.date.today()
128
+ st.session_state.selected_year = today.year
129
+ st.session_state.selected_month = today.month
130
+
131
+ params = st.query_params
132
+
133
+ if "year" in params and "month" in params:
134
+ st.session_state.selected_year = int(params["year"][0])
135
+ st.session_state.selected_month = int(params["month"][0])
136
+
137
+ # Show loading animation
138
+ with st.spinner("Fetching jobs... Please wait."):
139
+ job_ids = fetch_jobs(st.session_state.selected_year, st.session_state.selected_month)
140
+
141
+ with st.spinner("Processing sales orders... This may take up to 60 seconds"):
142
+ orders = fetch_sales_orders_for_jobs(job_ids)
143
+ st.balloons()
144
+
145
+ events_json = json.dumps(orders)
146
+
147
+ # Calendar with Priority Legend & Delivery Alerts
148
+ calendar_html = f"""
149
+ <!DOCTYPE html>
150
+ <html>
151
+ <head>
152
+ <link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css" rel="stylesheet">
153
+ <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js"></script>
154
+ <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales-all.min.js"></script>
155
+ <style>
156
+ body {{
157
+ font-family: 'Poppins', sans-serif;
158
+ background-color: #f8fafc;
159
+ display: flex;
160
+ flex-direction: column;
161
+ align-items: center;
162
+ justify-content: flex-start;
163
+ height: 100vh;
164
+ padding: 20px;
165
+ }}
166
+ /* πŸ“Œ Priority Legend Styling */
167
+ .priority-legend {{
168
+ display: flex;
169
+ justify-content: center;
170
+ align-items: center;
171
+ gap: 20px;
172
+ padding: 10px;
173
+ background: white;
174
+ border-radius: 8px;
175
+ margin-bottom: 15px;
176
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
177
+ width: 85vw;
178
+ max-width: 1200px;
179
+ }}
180
+ .priority-box {{
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ font-size: 14px;
185
+ font-weight: bold;
186
+ }}
187
+ .priority-box span {{
188
+ width: 16px;
189
+ height: 16px;
190
+ display: inline-block;
191
+ border-radius: 4px;
192
+ }}
193
+ #calendar-container {{
194
+ width: 100vw;
195
+ height: 100vh;
196
+ background: white;
197
+ box-shadow: none;
198
+ border-radius: 0;
199
+ padding: 0;
200
+ margin: 0;
201
+ display: flex;
202
+ justify-content: center;
203
+ align-items: center;
204
+ }}
205
+
206
+ #calendar {{
207
+ width: 95%;
208
+ height: 95%;
209
+ }}
210
+ .fc-event {{
211
+ border-radius: 6px;
212
+ padding: 6px;
213
+ font-size: 12px;
214
+ font-weight: bold;
215
+ white-space: normal;
216
+ text-align: left;
217
+ line-height: 1.5;
218
+ display: flex;
219
+ flex-direction: column;
220
+ justify-content: center;
221
+ background: rgba(0, 0, 0, 0.1);
222
+ border-left: 5px solid #2563eb;
223
+ color: black;
224
+ transition: all 0.2s ease-in-out;
225
+ box-shadow: 0px 3px 7px rgba(0, 0, 0, 0.12);
226
+ }}
227
+ .fc-event-title {{
228
+ padding: 6px;
229
+ background: white;
230
+ border-radius: 4px;
231
+ font-size: 12px;
232
+ font-weight: 600;
233
+ white-space: normal;
234
+ line-height: 1.4;
235
+ text-align: left;
236
+ }}
237
+ </style>
238
+ <script>
239
+ document.addEventListener('DOMContentLoaded', function() {{
240
+ var calendarEl = document.getElementById('calendar');
241
+ var calendar = new FullCalendar.Calendar(calendarEl, {{
242
+ initialView: 'dayGridMonth',
243
+ editable: true,
244
+ selectable: true,
245
+ height: '100%',
246
+ themeSystem: 'standard',
247
+ headerToolbar: {{
248
+ left: 'prev,next today',
249
+ center: 'title',
250
+ right: 'dayGridMonth,timeGridWeek,timeGridDay'
251
+ }},
252
+ eventContent: function(arg) {{
253
+ return {{ html: arg.event.title }};
254
+ }},
255
+ events: {events_json},
256
+ buttonIcons: true,
257
+ }});
258
+ calendar.render();
259
+ }});
260
+ </script>
261
+ </head>
262
+ <body>
263
+ <!-- πŸ“Œ Priority Legend Section (Now Outside Calendar) -->
264
+ <div class="priority-legend">
265
+ <div class="priority-box">
266
+ <span style="background-color: #dc2626;"></span> High Priority
267
+ </div>
268
+ <div class="priority-box">
269
+ <span style="background-color: #2563eb;"></span> Medium Priority
270
+ </div>
271
+ <div class="priority-box">
272
+ <span style="background-color: #22c55e;"></span> Low Priority
273
+ </div>
274
+ <div class="priority-box">
275
+ <span style="background-color: #ff9800;"></span> Delivery Alert
276
+ </div>
277
+ </div>
278
+
279
+ <!-- πŸ“Œ Calendar Container -->
280
+ <div id='calendar-container'>
281
+ <div id='calendar'></div>
282
+ </div>
283
+ </body>
284
+ </html>
285
+ """
286
+
287
+ # Display the FullCalendar.js
288
+ components.html(calendar_html, height=900, scrolling=False)
requirements.txt ADDED
Binary file (106 Bytes). View file