P
File size: 28,110 Bytes
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
9277f15
 
 
 
 
 
 
 
 
e993a5c
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
 
 
9277f15
e993a5c
 
9277f15
e993a5c
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
9277f15
 
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
 
e993a5c
9277f15
e993a5c
 
 
9277f15
 
 
 
 
 
 
 
e993a5c
 
 
 
 
 
 
 
9277f15
e993a5c
 
2acb8af
e993a5c
2acb8af
e993a5c
 
 
9277f15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e993a5c
 
9277f15
e993a5c
 
9277f15
e993a5c
 
 
 
 
 
 
 
9277f15
e993a5c
 
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9277f15
e993a5c
 
 
 
 
 
 
d518b14
9277f15
e993a5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
import os
import pandas as pd
import numpy as np
import gradio as gr
from collections import defaultdict
from openpyxl import Workbook
from openpyxl.styles import PatternFill
import shutil
import zipfile

# ------ CONFIGURATION ------
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
time_slots = ["8-9", "9-10", "10-11", "11-12", "12-1", "1-2", "2-3", "3-4", "4-5", "5-6", "6-7"]
lunch_slot = "1-2"
no_class_3rd_4th = {"Tuesday": ["4-5"], "Friday": ["5-6"], "Monday":["6-7"],"Tuesday":["6-7"],"Thursday":["6-7"]}
seminar_hall_blocked_slots = ["3-4", "4-5", "5-6"]

block_3rd = {"Monday", "Wednesday", "Friday"}
block_3rd_slots = ["2-3", "3-4", "4-5"]
block_4th = {"Monday", "Wednesday", "Thursday"}
block_4th_slots = ["10-11", "11-12", "12-1"]

restricted_5_6_slots = {
    1: {"Monday", "Wednesday", "Friday"},
    2: {"Monday", "Tuesday", "Wednesday"}
}

bcp_prefixes = ["phy", "msp", "chy", "msc", "bio", "msb", "i2c", "i2p", "i2b"]

def ordinal(n):
    mapping = ["First", "Second", "Third", "Fourth", "Fifth"]
    return mapping[n - 1] if 1 <= n <= 5 else f"Year{n or 'Unknown'}"

def extract_year_from_code(code):
    code = code.lower()
    numeric = ''.join(filter(str.isdigit, code))
    if not numeric:
        return None
    first_digit = int(numeric[0])
    if code.startswith(("msp", "msb", "msc", "msm")):
        return 1 if first_digit == 3 else 2 if first_digit == 4 else None
    if 1 <= first_digit <= 5:
        return first_digit
    return None

def is_bcp_major(code):
    return any(code.startswith(prefix) for prefix in bcp_prefixes)

def is_bcp_blocked(code, day, slot):
    if not is_bcp_major(code):
        return False
    year = extract_year_from_code(code)
    if year == 3 and day in block_3rd and slot in block_3rd_slots:
        return True
    if year == 4 and day in block_4th and slot in block_4th_slots:
        return True
    return False

def parse_ltpc(ltpc):
    ltpc = str(ltpc).strip()
    if '-' in ltpc:
        parts = list(map(int, ltpc.split('-')))
        while len(parts) < 4:
            parts.append(0)
        return tuple(parts[:4])
    digits = ''.join(filter(str.isdigit, ltpc)).zfill(4)
    return int(digits[0]), int(digits[1]), int(digits[2]), int(digits[3])

def find_valid_lecture_days(L, days_available):
    if L == 1:
        return [[d] for d in days_available]
    elif L == 2:
        return [[d1, d2] for d1 in days_available for d2 in days_available if abs(d1 - d2) >= 2 and d1 < d2]
    elif L == 3:
        options = []
        for d1 in days_available:
            for d2 in days_available:
                for d3 in days_available:
                    if d1 < d2 < d3:
                        if d2 - d1 == 1 and d3 - d2 >= 2:
                            options.append([d1, d2, d3])
                        elif d3 - d2 == 1 and d2 - d1 >= 2:
                            options.append([d1, d2, d3])
        return options
    return []

