@ubkinfotech/tecaher-erp 0.1.1 → 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.
@@ -31,7 +31,22 @@ function resolveNoteUrl(baseUrl, file) {
31
31
  if (!file) return '';
32
32
  if (/^https?:\/\//i.test(file)) return file;
33
33
  const base = String(baseUrl ?? '').replace(/\/?$/, '/');
34
- 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
+ }
35
50
  return `${base}student/note/${normalized}`;
36
51
  }
37
52
  function isImageUrl(file) {
@@ -47,6 +62,10 @@ function fileBaseName(path) {
47
62
  function ensureFileUri(path) {
48
63
  return path.startsWith('file://') ? path : `file://${path}`;
49
64
  }
65
+ function splitFileCsv(raw) {
66
+ if (!raw) return [];
67
+ return String(raw).split(',').map(item => item.trim()).filter(Boolean);
68
+ }
50
69
  function tryGetImageViewer() {
51
70
  try {
52
71
  const mod = require('react-native-image-viewing');
@@ -94,6 +113,68 @@ async function downloadPdfToLocal(args) {
94
113
  }
95
114
  return ensureFileUri(target);
96
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
+ }
97
178
  function SelectField({
98
179
  label,
99
180
  value,
@@ -166,7 +247,10 @@ function NoteEditor(props) {
166
247
  const RNFS = (0, _react.useMemo)(() => tryGetRNFS(), []);
167
248
  const FileViewer = (0, _react.useMemo)(() => tryGetFileViewer(), []);
168
249
  const [loading, setLoading] = (0, _react.useState)(props.mode === 'edit');
169
- 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');
170
254
  const [classes, setClasses] = (0, _react.useState)([]);
171
255
  const [sections, setSections] = (0, _react.useState)([]);
172
256
  const [subjects, setSubjects] = (0, _react.useState)([]);
@@ -176,8 +260,12 @@ function NoteEditor(props) {
176
260
  const [classOpt, setClassOpt] = (0, _react.useState)(null);
177
261
  const [sectionOpt, setSectionOpt] = (0, _react.useState)(null);
178
262
  const [subjectOpt, setSubjectOpt] = (0, _react.useState)(null);
179
- 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);
180
266
  const [viewerOpen, setViewerOpen] = (0, _react.useState)(false);
267
+ const [viewerIndex, setViewerIndex] = (0, _react.useState)(0);
268
+ const [viewerImages, setViewerImages] = (0, _react.useState)([]);
181
269
  const [pdfVisible, setPdfVisible] = (0, _react.useState)(false);
182
270
  const [pdfUri, setPdfUri] = (0, _react.useState)('');
183
271
  const [pdfHeaders, setPdfHeaders] = (0, _react.useState)({});
@@ -230,7 +318,9 @@ function NoteEditor(props) {
230
318
  setTitle(String(data?.title ?? ''));
231
319
  setDescription(String(data?.description ?? data?.remark ?? ''));
232
320
  setStatus(String(data?.status ?? 'publish').toLowerCase() === 'unpublish' ? 'unpublish' : 'publish');
233
- setFile(String(data?.file ?? ''));
321
+ const noteFiles = splitFileCsv(data?.file ?? '');
322
+ setFileList(noteFiles);
323
+ setActiveFile(noteFiles[0] ?? '');
234
324
  const classId = data?.class_id ?? props.defaults.classId;
235
325
  const sectionId = data?.section_id ?? props.defaults.sectionId;
236
326
  const subjectId = data?.sub_id ?? data?.subject_id ?? props.defaults.subjectId;
@@ -280,27 +370,51 @@ function NoteEditor(props) {
280
370
  dp = require('react-native-document-picker');
281
371
  } catch {}
282
372
  if (!dp) return;
283
- const selected = await dp.pickSingle({
373
+ const selected = await dp.pick({
284
374
  presentationStyle: 'fullScreen',
375
+ allowMultiSelection: true,
285
376
  type: [dp.types.pdf, dp.types.images]
286
377
  });
287
- const upload = {
288
- uri: selected.uri,
289
- name: selected.name ?? 'file',
290
- type: selected.type ?? 'application/octet-stream'
291
- };
292
- 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);
293
389
  try {
294
- const res = await (0, _notesService.uploadNoteFile)(api, upload);
295
- if (res?.Status === 'Success' && res?.data) {
296
- setFile(String(res.data));
297
- } else {
298
- _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;
299
411
  }
412
+ setUploadStatus(`${uploaded.length} file(s) uploaded successfully.`);
413
+ setUploadTone('success');
300
414
  } finally {
301
- setSaving(false);
415
+ setIsUploading(false);
302
416
  }
303
- }, [api]);
417
+ }, [api, schoolCode]);
304
418
  const pickCamera = (0, _react.useCallback)(async () => {
305
419
  let picker = null;
306
420
  try {
@@ -321,30 +435,51 @@ function NoteEditor(props) {
321
435
  name: asset.fileName ?? 'camera.jpg',
322
436
  type: asset.type ?? 'image/jpeg'
323
437
  };
324
- setSaving(true);
438
+ setFileError(null);
439
+ setUploadStatus('Uploading image...');
440
+ setUploadTone('info');
441
+ setIsUploading(true);
325
442
  try {
326
- const response = await (0, _notesService.uploadNoteFile)(api, upload);
443
+ const response = await (0, _notesService.uploadNoteFile)(api, upload, schoolCode);
327
444
  if (response?.Status === 'Success' && response?.data) {
328
- 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');
329
450
  } else {
330
- _reactNative.Alert.alert('Error', 'File not uploaded');
451
+ setFileError('Image upload failed');
452
+ setUploadStatus(String(response?.msg ?? 'Image upload failed'));
453
+ setUploadTone('error');
331
454
  }
332
455
  } finally {
333
- setSaving(false);
456
+ setIsUploading(false);
334
457
  }
335
- }, [api]);
336
- const viewAttachment = (0, _react.useCallback)(async () => {
337
- if (!file) {
458
+ }, [api, schoolCode]);
459
+ const viewAttachment = (0, _react.useCallback)(async selectedFile => {
460
+ const targetFile = String(selectedFile ?? activeFile ?? fileList[0] ?? '');
461
+ if (!targetFile) {
338
462
  _reactNative.Alert.alert('No Attachment');
339
463
  return;
340
464
  }
341
- const url = resolveNoteUrl(fileBaseUrl, file);
465
+ const url = resolveNoteUrl(fileBaseUrl, targetFile);
342
466
  const headers = await resolveHeaders();
343
- 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);
344
479
  setViewerOpen(true);
345
480
  return;
346
481
  }
347
- const isPdf = String(file).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
482
+ const isPdf = String(targetFile).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
348
483
  if (Pdf && isPdf) {
349
484
  if (!RNFS) {
350
485
  setPdfHeaders(headers);
@@ -358,7 +493,7 @@ function NoteEditor(props) {
358
493
  const local = await downloadPdfToLocal({
359
494
  RNFS,
360
495
  url,
361
- fileName: fileBaseName(file),
496
+ fileName: fileBaseName(targetFile),
362
497
  headers
363
498
  });
364
499
  setPdfHeaders({});
@@ -378,7 +513,7 @@ function NoteEditor(props) {
378
513
  }
379
514
  setDownloading(true);
380
515
  try {
381
- const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(file)}`;
516
+ const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(targetFile)}`;
382
517
  await RNFS.downloadFile({
383
518
  fromUrl: url,
384
519
  toFile: localFile,
@@ -392,10 +527,23 @@ function NoteEditor(props) {
392
527
  } finally {
393
528
  setDownloading(false);
394
529
  }
395
- }, [FileViewer, ImageView, Pdf, RNFS, file, fileBaseUrl, resolveHeaders]);
530
+ }, [FileViewer, ImageView, Pdf, RNFS, activeFile, fileBaseUrl, fileList, resolveHeaders]);
396
531
  const submit = (0, _react.useCallback)(async () => {
397
- if (!title || !description || !classOpt?.value || !sectionOpt?.value || !subjectOpt?.value || !file) {
398
- _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(', ')}`);
399
547
  return;
400
548
  }
401
549
  const payload = {
@@ -403,13 +551,13 @@ function NoteEditor(props) {
403
551
  title,
404
552
  description,
405
553
  remark: description,
406
- class_id: classOpt.value,
407
- section_id: sectionOpt.value,
408
- sub_id: subjectOpt.value,
409
- file,
554
+ class_id: selectedClassId,
555
+ section_id: selectedSectionId,
556
+ sub_id: selectedSubjectId,
557
+ file: fileList.join(','),
410
558
  status
411
559
  };
412
- setSaving(true);
560
+ setIsSubmitting(true);
413
561
  try {
414
562
  const res = props.mode === 'create' ? await (0, _notesService.createNote)(api, payload) : await (0, _notesService.updateNote)(api, {
415
563
  ...payload,
@@ -425,13 +573,13 @@ function NoteEditor(props) {
425
573
  } catch (e) {
426
574
  _reactNative.Alert.alert('Error', String(e?.message ?? 'Could not save note'));
427
575
  } finally {
428
- setSaving(false);
576
+ setIsSubmitting(false);
429
577
  }
430
- }, [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]);
431
579
  if (loading) {
432
580
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_LoadingState.LoadingState, {});
433
581
  }
434
- const fileUrl = file ? resolveNoteUrl(fileBaseUrl, file) : '';
582
+ const fileUrl = activeFile ? resolveNoteUrl(fileBaseUrl, activeFile) : '';
435
583
  const headers = authToken || schoolCode ? {
436
584
  ...(authToken ? {
437
585
  Authorization: authToken
@@ -489,7 +637,11 @@ function NoteEditor(props) {
489
637
  value: classOpt,
490
638
  options: classes,
491
639
  placeholder: "Select class",
492
- onChange: setClassOpt
640
+ onChange: next => {
641
+ setClassOpt(next);
642
+ setSectionOpt(null);
643
+ setSubjectOpt(null);
644
+ }
493
645
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SelectField, {
494
646
  label: "Section",
495
647
  value: sectionOpt,
@@ -535,24 +687,69 @@ function NoteEditor(props) {
535
687
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
536
688
  style: styles.secondaryBtn,
537
689
  onPress: () => pickDocument().catch(() => {}),
690
+ disabled: isUploading || isSubmitting,
538
691
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
539
692
  style: styles.secondaryBtnText,
540
- children: file ? 'Change File' : 'Upload File'
693
+ children: fileList.length ? 'Add More Files' : 'Upload File'
541
694
  })
542
695
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
543
696
  style: styles.secondaryBtn,
544
697
  onPress: () => pickCamera().catch(() => {}),
698
+ disabled: isUploading || isSubmitting,
545
699
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
546
700
  style: styles.secondaryBtnText,
547
701
  children: "Camera"
548
702
  })
549
703
  })]
550
- }), 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, {
551
712
  style: styles.fileText,
552
- children: ["Selected: ", fileBaseName(file)]
553
- }) : 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, {
554
751
  style: styles.previewWrap,
555
- onPress: () => setViewerOpen(true),
752
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
556
753
  activeOpacity: 0.9,
557
754
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
558
755
  source: {
@@ -564,31 +761,37 @@ function NoteEditor(props) {
564
761
  })
565
762
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
566
763
  style: styles.documentBtn,
567
- onPress: () => viewAttachment().catch(() => {}),
764
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
568
765
  disabled: downloading,
569
766
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
570
767
  style: styles.documentBtnText,
571
768
  children: downloading ? 'Opening Document...' : 'View Document'
572
769
  })
770
+ }) : null, fileError ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
771
+ style: styles.errorText,
772
+ children: fileError
573
773
  }) : null]
574
774
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
575
775
  style: styles.primaryBtn,
576
776
  onPress: () => submit().catch(() => {}),
577
- disabled: saving,
578
- children: saving ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
579
- 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
+ })]
580
786
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
581
787
  style: styles.primaryBtnText,
582
788
  children: props.mode === 'create' ? 'Create Note' : 'Update Note'
583
789
  })
584
790
  })]
585
791
  })]
586
- }), ImageView && file && isImageUrl(file) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
587
- images: [{
588
- uri: fileUrl,
589
- headers
590
- }],
591
- imageIndex: 0,
792
+ }), ImageView && viewerImages.length ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
793
+ images: viewerImages,
794
+ imageIndex: viewerIndex,
592
795
  visible: viewerOpen,
593
796
  onRequestClose: () => setViewerOpen(false)
594
797
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
@@ -657,7 +860,10 @@ function NotesScreen(props) {
657
860
  const [loadingMore, setLoadingMore] = (0, _react.useState)(false);
658
861
  const [error, setError] = (0, _react.useState)(null);
659
862
  const [selectedNote, setSelectedNote] = (0, _react.useState)(null);
863
+ const [selectedActiveFile, setSelectedActiveFile] = (0, _react.useState)('');
660
864
  const [viewerOpen, setViewerOpen] = (0, _react.useState)(false);
865
+ const [viewerIndex, setViewerIndex] = (0, _react.useState)(0);
866
+ const [viewerImages, setViewerImages] = (0, _react.useState)([]);
661
867
  const [pdfVisible, setPdfVisible] = (0, _react.useState)(false);
662
868
  const [pdfUri, setPdfUri] = (0, _react.useState)('');
663
869
  const [pdfHeaders, setPdfHeaders] = (0, _react.useState)({});
@@ -739,15 +945,26 @@ function NotesScreen(props) {
739
945
  if (mode !== 'list') return;
740
946
  loadList(props.page ?? 1, true).catch(() => {});
741
947
  }, [loadList, mode, props.page]);
742
- const viewNoteAttachment = (0, _react.useCallback)(async () => {
743
- 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] ?? '');
744
951
  if (!file) {
745
952
  _reactNative.Alert.alert('No Attachment');
746
953
  return;
747
954
  }
748
955
  const url = resolveNoteUrl(fileBaseUrl, file);
749
956
  const headers = await resolveHeaders();
750
- 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);
751
968
  setViewerOpen(true);
752
969
  return;
753
970
  }
@@ -799,7 +1016,7 @@ function NotesScreen(props) {
799
1016
  } finally {
800
1017
  setDownloading(false);
801
1018
  }
802
- }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedNote?.file]);
1019
+ }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedActiveFile, selectedNoteFiles]);
803
1020
  if (mode === 'create') {
804
1021
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(NoteEditor, {
805
1022
  mode: "create",
@@ -924,7 +1141,10 @@ function NotesScreen(props) {
924
1141
  style: styles.row,
925
1142
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
926
1143
  style: styles.secondaryBtn,
927
- onPress: () => setSelectedNote(item),
1144
+ onPress: () => {
1145
+ setSelectedNote(item);
1146
+ setSelectedActiveFile(splitFileCsv(item?.file ?? '')[0] ?? '');
1147
+ },
928
1148
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
929
1149
  style: styles.secondaryBtnText,
930
1150
  children: "View"
@@ -987,42 +1207,65 @@ function NotesScreen(props) {
987
1207
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
988
1208
  style: styles.noteDescFull,
989
1209
  children: String(selectedNote?.description ?? selectedNote?.remark ?? '-')
990
- }), selectedNote?.file ? isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
991
- style: styles.previewWrap,
992
- onPress: () => setViewerOpen(true),
993
- activeOpacity: 0.9,
994
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
995
- source: {
996
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file)),
997
- headers: authToken || schoolCode ? {
998
- ...(authToken ? {
999
- Authorization: authToken
1000
- } : {}),
1001
- ...(schoolCode ? {
1002
- school_code: schoolCode
1003
- } : {})
1004
- } : undefined
1005
- },
1006
- style: styles.previewImage,
1007
- resizeMode: "contain"
1008
- })
1009
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
1010
- style: styles.documentBtn,
1011
- onPress: () => viewNoteAttachment().catch(() => {}),
1012
- disabled: downloading,
1013
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
1014
- style: styles.documentBtnText,
1015
- children: downloading ? 'Opening Document...' : 'View Document'
1016
- })
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]
1017
1262
  }) : null]
1018
1263
  }) : null]
1019
1264
  })
1020
1265
  })
1021
- }), ImageView && selectedNote?.file && isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
1022
- images: [{
1023
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file))
1024
- }],
1025
- imageIndex: 0,
1266
+ }), ImageView && viewerImages.length ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageView, {
1267
+ images: viewerImages,
1268
+ imageIndex: viewerIndex,
1026
1269
  visible: viewerOpen,
1027
1270
  onRequestClose: () => setViewerOpen(false)
1028
1271
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
@@ -1194,6 +1437,51 @@ const styles = _reactNative.StyleSheet.create({
1194
1437
  color: '#374151',
1195
1438
  marginTop: 8
1196
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
+ },
1197
1485
  previewWrap: {
1198
1486
  marginTop: 12,
1199
1487
  borderWidth: 1,
@@ -1282,5 +1570,31 @@ const styles = _reactNative.StyleSheet.create({
1282
1570
  color: '#6B7280',
1283
1571
  textAlign: 'center',
1284
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
1285
1599
  }
1286
1600
  });