@ubkinfotech/tecaher-erp 0.1.0 → 0.1.2

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.
Files changed (28) hide show
  1. package/README.md +143 -67
  2. package/lib/commonjs/core/api/endpoints.js +1 -1
  3. package/lib/commonjs/core/auth/authContext.js +1 -2
  4. package/lib/commonjs/core/provider/ERPProvider.js +1 -2
  5. package/lib/commonjs/modules/assignment/screens/AssignmentScreen.js +295 -80
  6. package/lib/commonjs/modules/assignment/services/assignmentService.js +2 -2
  7. package/lib/commonjs/modules/attendance/screens/AttendanceScreen.js +1 -2
  8. package/lib/commonjs/modules/leaveRequest/screens/LeaveRequestScreen.js +7 -7
  9. package/lib/commonjs/modules/leaveRequest/services/leaveRequestService.js +10 -2
  10. package/lib/commonjs/modules/marks/screens/MarksScreen.js +1 -2
  11. package/lib/commonjs/modules/myAttendance/screens/MyAttendanceScreen.js +1 -2
  12. package/lib/commonjs/modules/notes/screens/NotesScreen.js +410 -97
  13. package/lib/commonjs/modules/notes/services/notesService.js +10 -2
  14. package/lib/commonjs/modules/noticeboard/screens/NoticeBoardScreen.js +1 -2
  15. package/lib/commonjs/modules/notification/screens/NotificationScreen.js +1 -2
  16. package/lib/commonjs/modules/promoteStudent/screens/PromoteStudentScreen.js +1 -2
  17. package/lib/commonjs/modules/timetable/screens/TimeTableScreen.js +1 -2
  18. package/lib/module/core/api/endpoints.js +1 -1
  19. package/lib/module/modules/assignment/screens/AssignmentScreen.js +295 -79
  20. package/lib/module/modules/assignment/services/assignmentService.js +2 -2
  21. package/lib/module/modules/leaveRequest/screens/LeaveRequestScreen.js +6 -5
  22. package/lib/module/modules/leaveRequest/services/leaveRequestService.js +10 -2
  23. package/lib/module/modules/notes/screens/NotesScreen.js +410 -96
  24. package/lib/module/modules/notes/services/notesService.js +10 -2
  25. package/lib/typescript/modules/assignment/services/assignmentService.d.ts +1 -1
  26. package/lib/typescript/modules/leaveRequest/services/leaveRequestService.d.ts +1 -1
  27. package/lib/typescript/modules/notes/services/notesService.d.ts +1 -1
  28. package/package.json +3 -3
@@ -12,8 +12,7 @@ var _ErrorState = require("../../../shared/empty-states/ErrorState");
12
12
  var _LoadingState = require("../../../shared/loaders/LoadingState");
13
13
  var _notesService = require("../services/notesService");
14
14
  var _jsxRuntime = require("react/jsx-runtime");