def generate_central_timetable_excel(timetable, tutorial_groups=None, lab_groups=None, output_path="Central_Timetable.xlsx"):
    from openpyxl import Workbook
    from openpyxl.styles import Alignment

    wb = Workbook()
    ws = wb.active
    ws.title = "Central Timetable"

    # Header Row
    ws.append(["Day"] + time_slots)

    for day in days:
        row = [day]
        for slot in time_slots:
            entries = []

            # Collect normal lecture/tutorial entries (years 1–5)
            for year in sorted(timetable.keys()):
                scheduled = timetable[year][day][slot]
                entries.extend(scheduled)

            # βœ… NEW: Append 1st-year tutorial groups with LG tags
            if tutorial_groups:
                for lg, sessions in tutorial_groups.items():
                    for d, s, r, cname in sessions:
                        if d == day and s == slot:
                            entries.append(f"{cname} (Tutorial) @ {r} ({lg})")

            # βœ… NEW: Append 1st-year lab groups with LG tags
            if lab_groups:
                for lg, sessions in lab_groups.items():
                    for d, slot_block, r, cname in sessions:
                        if d == day and slot in slot_block:
                            entries.append(f"{cname} (Lab) @ {r} ({lg})")

            row.append("\n".join(entries))
        ws.append(row)

    # βœ… Optional formatting
    for col in ws.columns:
        for cell in col:
            cell.alignment = Alignment(wrapText=True)

    # Set appropriate column widths
    for i in range(2, len(time_slots) + 2):
        ws.column_dimensions[chr(64 + i)].width = 30

    wb.save(output_path)



def generate_available_halls_report(room_slots, output_path="Available_Halls.xlsx"):
    from openpyxl import Workbook

    wb = Workbook()
    ws = wb.active
    ws.title = "Available Halls"

    # Header row: Slot names
    ws.append(["Day"] + time_slots)

    for day in days:
        row_data = [day]
        for slot in time_slots:
            available_rooms = []
            for room in room_slots:
                key = f"{day}-{slot}"
                if room_slots[room][key]:
                    available_rooms.append(room)
            row_data.append(", ".join(sorted(available_rooms)))
        ws.append(row_data)

    wb.save(output_path)


