@brite-future-schools-contracts/contracts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,941 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { z } from "zod";
3
+ import {
4
+ loginInputSchema,
5
+ loginOutputSchema,
6
+ mfaVerifyInputSchema,
7
+ changePasswordInputSchema,
8
+ refreshTokenOutputSchema,
9
+ refreshInputSchema,
10
+ branchSchema,
11
+ createBranchInputSchema,
12
+ studentSchema,
13
+ createStudentInputSchema,
14
+ listStudentsInputSchema,
15
+ guardianSchema,
16
+ sponsorSchema,
17
+ sponsorTypeEnum,
18
+ subjectSchema,
19
+ assessmentScoreInputSchema,
20
+ reportCardStatusEnum,
21
+ gradeLevelEnum,
22
+ assessmentTypeEnum,
23
+ feeItemSchema,
24
+ recordPaymentInputSchema,
25
+ invoiceStatusEnum,
26
+ staffSchema,
27
+ leaveRequestInputSchema,
28
+ leaveStatusEnum,
29
+ tripLogInputSchema,
30
+ busRegisterEntrySchema,
31
+ busRegisterStatusEnum,
32
+ assetCategoryEnum,
33
+ assetConditionEnum,
34
+ maintenanceTicketStatusEnum,
35
+ borrowingStatusEnum,
36
+ incidentSeverityEnum,
37
+ incidentStatusEnum,
38
+ disciplineRulingEnum,
39
+ payrollRunStatusEnum,
40
+ sendBulkSmsInputSchema,
41
+ paginationSchema,
42
+ uuidSchema,
43
+ userSchema,
44
+ } from "../schemas";
45
+
46
+ // ─── Auth Router ──────────────────────────────────────────────────────────────
47
+
48
+ export const authContract = {
49
+ login: oc.input(loginInputSchema).output(loginOutputSchema),
50
+
51
+ refresh: oc.input(refreshInputSchema).output(refreshTokenOutputSchema),
52
+
53
+ logout: oc.output(z.object({ success: z.boolean() })),
54
+
55
+ me: oc.output(
56
+ userSchema.extend({
57
+ roles: z.array(
58
+ z.object({
59
+ id: uuidSchema,
60
+ name: z.string(),
61
+ branchId: uuidSchema.nullable(),
62
+ }),
63
+ ),
64
+ }),
65
+ ),
66
+
67
+ verifyMfa: oc
68
+ .input(mfaVerifyInputSchema)
69
+ .output(z.object({ verified: z.boolean() })),
70
+
71
+ changePassword: oc
72
+ .input(changePasswordInputSchema)
73
+ .output(z.object({ success: z.boolean() })),
74
+
75
+ switchBranch: oc
76
+ .input(z.object({ branchId: uuidSchema }))
77
+ .output(z.object({ accessToken: z.string() })),
78
+ };
79
+
80
+ // ─── Branches Router ──────────────────────────────────────────────────────────
81
+
82
+ export const branchesContract = {
83
+ list: oc.output(z.array(branchSchema)),
84
+
85
+ get: oc.input(z.object({ id: uuidSchema })).output(branchSchema),
86
+
87
+ create: oc.input(createBranchInputSchema).output(branchSchema),
88
+
89
+ update: oc
90
+ .input(createBranchInputSchema.partial().extend({ id: uuidSchema }))
91
+ .output(branchSchema),
92
+
93
+ updateFeatureFlags: oc
94
+ .input(z.object({ branchId: uuidSchema, flags: z.record(z.boolean()) }))
95
+ .output(branchSchema),
96
+ };
97
+
98
+ // ─── Students Router ──────────────────────────────────────────────────────────
99
+
100
+ const studentWithGuardians = studentSchema.extend({
101
+ guardians: z.array(guardianSchema),
102
+ sponsors: z.array(sponsorSchema),
103
+ currentClass: z
104
+ .object({ id: uuidSchema, name: z.string(), grade: z.string() })
105
+ .nullable(),
106
+ });
107
+
108
+ export const studentsContract = {
109
+ list: oc.input(listStudentsInputSchema).output(
110
+ z.object({
111
+ data: z.array(studentSchema),
112
+ total: z.number(),
113
+ page: z.number(),
114
+ limit: z.number(),
115
+ }),
116
+ ),
117
+
118
+ get: oc.input(z.object({ id: uuidSchema })).output(studentWithGuardians),
119
+
120
+ create: oc.input(createStudentInputSchema).output(studentSchema),
121
+
122
+ update: oc
123
+ .input(createStudentInputSchema.partial().extend({ id: uuidSchema }))
124
+ .output(studentSchema),
125
+
126
+ enroll: oc
127
+ .input(
128
+ z.object({
129
+ studentId: uuidSchema,
130
+ classId: uuidSchema,
131
+ termId: uuidSchema,
132
+ }),
133
+ )
134
+ .output(z.object({ success: z.boolean() })),
135
+
136
+ transfer: oc
137
+ .input(
138
+ z.object({
139
+ studentId: uuidSchema,
140
+ toBranchId: uuidSchema,
141
+ reason: z.string(),
142
+ }),
143
+ )
144
+ .output(z.object({ success: z.boolean() })),
145
+
146
+ linkGuardian: oc
147
+ .input(
148
+ z.object({
149
+ studentId: uuidSchema,
150
+ guardianId: uuidSchema,
151
+ isPrimary: z.boolean(),
152
+ }),
153
+ )
154
+ .output(z.object({ success: z.boolean() })),
155
+
156
+ linkSponsor: oc
157
+ .input(
158
+ z.object({
159
+ studentId: uuidSchema,
160
+ sponsorId: uuidSchema,
161
+ termId: uuidSchema,
162
+ feeItemIds: z.array(uuidSchema),
163
+ }),
164
+ )
165
+ .output(z.object({ success: z.boolean() })),
166
+ };
167
+
168
+ // ─── Academics Router ─────────────────────────────────────────────────────────
169
+
170
+ export const academicsContract = {
171
+ subjects: {
172
+ list: oc
173
+ .input(
174
+ z.object({
175
+ branchId: uuidSchema,
176
+ gradeLevel: gradeLevelEnum.optional(),
177
+ }),
178
+ )
179
+ .output(z.array(subjectSchema)),
180
+ create: oc
181
+ .input(
182
+ z.object({
183
+ name: z.string(),
184
+ code: z.string(),
185
+ gradeLevel: gradeLevelEnum,
186
+ departmentId: uuidSchema.optional(),
187
+ }),
188
+ )
189
+ .output(subjectSchema),
190
+ },
191
+
192
+ timetable: {
193
+ get: oc.input(z.object({ classId: uuidSchema, termId: uuidSchema })).output(
194
+ z.array(
195
+ z.object({
196
+ id: uuidSchema,
197
+ dayOfWeek: z.number().min(1).max(5),
198
+ periodNo: z.number(),
199
+ subject: subjectSchema,
200
+ teacherName: z.string(),
201
+ room: z.string().nullable(),
202
+ startTime: z.string(),
203
+ endTime: z.string(),
204
+ }),
205
+ ),
206
+ ),
207
+ upsert: oc
208
+ .input(
209
+ z.object({
210
+ classId: uuidSchema,
211
+ termId: uuidSchema,
212
+ slots: z.array(
213
+ z.object({
214
+ dayOfWeek: z.number().min(1).max(5),
215
+ periodNo: z.number(),
216
+ subjectId: uuidSchema,
217
+ teacherId: uuidSchema,
218
+ room: z.string().optional(),
219
+ startTime: z.string(),
220
+ endTime: z.string(),
221
+ }),
222
+ ),
223
+ }),
224
+ )
225
+ .output(z.object({ success: z.boolean() })),
226
+ },
227
+
228
+ assessments: {
229
+ list: oc
230
+ .input(
231
+ z.object({
232
+ classId: uuidSchema,
233
+ termId: uuidSchema,
234
+ type: assessmentTypeEnum.optional(),
235
+ }),
236
+ )
237
+ .output(
238
+ z.array(
239
+ z.object({
240
+ id: uuidSchema,
241
+ title: z.string(),
242
+ type: assessmentTypeEnum,
243
+ date: z.string(),
244
+ maxScore: z.number(),
245
+ subjectId: uuidSchema,
246
+ }),
247
+ ),
248
+ ),
249
+ create: oc
250
+ .input(
251
+ z.object({
252
+ title: z.string(),
253
+ type: assessmentTypeEnum,
254
+ classId: uuidSchema,
255
+ subjectId: uuidSchema,
256
+ termId: uuidSchema,
257
+ date: z.string(),
258
+ maxScore: z.number(),
259
+ }),
260
+ )
261
+ .output(z.object({ id: uuidSchema })),
262
+ enterScores: oc
263
+ .input(z.object({ scores: z.array(assessmentScoreInputSchema) }))
264
+ .output(z.object({ saved: z.number() })),
265
+ },
266
+
267
+ reportCards: {
268
+ list: oc
269
+ .input(
270
+ z.object({
271
+ branchId: uuidSchema,
272
+ termId: uuidSchema,
273
+ classId: uuidSchema.optional(),
274
+ status: reportCardStatusEnum.optional(),
275
+ }),
276
+ )
277
+ .output(
278
+ z.array(
279
+ z.object({
280
+ id: uuidSchema,
281
+ studentId: uuidSchema,
282
+ studentName: z.string(),
283
+ status: reportCardStatusEnum,
284
+ generatedAt: z.string().datetime().nullable(),
285
+ }),
286
+ ),
287
+ ),
288
+ get: oc
289
+ .input(z.object({ studentId: uuidSchema, termId: uuidSchema }))
290
+ .output(
291
+ z.object({
292
+ id: uuidSchema,
293
+ studentId: uuidSchema,
294
+ termId: uuidSchema,
295
+ status: reportCardStatusEnum,
296
+ scores: z.array(z.any()),
297
+ teacherComments: z.string().nullable(),
298
+ generatedAt: z.string().datetime().nullable(),
299
+ }),
300
+ ),
301
+ updateStatus: oc
302
+ .input(
303
+ z.object({
304
+ id: uuidSchema,
305
+ status: reportCardStatusEnum,
306
+ comments: z.string().optional(),
307
+ }),
308
+ )
309
+ .output(z.object({ success: z.boolean() })),
310
+ },
311
+
312
+ attendance: {
313
+ markClass: oc
314
+ .input(
315
+ z.object({
316
+ classId: uuidSchema,
317
+ date: z.string(),
318
+ subjectId: uuidSchema,
319
+ records: z.array(
320
+ z.object({
321
+ studentId: uuidSchema,
322
+ present: z.boolean(),
323
+ note: z.string().optional(),
324
+ }),
325
+ ),
326
+ }),
327
+ )
328
+ .output(z.object({ saved: z.number() })),
329
+ getClassAttendance: oc
330
+ .input(
331
+ z.object({ classId: uuidSchema, from: z.string(), to: z.string() }),
332
+ )
333
+ .output(
334
+ z.array(
335
+ z.object({
336
+ studentId: uuidSchema,
337
+ studentName: z.string(),
338
+ presentDays: z.number(),
339
+ absentDays: z.number(),
340
+ percentage: z.number(),
341
+ }),
342
+ ),
343
+ ),
344
+ },
345
+ };
346
+
347
+ // ─── Finance Router ───────────────────────────────────────────────────────────
348
+
349
+ export const financeContract = {
350
+ feeItems: {
351
+ list: oc
352
+ .input(z.object({ branchId: uuidSchema }))
353
+ .output(z.array(feeItemSchema)),
354
+ create: oc
355
+ .input(
356
+ z.object({
357
+ name: z.string(),
358
+ category: z.any(),
359
+ defaultAmount: z.number(),
360
+ applicableGrades: z.array(gradeLevelEnum),
361
+ }),
362
+ )
363
+ .output(feeItemSchema),
364
+ },
365
+
366
+ invoices: {
367
+ listForStudent: oc
368
+ .input(z.object({ studentId: uuidSchema, termId: uuidSchema.optional() }))
369
+ .output(
370
+ z.array(
371
+ z.object({
372
+ id: uuidSchema,
373
+ termId: uuidSchema,
374
+ payerType: z.string(),
375
+ totalAmount: z.number(),
376
+ paidAmount: z.number(),
377
+ balance: z.number(),
378
+ status: invoiceStatusEnum,
379
+ dueDate: z.string().nullable(),
380
+ }),
381
+ ),
382
+ ),
383
+ generate: oc
384
+ .input(
385
+ z.object({
386
+ branchId: uuidSchema,
387
+ termId: uuidSchema,
388
+ studentIds: z.array(uuidSchema).optional(),
389
+ }),
390
+ )
391
+ .output(z.object({ generated: z.number() })),
392
+ },
393
+
394
+ payments: {
395
+ record: oc
396
+ .input(recordPaymentInputSchema)
397
+ .output(z.object({ id: uuidSchema, receipt: z.string() })),
398
+ listForStudent: oc.input(z.object({ studentId: uuidSchema })).output(
399
+ z.array(
400
+ z.object({
401
+ id: uuidSchema,
402
+ amount: z.number(),
403
+ method: z.string(),
404
+ reference: z.string().nullable(),
405
+ paymentDate: z.string(),
406
+ createdAt: z.string(),
407
+ }),
408
+ ),
409
+ ),
410
+ },
411
+
412
+ blocks: {
413
+ list: oc
414
+ .input(
415
+ z.object({
416
+ branchId: uuidSchema,
417
+ termId: uuidSchema,
418
+ active: z.boolean().optional(),
419
+ }),
420
+ )
421
+ .output(
422
+ z.array(
423
+ z.object({
424
+ id: uuidSchema,
425
+ studentId: uuidSchema,
426
+ studentName: z.string(),
427
+ blockType: z.string(),
428
+ isActive: z.boolean(),
429
+ balanceAtBlock: z.number(),
430
+ }),
431
+ ),
432
+ ),
433
+ lift: oc
434
+ .input(z.object({ blockId: uuidSchema, reason: z.string() }))
435
+ .output(z.object({ success: z.boolean() })),
436
+ },
437
+
438
+ reports: {
439
+ termCollection: oc
440
+ .input(z.object({ branchId: uuidSchema, termId: uuidSchema }))
441
+ .output(
442
+ z.object({
443
+ totalExpected: z.number(),
444
+ totalCollected: z.number(),
445
+ totalOutstanding: z.number(),
446
+ collectionRate: z.number(),
447
+ byPaymentMethod: z.record(z.number()),
448
+ }),
449
+ ),
450
+ },
451
+ };
452
+
453
+ // ─── HR Router ────────────────────────────────────────────────────────────────
454
+
455
+ export const hrContract = {
456
+ staff: {
457
+ list: oc
458
+ .input(
459
+ paginationSchema.extend({
460
+ branchId: uuidSchema,
461
+ search: z.string().optional(),
462
+ departmentId: uuidSchema.optional(),
463
+ }),
464
+ )
465
+ .output(z.object({ data: z.array(staffSchema), total: z.number() })),
466
+ get: oc.input(z.object({ id: uuidSchema })).output(staffSchema),
467
+ create: oc
468
+ .input(
469
+ staffSchema
470
+ .omit({ id: true })
471
+ .partial({
472
+ contractEnd: true,
473
+ tscNumber: true,
474
+ kraPin: true,
475
+ nhifNo: true,
476
+ nssfNo: true,
477
+ bankName: true,
478
+ bankAccount: true,
479
+ departmentId: true,
480
+ }),
481
+ )
482
+ .output(staffSchema),
483
+ },
484
+
485
+ leave: {
486
+ request: oc
487
+ .input(leaveRequestInputSchema)
488
+ .output(z.object({ id: uuidSchema })),
489
+ list: oc
490
+ .input(
491
+ z.object({
492
+ branchId: uuidSchema,
493
+ staffId: uuidSchema.optional(),
494
+ status: leaveStatusEnum.optional(),
495
+ }),
496
+ )
497
+ .output(z.array(z.any())),
498
+ approve: oc
499
+ .input(
500
+ z.object({
501
+ id: uuidSchema,
502
+ approved: z.boolean(),
503
+ comment: z.string().optional(),
504
+ }),
505
+ )
506
+ .output(z.object({ success: z.boolean() })),
507
+ },
508
+
509
+ attendance: {
510
+ mark: oc
511
+ .input(
512
+ z.object({
513
+ staffId: uuidSchema,
514
+ date: z.string(),
515
+ checkIn: z.string().optional(),
516
+ checkOut: z.string().optional(),
517
+ status: z.enum(["present", "absent", "late", "half_day"]),
518
+ }),
519
+ )
520
+ .output(z.object({ success: z.boolean() })),
521
+ getSummary: oc
522
+ .input(
523
+ z.object({ staffId: uuidSchema, month: z.number(), year: z.number() }),
524
+ )
525
+ .output(
526
+ z.object({
527
+ presentDays: z.number(),
528
+ absentDays: z.number(),
529
+ lateDays: z.number(),
530
+ }),
531
+ ),
532
+ },
533
+ };
534
+
535
+ // ─── Payroll Router ───────────────────────────────────────────────────────────
536
+
537
+ export const payrollContract = {
538
+ runs: {
539
+ list: oc
540
+ .input(z.object({ branchId: uuidSchema, year: z.number().optional() }))
541
+ .output(
542
+ z.array(
543
+ z.object({
544
+ id: uuidSchema,
545
+ month: z.number(),
546
+ year: z.number(),
547
+ status: payrollRunStatusEnum,
548
+ netTotal: z.number(),
549
+ }),
550
+ ),
551
+ ),
552
+ initiate: oc
553
+ .input(
554
+ z.object({ branchId: uuidSchema, month: z.number(), year: z.number() }),
555
+ )
556
+ .output(z.object({ id: uuidSchema, preview: z.array(z.any()) })),
557
+ approve: oc
558
+ .input(z.object({ id: uuidSchema }))
559
+ .output(z.object({ success: z.boolean() })),
560
+ getPayslip: oc
561
+ .input(z.object({ payrollRunId: uuidSchema, staffId: uuidSchema }))
562
+ .output(z.object({ url: z.string() })),
563
+ },
564
+ };
565
+
566
+ // ─── Transport Router ─────────────────────────────────────────────────────────
567
+
568
+ export const transportContract = {
569
+ vehicles: {
570
+ list: oc
571
+ .input(z.object({ branchId: uuidSchema }))
572
+ .output(
573
+ z.array(
574
+ z.object({
575
+ id: uuidSchema,
576
+ plateNo: z.string(),
577
+ type: z.string(),
578
+ status: z.string(),
579
+ currentTrip: z.any().nullable(),
580
+ }),
581
+ ),
582
+ ),
583
+ },
584
+
585
+ trips: {
586
+ start: oc.input(tripLogInputSchema).output(z.object({ id: uuidSchema })),
587
+ recordBoardings: oc
588
+ .input(
589
+ z.object({
590
+ tripId: uuidSchema,
591
+ entries: z.array(busRegisterEntrySchema),
592
+ }),
593
+ )
594
+ .output(z.object({ saved: z.number() })),
595
+ complete: oc
596
+ .input(
597
+ z.object({ tripId: uuidSchema, arrivalTime: z.string().datetime() }),
598
+ )
599
+ .output(z.object({ success: z.boolean() })),
600
+ reportIncident: oc
601
+ .input(
602
+ z.object({
603
+ tripId: uuidSchema,
604
+ description: z.string(),
605
+ severity: incidentSeverityEnum,
606
+ location: z.string().optional(),
607
+ }),
608
+ )
609
+ .output(z.object({ id: uuidSchema })),
610
+ },
611
+
612
+ maintenance: {
613
+ log: oc
614
+ .input(
615
+ z.object({
616
+ vehicleId: uuidSchema,
617
+ issue: z.string(),
618
+ severity: z.enum(["routine", "urgent", "critical"]),
619
+ }),
620
+ )
621
+ .output(z.object({ id: uuidSchema })),
622
+ list: oc
623
+ .input(z.object({ vehicleId: uuidSchema }))
624
+ .output(z.array(z.any())),
625
+ },
626
+ };
627
+
628
+ // ─── Assets Router ────────────────────────────────────────────────────────────
629
+
630
+ export const assetsContract = {
631
+ list: oc
632
+ .input(
633
+ paginationSchema.extend({
634
+ branchId: uuidSchema,
635
+ category: assetCategoryEnum.optional(),
636
+ search: z.string().optional(),
637
+ }),
638
+ )
639
+ .output(z.object({ data: z.array(z.any()), total: z.number() })),
640
+ create: oc
641
+ .input(
642
+ z.object({
643
+ name: z.string(),
644
+ category: assetCategoryEnum,
645
+ serialNo: z.string().optional(),
646
+ purchaseDate: z.string(),
647
+ purchaseCost: z.number(),
648
+ supplier: z.string().optional(),
649
+ warrantyExpiry: z.string().optional(),
650
+ condition: assetConditionEnum,
651
+ depreciationRate: z.number().min(0).max(100),
652
+ }),
653
+ )
654
+ .output(z.object({ id: uuidSchema })),
655
+ assign: oc
656
+ .input(
657
+ z.object({
658
+ assetId: uuidSchema,
659
+ staffId: uuidSchema.optional(),
660
+ room: z.string().optional(),
661
+ }),
662
+ )
663
+ .output(z.object({ success: z.boolean() })),
664
+ ticket: {
665
+ create: oc
666
+ .input(
667
+ z.object({
668
+ assetId: uuidSchema,
669
+ description: z.string(),
670
+ severity: z.string(),
671
+ }),
672
+ )
673
+ .output(z.object({ id: uuidSchema })),
674
+ update: oc
675
+ .input(
676
+ z.object({
677
+ ticketId: uuidSchema,
678
+ status: maintenanceTicketStatusEnum,
679
+ notes: z.string().optional(),
680
+ cost: z.number().optional(),
681
+ }),
682
+ )
683
+ .output(z.object({ success: z.boolean() })),
684
+ },
685
+ };
686
+
687
+ // ─── Library Router ───────────────────────────────────────────────────────────
688
+
689
+ export const libraryContract = {
690
+ books: {
691
+ list: oc
692
+ .input(
693
+ paginationSchema.extend({
694
+ branchId: uuidSchema,
695
+ search: z.string().optional(),
696
+ available: z.boolean().optional(),
697
+ }),
698
+ )
699
+ .output(z.object({ data: z.array(z.any()), total: z.number() })),
700
+ create: oc
701
+ .input(
702
+ z.object({
703
+ isbn: z.string(),
704
+ title: z.string(),
705
+ author: z.string(),
706
+ publisher: z.string().optional(),
707
+ year: z.number().optional(),
708
+ subjectArea: z.string().optional(),
709
+ copiesTotal: z.number().min(1),
710
+ shelfLocation: z.string().optional(),
711
+ }),
712
+ )
713
+ .output(z.object({ id: uuidSchema })),
714
+ },
715
+ borrowings: {
716
+ checkout: oc
717
+ .input(
718
+ z.object({
719
+ bookId: uuidSchema,
720
+ borrowerType: z.enum(["student", "staff"]),
721
+ borrowerId: uuidSchema,
722
+ dueDate: z.string(),
723
+ }),
724
+ )
725
+ .output(z.object({ id: uuidSchema })),
726
+ return: oc
727
+ .input(
728
+ z.object({ borrowingId: uuidSchema, condition: z.string().optional() }),
729
+ )
730
+ .output(z.object({ fine: z.number() })),
731
+ listOverdue: oc
732
+ .input(z.object({ branchId: uuidSchema }))
733
+ .output(z.array(z.any())),
734
+ },
735
+ };
736
+
737
+ // ─── Discipline Router ────────────────────────────────────────────────────────
738
+
739
+ export const disciplineContract = {
740
+ incidents: {
741
+ log: oc
742
+ .input(
743
+ z.object({
744
+ studentId: uuidSchema,
745
+ incidentType: z.string(),
746
+ description: z.string(),
747
+ severity: incidentSeverityEnum,
748
+ incidentDate: z.string(),
749
+ requiresCounsellor: z.boolean().default(false),
750
+ }),
751
+ )
752
+ .output(z.object({ id: uuidSchema })),
753
+ list: oc
754
+ .input(
755
+ paginationSchema.extend({
756
+ branchId: uuidSchema,
757
+ studentId: uuidSchema.optional(),
758
+ status: incidentStatusEnum.optional(),
759
+ }),
760
+ )
761
+ .output(z.object({ data: z.array(z.any()), total: z.number() })),
762
+ rule: oc
763
+ .input(
764
+ z.object({
765
+ incidentId: uuidSchema,
766
+ ruling: disciplineRulingEnum,
767
+ suspensionDays: z.number().optional(),
768
+ notes: z.string(),
769
+ }),
770
+ )
771
+ .output(z.object({ success: z.boolean() })),
772
+ },
773
+ };
774
+
775
+ // ─── Communications Router ────────────────────────────────────────────────────
776
+
777
+ export const commsContract = {
778
+ notifications: {
779
+ list: oc
780
+ .input(paginationSchema.extend({ unreadOnly: z.boolean().optional() }))
781
+ .output(z.object({ data: z.array(z.any()), unreadCount: z.number() })),
782
+ markRead: oc
783
+ .input(z.object({ ids: z.array(uuidSchema) }))
784
+ .output(z.object({ updated: z.number() })),
785
+ },
786
+ messages: {
787
+ list: oc.input(paginationSchema).output(z.array(z.any())),
788
+ send: oc
789
+ .input(
790
+ z.object({
791
+ recipientId: uuidSchema,
792
+ subject: z.string(),
793
+ body: z.string(),
794
+ parentId: uuidSchema.optional(),
795
+ }),
796
+ )
797
+ .output(z.object({ id: uuidSchema })),
798
+ },
799
+ sms: {
800
+ send: oc
801
+ .input(sendBulkSmsInputSchema)
802
+ .output(z.object({ queued: z.number() })),
803
+ },
804
+ notices: {
805
+ list: oc
806
+ .input(z.object({ branchId: uuidSchema, active: z.boolean().optional() }))
807
+ .output(z.array(z.any())),
808
+ create: oc
809
+ .input(
810
+ z.object({
811
+ title: z.string(),
812
+ body: z.string(),
813
+ audienceType: z.string(),
814
+ audienceIds: z.array(uuidSchema).optional(),
815
+ publishAt: z.string().optional(),
816
+ expiresAt: z.string().optional(),
817
+ }),
818
+ )
819
+ .output(z.object({ id: uuidSchema })),
820
+ },
821
+ };
822
+
823
+ // ─── Admin Router ─────────────────────────────────────────────────────────────
824
+
825
+ export const adminContract = {
826
+ auditLogs: {
827
+ list: oc
828
+ .input(
829
+ paginationSchema.extend({
830
+ branchId: uuidSchema.optional(),
831
+ userId: uuidSchema.optional(),
832
+ action: z.string().optional(),
833
+ from: z.string().optional(),
834
+ to: z.string().optional(),
835
+ }),
836
+ )
837
+ .output(z.object({ data: z.array(z.any()), total: z.number() })),
838
+ },
839
+ users: {
840
+ create: oc
841
+ .input(
842
+ z.object({
843
+ firstName: z.string(),
844
+ middleName: z.string().optional(),
845
+ lastName: z.string(),
846
+ email: z.string().email(),
847
+ phone: z.string().optional(),
848
+ roleId: uuidSchema,
849
+ branchId: uuidSchema,
850
+ temporaryPassword: z.string().min(8),
851
+ }),
852
+ )
853
+ .output(z.object({ id: uuidSchema })),
854
+ suspend: oc
855
+ .input(z.object({ userId: uuidSchema, reason: z.string() }))
856
+ .output(z.object({ success: z.boolean() })),
857
+ },
858
+ roles: {
859
+ list: oc
860
+ .input(z.object({ branchId: uuidSchema }))
861
+ .output(
862
+ z.array(
863
+ z.object({
864
+ id: uuidSchema,
865
+ name: z.string(),
866
+ isSystem: z.boolean(),
867
+ permissionCount: z.number(),
868
+ }),
869
+ ),
870
+ ),
871
+ create: oc
872
+ .input(
873
+ z.object({
874
+ name: z.string(),
875
+ description: z.string().optional(),
876
+ permissionIds: z.array(uuidSchema),
877
+ }),
878
+ )
879
+ .output(z.object({ id: uuidSchema })),
880
+ },
881
+ };
882
+
883
+ // ─── Storage Router ───────────────────────────────────────────────────────────
884
+
885
+ export const storageContract = {
886
+ presignUpload: oc
887
+ .input(
888
+ z.object({
889
+ fileName: z.string(),
890
+ mimeType: z.string(),
891
+ category: z.enum([
892
+ "student_photo",
893
+ "staff_photo",
894
+ "document",
895
+ "payslip",
896
+ "report_card",
897
+ "asset_photo",
898
+ "notice_attachment",
899
+ ]),
900
+ resourceId: uuidSchema.optional(),
901
+ }),
902
+ )
903
+ .output(
904
+ z.object({
905
+ uploadUrl: z.string(),
906
+ fileKey: z.string(),
907
+ expiresIn: z.number(),
908
+ }),
909
+ ),
910
+
911
+ confirmUpload: oc
912
+ .input(
913
+ z.object({
914
+ fileKey: z.string(),
915
+ resourceId: uuidSchema,
916
+ resourceType: z.string(),
917
+ }),
918
+ )
919
+ .output(z.object({ publicUrl: z.string() })),
920
+ };
921
+
922
+ // ─── Root Contract (flat router type for client inference) ───────────────────
923
+
924
+ export const appContract = {
925
+ auth: authContract,
926
+ branches: branchesContract,
927
+ students: studentsContract,
928
+ academics: academicsContract,
929
+ finance: financeContract,
930
+ hr: hrContract,
931
+ payroll: payrollContract,
932
+ transport: transportContract,
933
+ assets: assetsContract,
934
+ library: libraryContract,
935
+ discipline: disciplineContract,
936
+ comms: commsContract,
937
+ admin: adminContract,
938
+ storage: storageContract,
939
+ };
940
+
941
+ export type AppContract = typeof appContract;