15
- function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
16
- function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
15
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
17
16
  function normalizeList(result) {
18
17
  if (Array.isArray(result)) return result;
19
18
  if (Array.isArray(result?.data)) return result.data;
@@ -32,7 +31,22 @@ function resolveNoteUrl(baseUrl, file) {
32
31
  if (!file) return '';
33
32
  if (/^https?:\/\//i.test(file)) return file;
34
33
  const base = String(baseUrl ?? '').replace(/\/?$/, '/');
35
- const normalized = String(file).replace(/^\/+/, '');
34
+ const normalized = String(file).replace(/\\/g, '/').replace(/^\/+/, '');
35
+ const uploadsIndex = base.toLowerCase().indexOf('uploads/');
36
+ const uploadsBase = uploadsIndex >= 0 ? base.slice(0, uploadsIndex + 'uploads/'.length) : base;
37
+ const uploadsRoot = uploadsIndex >= 0 ? base.slice(0, uploadsIndex) : base;
38
+ if (normalized.startsWith('uploads/')) {
39
+ return `${uploadsRoot}${normalized}`;
40
+ }
41
+ if (normalized.startsWith('school_')) {
42
+ return `${uploadsBase}${normalized}`;
43
+ }
44
+ if (normalized.startsWith('student/note/')) {
45
+ return `${base}${normalized}`;
46
+ }
47
+ if (normalized.includes('/')) {
48
+ return `${base}${normalized}`;
49
+ }
36
50
  return `${base}student/note/${normalized}`;
37
51
  }
38
52
  function isImageUrl(file) {
@@ -48,6 +62,10 @@ function fileBaseName(path) {
48
62
  function ensureFileUri(path) {
49
63
  return path.startsWith('file://') ? path : `file://${path}`;
50
64
  }
65
+ function splitFileCsv(raw) {
66
+ if (!raw) return [];
67
+ return String(raw).split(',').map(item => item.trim()).filter(Boolean);
68
+ }
51
69
  function tryGetImageViewer() {
52
70
  try {
53
71
  const mod = require('react-native-image-viewing');
@@ -95,6 +113,68 @@ async function downloadPdfToLocal(args) {
95
113
  }
96
114
  return ensureFileUri(target);
97
115
  }
116
+ function StatusBanner({
117
+ text,
118
+ busy = false,
119
+ tone = 'info'
120
+ }) {
121
+ const palette = tone === 'error' ? {
122
+ backgroundColor: '#FEF2F2',
123
+ borderColor: '#FECACA',
124
+ textColor: '#B91C1C',
125
+ spinnerColor: '#DC2626'
126
+ } : tone === 'success' ? {
127
+ backgroundColor: '#ECFDF5',
128
+ borderColor: '#A7F3D0',
129
+ textColor: '#047857',
130
+ spinnerColor: '#059669'
131
+ } : tone === 'warning' ? {
132
+ backgroundColor: '#FFF7ED',
133
+ borderColor: '#FED7AA',
134
+ textColor: '#C2410C',
135
+ spinnerColor: '#EA580C'
136
+ } : {
137
+ backgroundColor: '#EFF6FF',
138
+ borderColor: '#BFDBFE',
139
+ textColor: '#1D4ED8',
140
+ spinnerColor: '#2563EB'
141
+ };
142
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
143
+ style: [styles.statusBanner, {
144
+ backgroundColor: palette.backgroundColor,
145
+ borderColor: palette.borderColor
146
+ }],
147
+ children: [busy ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
148
+ size: "small",
149
+ color: palette.spinnerColor
150
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
151
+ style: [styles.statusBannerText, {
152
+ color: palette.textColor
153
+ }],
154
+ children: text
155
+ })]
156
+ });
157
+ }
158
+ async function uploadNoteFilesBatch(args) {
159
+ const uploaded = [];
160
+ let failedCount = 0;
161
+ await Promise.all(args.files.map(async file => {
162
+ try {
163
+ const response = await (0, _notesService.uploadNoteFile)(args.api, file, args.schoolCode);
164
+ if (response?.Status === 'Success' && response?.data) {
165
+ uploaded.push(String(response.data));
166
+ return;
167
+ }
168
+ } catch {
169
+ // Keep partial success behavior for multi-file uploads.
170
+ }
171
+ failedCount += 1;
172
+ }));
173
+ return {
174
+ uploaded,
175
+ failedCount
176
+ };
177
+ }
98
178
  function SelectField({
99
179
  label,
100
180
  value,
@@ -167,7 +247,10 @@ function NoteEditor(props) {
167
247
  const RNFS = (0, _react.useMemo)(() => tryGetRNFS(), []);
168
248
  const FileViewer = (0, _react.useMemo)(() => tryGetFileViewer(), []);
169
249
  const [loading, setLoading] = (0, _react.useState)(props.mode === 'edit');
170
- const [saving, setSaving] = (0, _react.useState)(false);
250
+ const [isUploading, setIsUploading] = (0, _react.useState)(false);
251
+ const [isSubmitting, setIsSubmitting] = (0, _react.useState)(false);
252
+ const [uploadStatus, setUploadStatus] = (0, _react.useState)('');
253
+ const [uploadTone, setUploadTone] = (0, _react.useState)('info');
171
254
  const [classes, setClasses] = (0, _react.useState)([]);
172
255
  const [sections, setSections] = (0, _react.useState)([]);
173
256
  const [subjects, setSubjects] = (0, _react.useState)([]);
@@ -177,8 +260,12 @@ function NoteEditor(props) {
177
260
  const [classOpt, setClassOpt] = (0, _react.useState)(null);
178
261
  const [sectionOpt, setSectionOpt] = (0, _react.useState)(null);
179
262
  const [subjectOpt, setSubjectOpt] = (0, _react.useState)(null);
180
- const [file, setFile] = (0, _react.useState)('');
263
+ const [fileList, setFileList] = (0, _react.useState)([]);
264
+ const [activeFile, setActiveFile] = (0, _react.useState)('');
265
+ const [fileError, setFileError] = (0, _react.useState)(null);
181
266
  const [viewerOpen, setViewerOpen] = (0, _react.useState)(false);
267
+ const [viewerIndex, setViewerIndex] = (0, _react.useState)(0);
268
+ const [viewerImages, setViewerImages] = (0, _react.useState)([]);
182
269
  const [pdfVisible, setPdfVisible] = (0, _react.useState)(false);
183
270
  const [pdfUri, setPdfUri] = (0, _react.useState)('');
184
271
  const [pdfHeaders, setPdfHeaders] = (0, _react.useState)({});
@@ -231,7 +318,9 @@ function NoteEditor(props) {
231
318
  setTitle(String(data?.title ?? ''));
232
319
  setDescription(String(data?.description ?? data?.remark ?? ''));
233
320
  setStatus(String(data?.status ?? 'publish').toLowerCase() === 'unpublish' ? 'unpublish' : 'publish');
234
- setFile(String(data?.file ?? ''));
321
+ const noteFiles = splitFileCsv(data?.file ?? '');
322
+ setFileList(noteFiles);
323
+ setActiveFile(noteFiles[0] ?? '');
235
324
  const classId = data?.class_id ?? props.defaults.classId;
236
325
  const sectionId = data?.section_id ?? props.defaults.sectionId;
237
326
  const subjectId = data?.sub_id ?? data?.subject_id ?? props.defaults.subjectId;
@@ -281,27 +370,51 @@ function NoteEditor(props) {
281
370
  dp = require('react-native-document-picker');
282
371
  } catch {}
283
372
  if (!dp) return;
284
- const selected = await dp.pickSingle({
373
+ const selected = await dp.pick({
285
374
  presentationStyle: 'fullScreen',
375
+ allowMultiSelection: true,
286
376
  type: [dp.types.pdf, dp.types.images]
287
377
  });
288
- const upload = {
289
- uri: selected.uri,
290
- name: selected.name ?? 'file',
291
- type: selected.type ?? 'application/octet-stream'
292
- };
293
- setSaving(true);
378
+ const picks = Array.isArray(selected) ? selected : [selected];
379
+ const uploads = picks.map(item => ({
380
+ uri: item.uri,
381
+ name: item.name ?? 'file',
382
+ type: item.type ?? 'application/octet-stream'
383
+ })).filter(item => !!item.uri);
384
+ if (!uploads.length) return;
385
+ setFileError(null);
386
+ setUploadStatus(`Uploading ${uploads.length} file(s)...`);
387
+ setUploadTone('info');
388
+ setIsUploading(true);
294
389
  try {
295
- const res = await (0, _notesService.uploadNoteFile)(api, upload);
296
- if (res?.Status === 'Success' && res?.data) {
297
- setFile(String(res.data));
298
- } else {
299
- _reactNative.Alert.alert('Error', 'File not uploaded');
390
+ const {
391
+ uploaded,
392
+ failedCount
393
+ } = await uploadNoteFilesBatch({
394
+ api,
395
+ files: uploads,
396
+ schoolCode
397
+ });
398
+ if (!uploaded.length) {
399
+ setFileError('File upload failed');
400
+ setUploadStatus('File upload failed');
401
+ setUploadTone('error');
402
+ return;
403
+ }
404
+ setFileList(prev => [...prev, ...uploaded]);
405
+ setActiveFile(prev => prev || uploaded[0] || '');
406
+ if (failedCount > 0) {
407
+ setFileError(`${failedCount} file(s) failed to upload`);
408
+ setUploadStatus(`${uploaded.length} file(s) uploaded and ${failedCount} failed.`);
409
+ setUploadTone('warning');
410
+ return;
300
411
  }
412
+ setUploadStatus(`${uploaded.length} file(s) uploaded successfully.`);
413
+ setUploadTone('success');
301
414
  } finally {
302
- setSaving(false);
415
+ setIsUploading(false);
303
416
  }
304
- }, [api]);
417
+ }, [api, schoolCode]);
305
418
  const pickCamera = (0, _react.useCallback)(async () => {
306
419
  let picker = null;
307
420
  try {
@@ -322,30 +435,51 @@ function NoteEditor(props) {
322
435
  name: asset.fileName ?? 'camera.jpg',
323
436
  type: asset.type ?? 'image/jpeg'
324
437
  };
325
- setSaving(true);
438
+ setFileError(null);
439
+ setUploadStatus('Uploading image...');
440
+ setUploadTone('info');
441
+ setIsUploading(true);
326
442
  try {
327
- const response = await (0, _notesService.uploadNoteFile)(api, upload);
443
+ const response = await (0, _notesService.uploadNoteFile)(api, upload, schoolCode);
328
444
  if (response?.Status === 'Success' && response?.data) {
329
- setFile(String(response.data));
445
+ const nextFile = String(response.data);
446
+ setFileList(prev => [...prev, nextFile]);
447
+ setActiveFile(prev => prev || nextFile);
448
+ setUploadStatus('Image uploaded successfully.');
449
+ setUploadTone('success');
330
450
  } else {
331
- _reactNative.Alert.alert('Error', 'File not uploaded');
451
+ setFileError('Image upload failed');
452
+ setUploadStatus(String(response?.msg ?? 'Image upload failed'));
453
+ setUploadTone('error');
332
454
  }
333
455
  } finally {
334
- setSaving(false);
456
+ setIsUploading(false);
335
457
  }
336
- }, [api]);
337
- const viewAttachment = (0, _react.useCallback)(async () => {
338
- if (!file) {
458
+ }, [api, schoolCode]);
459
+ const viewAttachment = (0, _react.useCallback)(async selectedFile => {
460
+ const targetFile = String(selectedFile ?? activeFile ?? fileList[0] ?? '');
461
+ if (!targetFile) {
339
462
  _reactNative.Alert.alert('No Attachment');
340
463
  return;
341
464
  }
342
- const url = resolveNoteUrl(fileBaseUrl, file);
465
+ const url = resolveNoteUrl(fileBaseUrl, targetFile);
343
466
  const headers = await resolveHeaders();
344
- if (ImageView && isImageUrl(file)) {
467
+ const targetIsImage = isImageUrl(targetFile) || isImageUrl(url);
468
+ if (ImageView && targetIsImage) {
469
+ const imageFiles = fileList.filter(currentFile => {
470
+ const currentUrl = resolveNoteUrl(fileBaseUrl, currentFile);
471
+ return isImageUrl(currentFile) || isImageUrl(currentUrl);
472
+ });
473
+ const nextIndex = Math.max(0, imageFiles.findIndex(currentFile => String(currentFile) === String(targetFile)));
474
+ setViewerImages(imageFiles.map(currentFile => ({
475
+ uri: resolveNoteUrl(fileBaseUrl, currentFile),
476
+ headers: Object.keys(headers).length ? headers : undefined
477
+ })));
478
+ setViewerIndex(nextIndex);
345
479
  setViewerOpen(true);
346
480
  return;
347
481
  }
348
- const isPdf = String(file).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
482
+ const isPdf = String(targetFile).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
349
483
  if (Pdf && isPdf) {
350
484
  if (!RNFS) {
351
485
  setPdfHeaders(headers);
@@ -359,7 +493,7 @@ function NoteEditor(props) {
359
493
  const local = await downloadPdfToLocal({
360
494
  RNFS,
361
495
  url,
362
- fileName: fileBaseName(file),
496
+ fileName: fileBaseName(targetFile),
363
497
  headers
364
498
  });
365
499
  setPdfHeaders({});
@@ -379,7 +513,7 @@ function NoteEditor(props) {
379
513
  }
380
514
  setDownloading(true);
381
515
  try {
382
- const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(file)}`;
516
+ const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(targetFile)}`;
383
517
  await RNFS.downloadFile({
384
518
  fromUrl: url,
385
519
  toFile: localFile,
@@ -393,10 +527,23 @@ function NoteEditor(props) {
393
527
  } finally {
394
528
  setDownloading(false);
395
529
  }
396
- }, [FileViewer, ImageView, Pdf, RNFS, file, fileBaseUrl, resolveHeaders]);
530
+ }, [FileViewer, ImageView, Pdf, RNFS, activeFile, fileBaseUrl, fileList, resolveHeaders]);
397
531
  const submit = (0, _react.useCallback)(async () => {
398
- if (!title || !description || !classOpt?.value || !sectionOpt?.value || !subjectOpt?.value || !file) {
399
- _reactNative.Alert.alert('Error', 'Please enter all the details');
532
+ const selectedClassId = classOpt?.value;
533
+ const selectedSectionId = sectionOpt?.value;
534
+ const selectedSubjectId = subjectOpt?.value;
535
+ const missing = [];
536
+ if (!title) missing.push('title');
537
+ if (!description) missing.push('description');
538
+ if (!selectedClassId) missing.push('class');
539
+ if (!selectedSectionId) missing.push('section');
540
+ if (!selectedSubjectId) missing.push('subject');
541
+ if (props.mode === 'edit' && !fileList.length) missing.push('file');
542
+ if (missing.length) {
543
+ if (props.mode === 'edit' && !fileList.length) {
544
+ setFileError('File is required');
545
+ }
546
+ _reactNative.Alert.alert('Error', `Please fill: ${missing.join(', ')}`);
400
547
  return;
401
548
  }
402
549
  const payload = {
@@ -404,13 +551,13 @@ function NoteEditor(props) {
404
551
  title,
405
552
  description,
406
553
  remark: description,
407
- class_id: classOpt.value,
408
- section_id: sectionOpt.value,
409
- sub_id: subjectOpt.value,
410
- file,
554
+ class_id: selectedClassId,
555
+ section_id: selectedSectionId,
556
+ sub_id: selectedSubjectId,
557
+ file: fileList.join(','),
411
558
  status
412
559
  };
413
- setSaving(true);
560
+ setIsSubmitting(true);
414
561
  try {
415
562
  const res = props.mode === 'create' ? await (0, _notesService.createNote)(api, payload) : await (0, _notesService.updateNote)(api, {
416
563
  ...payload,
@@ -426,13 +573,13 @@ function NoteEditor(props) {
426
573
  } catch (e) {
427
574
  _reactNative.Alert.alert('Error', String(e?.message ?? 'Could not save note'));
428
575
  } finally {
429
- setSaving(false);
576
+ setIsSubmitting(false);
430
577
  }
431
- }, [api, classOpt?.value, description, file, props, sectionOpt?.value, status, subjectOpt?.value, title]);
578
+ }, [api, classOpt?.value, description, fileList, props, sectionOpt?.value, status, subjectOpt?.value, title]);
432
579
  if (loading) {
433
580
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_LoadingState.LoadingState, {});
434
581
  }
435
- const fileUrl = file ? resolveNoteUrl(fileBaseUrl, file) : '';
582
+ const fileUrl = activeFile ? resolveNoteUrl(fileBaseUrl, activeFile) : '';
436
583
  const headers = authToken || schoolCode ? {
437
584
  ...(authToken ? {
438
585
  Authorization: authToken
@@ -490,7 +637,11 @@ function NoteEditor(props) {
490
637
  value: classOpt,
491
638
  options: classes,
492
639
  placeholder: "Select class",
493
- onChange: setClassOpt
640
+ onChange: next => {
641
+ setClassOpt(next);
642
+ setSectionOpt(null);
643
+ setSubjectOpt(null);
644
+ }
494
645
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SelectField, {
495
646
  label: "Section",
496
647
  value: sectionOpt,
@@ -536,24 +687,69 @@ function NoteEditor(props) {
536
687
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
537
688
  style: styles.secondaryBtn,
538
689
  onPress: () => pickDocument().catch(() => {}),
690
+ disabled: isUploading || isSubmitting,
539
691
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
540
692
  style: styles.secondaryBtnText,
541
- children: file ? 'Change File' : 'Upload File'
693
+ children: fileList.length ? 'Add More Files' : 'Upload File'
542
694
  })
543
695
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
544
696
  style: styles.secondaryBtn,
545
697
  onPress: () => pickCamera().catch(() => {}),
698
+ disabled: isUploading || isSubmitting,
546
699
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
547
700
  style: styles.secondaryBtnText,
548
701
  children: "Camera"
549
702
  })
550
703
  })]
551
- }), file ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
704
+ }), isUploading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(StatusBanner, {
705
+ text: uploadStatus || 'Uploading file...',
706
+ busy: true,
707
+ tone: uploadTone
708
+ }) : uploadStatus ? /*#__PURE__*/(0, _jsxRuntime.jsx)(StatusBanner, {
709
+ text: uploadStatus,
710
+ tone: uploadTone
711
+ }) : null, fileList.length ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
552
712
  style: styles.fileText,
553
- children: ["Selected: ", fileBaseName(file)]
554
- }) : null, file ? isImageUrl(file) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
713
+ children: ["Selected: ", fileList.length, " file", fileList.length > 1 ? 's' : '']
714
+ }) : null, fileList.length ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
715
+ style: styles.attachmentList,
716
+ children: fileList.map((currentFile, index) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
717
+ style: styles.attachmentRow,
718
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
719
+ style: styles.attachmentName,
720
+ numberOfLines: 1,
721
+ children: fileBaseName(currentFile)
722
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
723
+ style: styles.attachmentActions,
724
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
725
+ style: styles.inlineActionBtn,
726
+ onPress: () => {
727
+ setActiveFile(currentFile);
728
+ viewAttachment(currentFile).catch(() => {});
729
+ },
730
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
731
+ style: styles.inlineActionText,
732
+ children: "View"
733
+ })
734
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
735
+ style: styles.inlineRemoveBtn,
736
+ onPress: () => {
737
+ setFileList(prev => {
738
+ const next = prev.filter((_, itemIndex) => itemIndex !== index);
739
+ setActiveFile(current => current === currentFile ? next[0] ?? '' : current);
740
+ return next;
741
+ });
742
+ },
743
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
744
+ style: styles.inlineRemoveText,
745
+ children: "Remove"
746
+ })
747
+ })]
748
+ })]
749
+ }, `${currentFile}-${index}`))
750
+ }) : null, activeFile ? isImageUrl(activeFile) || isImageUrl(fileUrl) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
555
751
  style: styles.previewWrap,