def schedule_bs_ms_first_year(
    course_df, room_df, room_slots, time_slots, days, output_dir,
    first_year_lectures_by_day_slot=None,
    timetable=None  # βœ… NEW ARG: pass the main timetable dict (defaultdict)
):
    from openpyxl import Workbook
    from collections import defaultdict
    import os

    TUTORIAL_ROOMS = ["LHC 105", "LHC 106", "LHC 107", "LHC 108"]
    LAB_SLOTS = ["2-3", "3-4", "4-5"]
    LECTURE_SLOTS = ["8-9", "9-10", "10-11", "11-12", "12-1"]
    LUNCH_SLOT = "1-2"
    FORBIDDEN_5_6_DAYS = {"Monday", "Wednesday", "Friday"}

    # Mapping TGs to LGs
    tg_to_lg = {
        "TG-1": "LG-1", "TG-2": "LG-1",
        "TG-3": "LG-2", "TG-4": "LG-2",
        "TG-5": "LG-3", "TG-6": "LG-3",
        "TG-7": "LG-4", "TG-8": "LG-4",
        "TG-9": "LG-5", "TG-10": "LG-5",
    }

    # Extract valid lab rooms
    lab_rooms = []
    for _, row in room_df.iterrows():
        for col in ["Computer Lab", "Class Room", "Seminar Halls"]:
            room = row.get(col)
            if pd.notna(room) and ("lab" in str(room).lower() or "computer" in str(room).lower()):
                lab_rooms.append(str(room).strip())

    lab_groups = {f"LG-{i+1}": [] for i in range(5)}
    tutorial_groups = {f"LG-{i+1}": [] for i in range(5)}
    tutorial_slot_count = defaultdict(lambda: defaultdict(int))  # day -> slot -> count
    used_slots_by_lg = defaultdict(set)
    unscheduled = []

    for _, row in course_df.iterrows():
        cname = row["Course Name"]
        subfolder = row["Sub folder"]
        ltpc = row["[LTPC]"]
        students = int(row["Total Students"])
        L, T, P, C = parse_ltpc(ltpc)

        codes = cname.split("-")
        years = [extract_year_from_code(c) for c in codes if extract_year_from_code(c) is not None]
        if not years or max(years) != 1:
            continue  # Not a 1st year course

        # --- Schedule Labs ---
        if P > 0:
            for lg_index in range(5):
                lg = f"LG-{lg_index+1}"
                assigned = False
                for day in days:
                    for room in lab_rooms:
                        if all(room_slots[room][f"{day}-{slot}"] for slot in LAB_SLOTS):
                            for slot in LAB_SLOTS:
                                room_slots[room][f"{day}-{slot}"] = False
                                used_slots_by_lg[lg].add(f"{day}-{slot}")
                                # βœ… NEW: Add to central timetable
                                if timetable:
                                    timetable[1][day][slot].append(f"{cname} (Lab) @ {room} ({lg})")
                            lab_groups[lg].append((day, LAB_SLOTS, room, cname))
                            assigned = True
                            break
                    if assigned:
                        break
                if not assigned:
                    unscheduled.append({
                        "Course Name": cname,
                        "LTPC": ltpc,
                        "Sub folder": subfolder,
                        "Reason": f"No available 2–5PM block for {lg}"
                    })

        # --- Schedule Tutorials ---
        if T == 1:
            for lg_index in range(5):
                lg = f"LG-{lg_index+1}"
                tg1 = f"TG-{2*lg_index+1}"
                tg2 = f"TG-{2*lg_index+2}"
                assigned = 0

                for day in days:
                    for slot in time_slots:
                        if slot == LUNCH_SLOT or slot in LAB_SLOTS or (slot == "5-6" and day in FORBIDDEN_5_6_DAYS):
                            continue
                        if slot in used_slots_by_lg[lg]:
                            continue
                        if tutorial_slot_count[day][slot] >= 3:
                            continue

                        available_rooms = [r for r in TUTORIAL_ROOMS if room_slots[r][f"{day}-{slot}"]]
                        if not available_rooms:
                            continue

                        room = available_rooms[0]
                        room_slots[room][f"{day}-{slot}"] = False
                        tutorial_slot_count[day][slot] += 1
                        tutorial_groups[lg].append((day, slot, room, cname))
                        used_slots_by_lg[lg].add(f"{day}-{slot}")
                        assigned += 1

                        # βœ… NEW: Add to central timetable
                        if timetable:
                            timetable[1][day][slot].append(f"{cname} (Tutorial) @ {room} ({lg})")

                        if assigned == 2:
                            break
                    if assigned == 2:
                        break

                if assigned < 2:
                    unscheduled.append({
                        "Course Name": cname,
                        "LTPC": ltpc,
                        "Sub folder": subfolder,
                        "Reason": f"Tutorials for {tg1}, {tg2} (under {lg}) not scheduled"
                    })

    # --- Write LG-wise Excel files with lectures ---
    year_folder = os.path.join(output_dir, "1st Year")
    os.makedirs(year_folder, exist_ok=True)

    for lg in lab_groups:
        wb = Workbook()
        ws = wb.active
        ws.title = lg
        ws.append(["Day"] + time_slots)

        for day in days:
            row = [day]
            for slot in time_slots:
                val = ""

                # Tutorials
                for d, s, r, c in tutorial_groups[lg]:
                    if d == day and s == slot:
                        val += f"{c} (Tutorial) @ {r}\n"

                # Labs
                for d, slot_block, r, c in lab_groups[lg]:
                    if d == day and slot in slot_block:
                        val += f"{c} (Lab) @ {r}\n"

                # Lectures
                if first_year_lectures_by_day_slot:
                    for entry in first_year_lectures_by_day_slot[day][slot]:
                        if f"{day}-{slot}" not in used_slots_by_lg[lg]:
                            val += f"{entry} (Lecture)\n"
                            used_slots_by_lg[lg].add(f"{day}-{slot}")

                row.append(val.strip())
            ws.append(row)

        # βœ… NEW: Rename file with LG-TG mapping
        lg_index = int(lg.split("-")[1])
        tg1 = f"TG-{2 * (lg_index - 1) + 1}"
        tg2 = f"TG-{2 * (lg_index - 1) + 2}"
        filename = f"{lg} ({tg1} & {tg2}).xlsx"

        wb.save(os.path.join(year_folder, filename))

    # --- Export Unscheduled ---
    if unscheduled:
        pd.DataFrame(unscheduled).to_excel(os.path.join(output_dir, "Unscheduled_LTP_Report.xlsx"), index=False)

    return tutorial_groups, lab_groups, unscheduled








def is_slot_allowed_for_year(year, day, slot):
    if slot == lunch_slot:
        return False
    if year == 1:
        # BS-MS 1st Year: Lectures only between 8–1, no 5–6 on M/W/F
        if slot not in ["8-9", "9-10", "10-11", "11-12", "12-1"]:
            return False
        if slot == "5-6" and day in ["Monday", "Wednesday", "Friday"]:
            return False
    if year in restricted_5_6_slots and slot == "5-6" and day in restricted_5_6_slots[year]:
        return False
    if year in [3, 4] and slot in no_class_3rd_4th.get(day, []):
        return False
    return True


def generate_timetable(course_df, room_df, output_dir, room_slots, timetable):
    from openpyxl import Workbook
    from openpyxl.styles import PatternFill
    from collections import defaultdict

    course_colors = {}
    course_color_palette = ["FFCCCC", "CCFFCC", "CCCCFF", "FFFFCC", "CCFFFF", "FFCCFF", "F0E68C", "E6E6FA"]
    color_index = 0
    first_year_lectures_by_day_slot = defaultdict(lambda: defaultdict(list))
    unscheduled_courses = []

    # βœ… Build usable room list (merged from all columns)
    room_data = []
    for i in range(len(room_df)):
        for hall_type, room_col, cap_col in [
            ("Class Room", "Class Room", "Class Capacity"),
            ("Computer Lab", "Computer Lab", "Computer Capacity"),
            ("Seminar Halls", "Seminar Halls", "Seminar Capacity")
        ]:
            room = room_df[room_col].iloc[i]
            cap = room_df[cap_col].iloc[i]
            if pd.notna(room) and pd.notna(cap):
                room_data.append((str(room).strip(), int(cap), hall_type))

    grouped = course_df.groupby("Sub folder")

    for subfolder, group in grouped:
        sorted_courses = group.sort_values(by="Total Students", ascending=False)

        for _, row in sorted_courses.iterrows():
            cname = row["Course Name"]
            course_group = cname.split("-")
            ltpc = row["[LTPC]"]
            students = int(row["Total Students"])
            years_in_group = [extract_year_from_code(part) for part in course_group]
            valid_years = list(filter(None, years_in_group))
            if not valid_years or max(valid_years) > 5:
                unscheduled_courses.append({"Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "Invalid or unsupported year in course code"})
                continue
            year = max(valid_years)
            L, T, P, C = parse_ltpc(ltpc)

            # Assign course color
            if cname not in course_colors:
                course_colors[cname] = course_color_palette[color_index % len(course_color_palette)]
                color_index += 1

            # --- Assign suitable room ---
            assigned_room = None
            if L > 0:
                # For lectures, prefer Class Rooms or Seminar Halls
                for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)):
                    if cap >= students and hall_type in ["Class Room", "Seminar Halls"]:
                        assigned_room = (room, hall_type)
                        break
            else:
                # For tutorials (without lectures)
                for room, cap, hall_type in sorted(room_data, key=lambda x: abs(x[1] - students)):
                    if cap >= students:
                        assigned_room = (room, hall_type)
                        break

            if not assigned_room:
                unscheduled_courses.append({"Course Name": cname, "LTPC": ltpc, "Sub folder": subfolder, "Reason": "No suitable room"})
                continue

            room_name, hall_type = assigned_room

            # --- Assign lecture slots ---
            lecture_day_options = find_valid_lecture_days(L, list(range(len(days))))
            best_assigned = []
            max_assigned = 0

            for day_indices in lecture_day_options:
                temp_assigned = []
                for idx in day_indices:
                    day = days[idx]
                    for slot in time_slots:
                        if not is_slot_allowed_for_year(year, day, slot):
                            continue
                        if hall_type == "Seminar Halls" and slot in seminar_hall_blocked_slots:
                            continue
                        if not room_slots[room_name][f"{day}-{slot}"]:
                            continue
                        if any(is_bcp_blocked(part, day, slot) for part in course_group):
                            continue

                        entry = f"{cname} @ {room_name}"
                        timetable[year][day][slot].append(entry)
                        room_slots[room_name][f"{day}-{slot}"] = False

                        if year == 1:
                            first_year_lectures_by_day_slot[day][slot].append(entry)

                        temp_assigned.append(idx)
                        break
                if len(temp_assigned) > max_assigned:
                    best_assigned = temp_assigned
                    max_assigned = len(temp_assigned)
                if max_assigned == L:
                    break

            if max_assigned < L:
                unscheduled_courses.append({
                    "Course Name": cname,
                    "LTPC": ltpc,
                    "Sub folder": subfolder,
                    "Reason": f"Only {max_assigned} of {L} lectures scheduled"
                })

            # --- Schedule Tutorial for non-1st Year ---
            if T == 1 and year != 1:
                tutorial_scheduled = False
                for i, day in enumerate(days):
                    if i in best_assigned:
                        continue
                    for slot in time_slots:
                        if not is_slot_allowed_for_year(year, day, slot):
                            continue
                        if not room_slots[room_name][f"{day}-{slot}"]:
                            continue
                        if any(is_bcp_blocked(part, day, slot) for part in course_group):
                            continue

                        timetable[year][day][slot].append(f"{cname} Tutorial @ {room_name}")
                        room_slots[room_name][f"{day}-{slot}"] = False
                        tutorial_scheduled = True
                        break
                    if tutorial_scheduled:
                        break
                if not tutorial_scheduled:
                    unscheduled_courses.append({
                        "Course Name": cname,
                        "LTPC": ltpc,
                        "Sub folder": subfolder,
                        "Reason": "Tutorial slot unavailable due to conflicts/constraints"
                    })

    # --- Write Timetables (Years 2–5) ---
    year_major_courses = defaultdict(lambda: defaultdict(list))
    for _, row in course_df.iterrows():
        cname = row["Course Name"]
        years = [extract_year_from_code(code) for code in cname.split("-")]
        valid_years = list(filter(None, years))
        if not valid_years or max(valid_years) > 5:
            continue
        year = max(valid_years)
        for part in cname.split("-"):
            code = part.lower()
            for prefix, major in {
                "mat": "Math", "msm": "Math", "phy": "Physics", "msp": "Physics", "chy": "Chemistry", "msc": "Chemistry",
                "bio": "Biology", "msb": "Biology", "ees": "Earth Science", "dsc": "Data Science", "i2m": "Math",
                "i2p": "Physics", "i2c": "Chemistry", "i2b": "Biology"
            }.items():
                if code.startswith(prefix):
                    year_major_courses[year][major].append(cname)

    for year in timetable:
        if year == 1:
            continue  # 1st-year handled separately

        year_folder = os.path.join(output_dir, f"{ordinal(year)} Year")
        os.makedirs(year_folder, exist_ok=True)

        for major in year_major_courses[year]:
            fname = f"{ordinal(year)} Year, {major} Major.xlsx"
            fpath = os.path.join(year_folder, fname)
            wb = Workbook()
            ws = wb.active
            ws.append(["Day"] + time_slots)

            for row_idx, day in enumerate(days, start=2):
                row_data = [day]
                for col_idx, slot in enumerate(time_slots, start=2):
                    entries = timetable[year][day][slot]
                    val = ", ".join(e for e in entries if any(c in e for c in year_major_courses[year][major]))
                    row_data.append(val)
                ws.append(row_data)

                for col_idx, slot in enumerate(time_slots, start=2):
                    cell = ws.cell(row=row_idx, column=col_idx)
                    entries = timetable[year][day][slot]
                    for cname in year_major_courses[year][major]:
                        if any(cname in e for e in entries):
                            cell.fill = PatternFill(start_color=course_colors[cname], end_color=course_colors[cname], fill_type="solid")
                            break
            wb.save(fpath)

    # βœ… Save Unscheduled Report
    if unscheduled_courses:
        pd.DataFrame(unscheduled_courses).to_excel("Unscheduled_LTP_Report.xlsx", index=False)

    return first_year_lectures_by_day_slot