556
- onPress: () => setViewerOpen(true),
752
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
557
753
  activeOpacity: 0.9,
558
754
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
559
755
  source: {
@@ -565,31 +761,37 @@ function NoteEditor(props) {
565
761
  })
566
762
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
567
763
  style: styles.documentBtn,
568
- onPress: () => viewAttachment().catch(() => {}),
764
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
569
765
  disabled: downloading,
570
766
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
571
767
  style: styles.documentBtnText,
572
768
  children: downloading ? 'Opening Document...' : 'View Document'
573
769
  })
770
+ }) : null, fileError ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
771
+ style: styles.errorText,
772
+ children: fileError
574
773
  }) : null]
575
774
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
576
775
  style: styles.primaryBtn,
577
776
  onPress: () => submit().catch(() => {}),
578
- disabled: saving,
579
- children: saving ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
580
- color: "#FFFFFF"
777
+ disabled: isUploading || isSubmitting,
778
+ children: isSubmitting ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
779
+ style: styles.loadingRow,
780
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
781
+ color: "#FFFFFF"
782
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
783
+ style: styles.primaryBtnText,
784
+ children: props.mode === 'create' ? 'Creating...' : 'Updating...'
785
+ })]
581
786
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
582
787
  style: styles.primaryBtnText,
583
788
  children: props.mode === 'create' ? 'Create Note' : 'Update Note'