# -- All other imported functions like: generate_available_halls_report, generate_central_timetable_excel, schedule_bs_ms_first_year, generate_timetable --
# You already posted them correctly. I won't repeat to save space unless you ask.

# --- Final Working Process Function ---
def process(course_file, room_file):
    import shutil
    import zipfile
    from collections import defaultdict
    import pandas as pd
    import os

    # --- Read Excel Files ---
    course_df = pd.read_excel(course_file.name)
    room_df = pd.read_excel(room_file.name)

    # βœ… Rename columns for clarity (based on your Excel screenshot)
    room_df.columns = [
        "Class Room", "Class Capacity",
        "Computer Lab", "Computer Capacity",
        "Seminar Halls", "Seminar Capacity"
    ]

    # --- Create output directories ---
    output_dir = "generated_routines"
    output_1st_year_dir = "generated_1st_year_LT"

    for path in [output_dir, output_1st_year_dir]:
        if os.path.exists(path):
            shutil.rmtree(path)
        os.makedirs(path)

    # --- Initialize room availability ---
    room_slots = defaultdict(lambda: defaultdict(lambda: True))

    # βœ… BLOCK 2–5 PM for BS-MS 1st Year labs
    for day in days:
        for room in room_slots:
            for slot in ["2-3", "3-4", "4-5"]:
                room_slots[room][f"{day}-{slot}"] = False

    # βœ… Build room_data (now includes Seminar Halls correctly)
    room_data = []
    for i in range(len(room_df)):
        # Class Room
        if pd.notna(room_df["Class Room"].iloc[i]) and pd.notna(room_df["Class Capacity"].iloc[i]):
            room_data.append((
                str(room_df["Class Room"].iloc[i]).strip(),
                int(room_df["Class Capacity"].iloc[i]),
                "Class Room"
            ))

        # Computer Lab
        if pd.notna(room_df["Computer Lab"].iloc[i]) and pd.notna(room_df["Computer Capacity"].iloc[i]):
            room_data.append((
                str(room_df["Computer Lab"].iloc[i]).strip(),
                int(room_df["Computer Capacity"].iloc[i]),
                "Computer Lab"
            ))

        # Seminar Hall
        if pd.notna(room_df["Seminar Halls"].iloc[i]) and pd.notna(room_df["Seminar Capacity"].iloc[i]):
            room_data.append((
                str(room_df["Seminar Halls"].iloc[i]).strip(),
                int(room_df["Seminar Capacity"].iloc[i]),
                "Seminar Halls"
            ))

    # βœ… Initialize central timetable
    timetable = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

    # Step 1: Generate lectures (Years 2–5) + collect 1st year lectures
    first_year_lectures = generate_timetable(course_df, room_df, output_dir, room_slots, timetable)

    # Step 2: Schedule 1st year labs + tutorials
    tg, lg, unscheduled_1st = schedule_bs_ms_first_year(
        course_df,
        room_df,
        room_slots,
        time_slots,
        days,
        output_1st_year_dir,
        first_year_lectures_by_day_slot=first_year_lectures,
        timetable=timetable
    )

    # Step 3: Export reports
    generate_available_halls_report(room_slots)
    generate_central_timetable_excel(timetable)

    # Step 4: Zip Final_Timetable (2nd–5th Year)
    zip_path_main = "Final_Timetable.zip"
    with zipfile.ZipFile(zip_path_main, 'w') as zipf:
        for foldername, _, filenames in os.walk(output_dir):
            for filename in filenames:
                filepath = os.path.join(foldername, filename)
                arcname = os.path.relpath(filepath, output_dir)
                zipf.write(filepath, arcname)

    # Step 5: Zip 1st_Year_Labs_Tutorials.zip (includes lectures + labs + tutorials)
    zip_path_1st = "1st_Year_Labs_Tutorials.zip"
    with zipfile.ZipFile(zip_path_1st, 'w') as zipf:
        for foldername, _, filenames in os.walk(output_1st_year_dir):
            for filename in filenames:
                filepath = os.path.join(foldername, filename)
                arcname = os.path.relpath(filepath, output_1st_year_dir)
                zipf.write(filepath, arcname)

    # Step 6: Return final file outputs
    return (
        zip_path_main,
        zip_path_1st,
        ("Unscheduled_LTP_Report.xlsx" if os.path.exists("Unscheduled_LTP_Report.xlsx") else None),
        ("Available_Halls.xlsx" if os.path.exists("Available_Halls.xlsx") else None),
        ("Central_Timetable.xlsx" if os.path.exists("Central_Timetable.xlsx") else None)
    )