584
789
  })
585
790
  })]
586
791
  })]
587
- }), ImageView && file && isImageUrl(file) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
588
- images: [{
589
- uri: fileUrl,
590
- headers
591
- }],
592
- imageIndex: 0,
792
+ }), ImageView && viewerImages.length ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
793
+ images: viewerImages,
794
+ imageIndex: viewerIndex,
593
795
  visible: viewerOpen,
594
796
  onRequestClose: () => setViewerOpen(false)
595
797
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
@@ -658,7 +860,10 @@ function NotesScreen(props) {
658
860
  const [loadingMore, setLoadingMore] = (0, _react.useState)(false);
659
861
  const [error, setError] = (0, _react.useState)(null);
660
862
  const [selectedNote, setSelectedNote] = (0, _react.useState)(null);
863
+ const [selectedActiveFile, setSelectedActiveFile] = (0, _react.useState)('');
661
864
  const [viewerOpen, setViewerOpen] = (0, _react.useState)(false);
865
+ const [viewerIndex, setViewerIndex] = (0, _react.useState)(0);
866
+ const [viewerImages, setViewerImages] = (0, _react.useState)([]);
662
867
  const [pdfVisible, setPdfVisible] = (0, _react.useState)(false);
663
868
  const [pdfUri, setPdfUri] = (0, _react.useState)('');
664
869
  const [pdfHeaders, setPdfHeaders] = (0, _react.useState)({});
@@ -740,15 +945,26 @@ function NotesScreen(props) {
740
945
  if (mode !== 'list') return;
741
946
  loadList(props.page ?? 1, true).catch(() => {});
742
947
  }, [loadList, mode, props.page]);