# --- Gradio UI Launcher ---
import gradio as gr

def launch_ui():
    with gr.Blocks(css="""
        .centered-title {
            text-align: center;
            font-size: 2.2em;
            font-weight: bold;
            color: #2b3d4f;
            background: #e0f7fa;
            padding: 10px;
            border-radius: 12px;
            margin-bottom: 20px;
        }
        .highlight-button {
            background-color: #1976D2 !important;
            color: white !important;
            border-radius: 8px;
            padding: 10px 20px;
        }
    """) as iface:

        # πŸ”Ό Banner Image (Header)
        gr.Image(
            value="https://drive.google.com/uc?id=1UDlI15QVKy0JSkfFCG7yMAR7esv35O-v",
            height=400,
            width=8000,  # βœ… Reasonable width
            show_label=False,
            container=False
        )

        # 🏷️ Styled Title
        gr.HTML('<div class="centered-title">IISER TVM Course Timetable Scheduler</div>')

        # πŸ“ File Inputs
        with gr.Row():
            course_input = gr.File(label="πŸ“˜ Upload Course Excel File (.xlsx)")
            room_input = gr.File(label="🏬 Upload Room Details Excel File (.xlsx)")

        # πŸš€ Styled Button
        run_button = gr.Button("Generate Timetable", elem_classes="highlight-button")

        # 🧾 Outputs (Timetable files)
        with gr.Row():
            timetable_output = gr.File(label="πŸ“ Download Timetable ZIP")
            first_year_output = gr.File(label="πŸ§ͺ 1st Year Labs & Tutorials ZIP")

        with gr.Row():
            unscheduled_output = gr.File(label="⚠️ Unscheduled L/T/P Report (if any)")
            available_halls_output = gr.File(label="🏩 Available Halls Report")
            central_output = gr.File(label="πŸ“˜ Central Timetable (All Courses)")

        # 🧠 Process Trigger
        run_button.click(
            fn=process,
            inputs=[course_input, room_input],
            outputs=[
                timetable_output,
                first_year_output,
                unscheduled_output,
                available_halls_output,
                central_output
            ]
        )

    iface.launch(share=True, debug=True)


if __name__ == "__main__":
    launch_ui()