743
- const viewNoteAttachment = (0, _react.useCallback)(async () => {
744
- const file = String(selectedNote?.file ?? '');
948
+ const selectedNoteFiles = (0, _react.useMemo)(() => splitFileCsv(selectedNote?.file ?? ''), [selectedNote?.file]);
949
+ const viewNoteAttachment = (0, _react.useCallback)(async selectedFile => {
950
+ const file = String(selectedFile ?? selectedActiveFile ?? selectedNoteFiles[0] ?? '');
745
951
  if (!file) {
746
952
  _reactNative.Alert.alert('No Attachment');
747
953
  return;
748
954
  }
749
955
  const url = resolveNoteUrl(fileBaseUrl, file);
750
956
  const headers = await resolveHeaders();
751
- if (ImageView && isImageUrl(file)) {
957
+ if (ImageView && (isImageUrl(file) || isImageUrl(url))) {
958
+ const imageFiles = selectedNoteFiles.filter(currentFile => {
959
+ const currentUrl = resolveNoteUrl(fileBaseUrl, currentFile);
960
+ return isImageUrl(currentFile) || isImageUrl(currentUrl);
961
+ });
962
+ const nextIndex = Math.max(0, imageFiles.findIndex(currentFile => String(currentFile) === String(file)));
963
+ setViewerImages(imageFiles.map(currentFile => ({
964
+ uri: resolveNoteUrl(fileBaseUrl, currentFile),
965
+ headers: Object.keys(headers).length ? headers : undefined
966
+ })));
967
+ setViewerIndex(nextIndex);
752
968
  setViewerOpen(true);
753
969
  return;
754
970
  }
@@ -800,7 +1016,7 @@ function NotesScreen(props) {
800
1016
  } finally {
801
1017
  setDownloading(false);
802
1018
  }
803
- }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedNote?.file]);
1019
+ }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedActiveFile, selectedNoteFiles]);
804
1020
  if (mode === 'create') {
805
1021
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(NoteEditor, {
806
1022
  mode: "create",
@@ -925,7 +1141,10 @@ function NotesScreen(props) {
925
1141
  style: styles.row,
926
1142
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
927
1143
  style: styles.secondaryBtn,
928
- onPress: () => setSelectedNote(item),
1144
+ onPress: () => {
1145
+ setSelectedNote(item);
1146
+ setSelectedActiveFile(splitFileCsv(item?.file ?? '')[0] ?? '');
1147
+ },
929
1148
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
930
1149
  style: styles.secondaryBtnText,
931
1150
  children: "View"
@@ -988,42 +1207,65 @@ function NotesScreen(props) {
988
1207
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
989
1208
  style: styles.noteDescFull,
990
1209
  children: String(selectedNote?.description ?? selectedNote?.remark ?? '-')
991
- }), selectedNote?.file ? isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
992
- style: styles.previewWrap,
993
- onPress: () => setViewerOpen(true),
994
- activeOpacity: 0.9,
995
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
996
- source: {
997
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file)),
998
- headers: authToken || schoolCode ? {
999
- ...(authToken ? {
1000
- Authorization: authToken
1001
- } : {}),
1002
- ...(schoolCode ? {
1003
- school_code: schoolCode
1004
- } : {})
1005
- } : undefined
1006
- },
1007
- style: styles.previewImage,
1008
- resizeMode: "contain"
1009
- })
1010
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
1011
- style: styles.documentBtn,
1012
- onPress: () => viewNoteAttachment().catch(() => {}),
1013
- disabled: downloading,
1014
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
1015
- style: styles.documentBtnText,
1016
- children: downloading ? 'Opening Document...' : 'View Document'
1017
- })
1210
+ }), selectedNoteFiles.length ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
1211
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
1212
+ style: styles.fileText,
1213
+ children: ["Selected: ", selectedNoteFiles.length, " file", selectedNoteFiles.length > 1 ? 's' : '']
1214
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
1215
+ style: styles.attachmentList,
1216
+ children: selectedNoteFiles.map((currentFile, index) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
1217
+ style: styles.attachmentRow,
1218
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
1219
+ style: styles.attachmentName,
1220
+ numberOfLines: 1,
1221
+ children: fileBaseName(currentFile)
1222
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
1223
+ style: styles.inlineActionBtn,
1224
+ onPress: () => {
1225
+ setSelectedActiveFile(currentFile);
1226
+ viewNoteAttachment(currentFile).catch(() => {});
1227
+ },
1228
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
1229
+ style: styles.inlineActionText,
1230
+ children: "View"
1231
+ })
1232
+ })]
1233
+ }, `${currentFile}-${index}`))
1234
+ }), selectedActiveFile ? isImageUrl(selectedActiveFile) || isImageUrl(resolveNoteUrl(fileBaseUrl, selectedActiveFile)) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
1235
+ style: styles.previewWrap,
1236
+ onPress: () => viewNoteAttachment(selectedActiveFile).catch(() => {}),
1237
+ activeOpacity: 0.9,
1238
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
1239
+ source: {
1240
+ uri: resolveNoteUrl(fileBaseUrl, selectedActiveFile),
1241
+ headers: authToken || schoolCode ? {
1242
+ ...(authToken ? {
1243
+ Authorization: authToken
1244
+ } : {}),
1245
+ ...(schoolCode ? {
1246
+ school_code: schoolCode
1247
+ } : {})
1248
+ } : undefined
1249
+ },
1250
+ style: styles.previewImage,
1251
+ resizeMode: "contain"
1252
+ })
1253
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
1254
+ style: styles.documentBtn,
1255
+ onPress: () => viewNoteAttachment(selectedActiveFile).catch(() => {}),
1256
+ disabled: downloading,
1257
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
1258
+ style: styles.documentBtnText,
1259
+ children: downloading ? 'Opening Document...' : 'View Document'
1260
+ })
1261
+ }) : null]
1018
1262
  }) : null]
1019
1263
  }) : null]
1020
1264
  })
1021
1265
  })
1022
- }), ImageView && selectedNote?.file && isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
1023
- images: [{
1024
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file))
1025
- }],
1026
- imageIndex: 0,
1266
+ }), ImageView && viewerImages.length ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
1267
+ images: viewerImages,
1268
+ imageIndex: viewerIndex,
1027
1269
  visible: viewerOpen,
1028
1270
  onRequestClose: () => setViewerOpen(false)
1029
1271
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
@@ -1195,6 +1437,51 @@ const styles = _reactNative.StyleSheet.create({
1195
1437
  color: '#374151',
1196
1438
  marginTop: 8
1197
1439
  },
1440
+ attachmentList: {
1441
+ width: '100%',
1442
+ marginTop: 8
1443
+ },
1444
+ attachmentRow: {
1445
+ flexDirection: 'row',
1446
+ alignItems: 'center',
1447
+ justifyContent: 'space-between',
1448
+ paddingVertical: 8,
1449
+ borderBottomWidth: 1,
1450
+ borderBottomColor: '#E5E7EB',
1451
+ gap: 10
1452
+ },
1453
+ attachmentName: {
1454
+ flex: 1,
1455
+ color: '#111827',
1456
+ fontSize: 13
1457
+ },
1458
+ attachmentActions: {
1459
+ flexDirection: 'row',
1460
+ alignItems: 'center',
1461
+ gap: 8
1462
+ },
1463
+ inlineActionBtn: {
1464
+ paddingHorizontal: 10,
1465
+ paddingVertical: 6,
1466
+ borderRadius: 8,
1467
+ backgroundColor: '#DBEAFE'
1468
+ },
1469
+ inlineActionText: {
1470
+ color: '#1D4ED8',
1471
+ fontWeight: '700',
1472
+ fontSize: 12
1473
+ },
1474
+ inlineRemoveBtn: {
1475
+ paddingHorizontal: 10,
1476
+ paddingVertical: 6,
1477
+ borderRadius: 8,
1478
+ backgroundColor: '#FEE2E2'
1479
+ },
1480
+ inlineRemoveText: {
1481
+ color: '#B91C1C',
1482
+ fontWeight: '700',
1483
+ fontSize: 12
1484
+ },
1198
1485
  previewWrap: {
1199
1486
  marginTop: 12,
1200
1487
  borderWidth: 1,
@@ -1283,5 +1570,31 @@ const styles = _reactNative.StyleSheet.create({
1283
1570
  color: '#6B7280',
1284
1571
  textAlign: 'center',
1285
1572
  paddingVertical: 16
1573
+ },
1574
+ statusBanner: {
1575
+ marginTop: 10,
1576
+ paddingHorizontal: 12,
1577
+ paddingVertical: 10,
1578
+ borderRadius: 12,
1579
+ borderWidth: 1,
1580
+ flexDirection: 'row',
1581
+ alignItems: 'center',
1582
+ gap: 10
1583
+ },
1584
+ statusBannerText: {
1585
+ flex: 1,
1586
+ fontSize: 12,
1587
+ fontWeight: '700'
1588
+ },
1589
+ errorText: {
1590
+ marginTop: 6,
1591
+ fontSize: 12,
1592
+ color: '#DC2626',
1593
+ fontWeight: '600'
1594
+ },
1595
+ loadingRow: {
1596
+ flexDirection: 'row',
1597
+ alignItems: 'center',
1598
+ gap: 8
1286
1599
  }
1287
1600
  });