@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.
@@ -7,7 +7,7 @@ import { EmptyState } from "../../../shared/empty-states/EmptyState.js";
7
7
  import { ErrorState } from "../../../shared/empty-states/ErrorState.js";
8
8
  import { LoadingState } from "../../../shared/loaders/LoadingState.js";
9
9
  import { createNote, fetchNoteDetails, fetchNotesClasses, fetchNotesList, fetchNotesSections, fetchNotesSubjects, updateNote, uploadNoteFile } from "../services/notesService.js";
10
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
11
  function normalizeList(result) {
12
12
  if (Array.isArray(result)) return result;
13
13
  if (Array.isArray(result?.data)) return result.data;
@@ -26,7 +26,22 @@ function resolveNoteUrl(baseUrl, file) {
26
26
  if (!file) return '';
27
27
  if (/^https?:\/\//i.test(file)) return file;
28
28
  const base = String(baseUrl ?? '').replace(/\/?$/, '/');
29
- const normalized = String(file).replace(/^\/+/, '');
29
+ const normalized = String(file).replace(/\\/g, '/').replace(/^\/+/, '');
30
+ const uploadsIndex = base.toLowerCase().indexOf('uploads/');
31
+ const uploadsBase = uploadsIndex >= 0 ? base.slice(0, uploadsIndex + 'uploads/'.length) : base;
32
+ const uploadsRoot = uploadsIndex >= 0 ? base.slice(0, uploadsIndex) : base;
33
+ if (normalized.startsWith('uploads/')) {
34
+ return `${uploadsRoot}${normalized}`;
35
+ }
36
+ if (normalized.startsWith('school_')) {
37
+ return `${uploadsBase}${normalized}`;
38
+ }
39
+ if (normalized.startsWith('student/note/')) {
40
+ return `${base}${normalized}`;
41
+ }
42
+ if (normalized.includes('/')) {
43
+ return `${base}${normalized}`;
44
+ }
30
45
  return `${base}student/note/${normalized}`;
31
46
  }
32
47
  function isImageUrl(file) {
@@ -42,6 +57,10 @@ function fileBaseName(path) {
42
57
  function ensureFileUri(path) {
43
58
  return path.startsWith('file://') ? path : `file://${path}`;
44
59
  }
60
+ function splitFileCsv(raw) {
61
+ if (!raw) return [];
62
+ return String(raw).split(',').map(item => item.trim()).filter(Boolean);
63
+ }
45
64
  function tryGetImageViewer() {
46
65
  try {
47
66
  const mod = require('react-native-image-viewing');
@@ -89,6 +108,68 @@ async function downloadPdfToLocal(args) {
89
108
  }
90
109
  return ensureFileUri(target);
91
110
  }
111
+ function StatusBanner({
112
+ text,
113
+ busy = false,
114
+ tone = 'info'
115
+ }) {
116
+ const palette = tone === 'error' ? {
117
+ backgroundColor: '#FEF2F2',
118
+ borderColor: '#FECACA',
119
+ textColor: '#B91C1C',
120
+ spinnerColor: '#DC2626'
121
+ } : tone === 'success' ? {
122
+ backgroundColor: '#ECFDF5',
123
+ borderColor: '#A7F3D0',
124
+ textColor: '#047857',
125
+ spinnerColor: '#059669'
126
+ } : tone === 'warning' ? {
127
+ backgroundColor: '#FFF7ED',
128
+ borderColor: '#FED7AA',
129
+ textColor: '#C2410C',
130
+ spinnerColor: '#EA580C'
131
+ } : {
132
+ backgroundColor: '#EFF6FF',
133
+ borderColor: '#BFDBFE',
134
+ textColor: '#1D4ED8',
135
+ spinnerColor: '#2563EB'
136
+ };
137
+ return /*#__PURE__*/_jsxs(View, {
138
+ style: [styles.statusBanner, {
139
+ backgroundColor: palette.backgroundColor,
140
+ borderColor: palette.borderColor
141
+ }],
142
+ children: [busy ? /*#__PURE__*/_jsx(ActivityIndicator, {
143
+ size: "small",
144
+ color: palette.spinnerColor
145
+ }) : null, /*#__PURE__*/_jsx(Text, {
146
+ style: [styles.statusBannerText, {
147
+ color: palette.textColor
148
+ }],
149
+ children: text
150
+ })]
151
+ });
152
+ }
153
+ async function uploadNoteFilesBatch(args) {
154
+ const uploaded = [];
155
+ let failedCount = 0;
156
+ await Promise.all(args.files.map(async file => {
157
+ try {
158
+ const response = await uploadNoteFile(args.api, file, args.schoolCode);
159
+ if (response?.Status === 'Success' && response?.data) {
160
+ uploaded.push(String(response.data));
161
+ return;
162
+ }
163
+ } catch {
164
+ // Keep partial success behavior for multi-file uploads.
165
+ }
166
+ failedCount += 1;
167
+ }));
168
+ return {
169
+ uploaded,
170
+ failedCount
171
+ };
172
+ }
92
173
  function SelectField({
93
174
  label,
94
175
  value,
@@ -161,7 +242,10 @@ function NoteEditor(props) {
161
242
  const RNFS = useMemo(() => tryGetRNFS(), []);
162
243
  const FileViewer = useMemo(() => tryGetFileViewer(), []);
163
244
  const [loading, setLoading] = useState(props.mode === 'edit');
164
- const [saving, setSaving] = useState(false);
245
+ const [isUploading, setIsUploading] = useState(false);
246
+ const [isSubmitting, setIsSubmitting] = useState(false);
247
+ const [uploadStatus, setUploadStatus] = useState('');
248
+ const [uploadTone, setUploadTone] = useState('info');
165
249
  const [classes, setClasses] = useState([]);
166
250
  const [sections, setSections] = useState([]);
167
251
  const [subjects, setSubjects] = useState([]);
@@ -171,8 +255,12 @@ function NoteEditor(props) {
171
255
  const [classOpt, setClassOpt] = useState(null);
172
256
  const [sectionOpt, setSectionOpt] = useState(null);
173
257
  const [subjectOpt, setSubjectOpt] = useState(null);
174
- const [file, setFile] = useState('');
258
+ const [fileList, setFileList] = useState([]);
259
+ const [activeFile, setActiveFile] = useState('');
260
+ const [fileError, setFileError] = useState(null);
175
261
  const [viewerOpen, setViewerOpen] = useState(false);
262
+ const [viewerIndex, setViewerIndex] = useState(0);
263
+ const [viewerImages, setViewerImages] = useState([]);
176
264
  const [pdfVisible, setPdfVisible] = useState(false);
177
265
  const [pdfUri, setPdfUri] = useState('');
178
266
  const [pdfHeaders, setPdfHeaders] = useState({});
@@ -225,7 +313,9 @@ function NoteEditor(props) {
225
313
  setTitle(String(data?.title ?? ''));
226
314
  setDescription(String(data?.description ?? data?.remark ?? ''));
227
315
  setStatus(String(data?.status ?? 'publish').toLowerCase() === 'unpublish' ? 'unpublish' : 'publish');
228
- setFile(String(data?.file ?? ''));
316
+ const noteFiles = splitFileCsv(data?.file ?? '');
317
+ setFileList(noteFiles);
318
+ setActiveFile(noteFiles[0] ?? '');
229
319
  const classId = data?.class_id ?? props.defaults.classId;
230
320
  const sectionId = data?.section_id ?? props.defaults.sectionId;
231
321
  const subjectId = data?.sub_id ?? data?.subject_id ?? props.defaults.subjectId;
@@ -275,27 +365,51 @@ function NoteEditor(props) {
275
365
  dp = require('react-native-document-picker');
276
366
  } catch {}
277
367
  if (!dp) return;
278
- const selected = await dp.pickSingle({
368
+ const selected = await dp.pick({
279
369
  presentationStyle: 'fullScreen',
370
+ allowMultiSelection: true,
280
371
  type: [dp.types.pdf, dp.types.images]
281
372
  });
282
- const upload = {
283
- uri: selected.uri,
284
- name: selected.name ?? 'file',
285
- type: selected.type ?? 'application/octet-stream'
286
- };
287
- setSaving(true);
373
+ const picks = Array.isArray(selected) ? selected : [selected];
374
+ const uploads = picks.map(item => ({
375
+ uri: item.uri,
376
+ name: item.name ?? 'file',
377
+ type: item.type ?? 'application/octet-stream'
378
+ })).filter(item => !!item.uri);
379
+ if (!uploads.length) return;
380
+ setFileError(null);
381
+ setUploadStatus(`Uploading ${uploads.length} file(s)...`);
382
+ setUploadTone('info');
383
+ setIsUploading(true);
288
384
  try {
289
- const res = await uploadNoteFile(api, upload);
290
- if (res?.Status === 'Success' && res?.data) {
291
- setFile(String(res.data));
292
- } else {
293
- Alert.alert('Error', 'File not uploaded');
385
+ const {
386
+ uploaded,
387
+ failedCount
388
+ } = await uploadNoteFilesBatch({
389
+ api,
390
+ files: uploads,
391
+ schoolCode
392
+ });
393
+ if (!uploaded.length) {
394
+ setFileError('File upload failed');
395
+ setUploadStatus('File upload failed');
396
+ setUploadTone('error');
397
+ return;
398
+ }
399
+ setFileList(prev => [...prev, ...uploaded]);
400
+ setActiveFile(prev => prev || uploaded[0] || '');
401
+ if (failedCount > 0) {
402
+ setFileError(`${failedCount} file(s) failed to upload`);
403
+ setUploadStatus(`${uploaded.length} file(s) uploaded and ${failedCount} failed.`);
404
+ setUploadTone('warning');
405
+ return;
294
406
  }
407
+ setUploadStatus(`${uploaded.length} file(s) uploaded successfully.`);
408
+ setUploadTone('success');
295
409
  } finally {
296
- setSaving(false);
410
+ setIsUploading(false);
297
411
  }
298
- }, [api]);
412
+ }, [api, schoolCode]);
299
413
  const pickCamera = useCallback(async () => {
300
414
  let picker = null;
301
415
  try {
@@ -316,30 +430,51 @@ function NoteEditor(props) {
316
430
  name: asset.fileName ?? 'camera.jpg',
317
431
  type: asset.type ?? 'image/jpeg'
318
432
  };
319
- setSaving(true);
433
+ setFileError(null);
434
+ setUploadStatus('Uploading image...');
435
+ setUploadTone('info');
436
+ setIsUploading(true);
320
437
  try {
321
- const response = await uploadNoteFile(api, upload);
438
+ const response = await uploadNoteFile(api, upload, schoolCode);
322
439
  if (response?.Status === 'Success' && response?.data) {
323
- setFile(String(response.data));
440
+ const nextFile = String(response.data);
441
+ setFileList(prev => [...prev, nextFile]);
442
+ setActiveFile(prev => prev || nextFile);
443
+ setUploadStatus('Image uploaded successfully.');
444
+ setUploadTone('success');
324
445
  } else {
325
- Alert.alert('Error', 'File not uploaded');
446
+ setFileError('Image upload failed');
447
+ setUploadStatus(String(response?.msg ?? 'Image upload failed'));
448
+ setUploadTone('error');
326
449
  }
327
450
  } finally {
328
- setSaving(false);
451
+ setIsUploading(false);
329
452
  }
330
- }, [api]);
331
- const viewAttachment = useCallback(async () => {
332
- if (!file) {
453
+ }, [api, schoolCode]);
454
+ const viewAttachment = useCallback(async selectedFile => {
455
+ const targetFile = String(selectedFile ?? activeFile ?? fileList[0] ?? '');
456
+ if (!targetFile) {
333
457
  Alert.alert('No Attachment');
334
458
  return;
335
459
  }
336
- const url = resolveNoteUrl(fileBaseUrl, file);
460
+ const url = resolveNoteUrl(fileBaseUrl, targetFile);
337
461
  const headers = await resolveHeaders();
338
- if (ImageView && isImageUrl(file)) {
462
+ const targetIsImage = isImageUrl(targetFile) || isImageUrl(url);
463
+ if (ImageView && targetIsImage) {
464
+ const imageFiles = fileList.filter(currentFile => {
465
+ const currentUrl = resolveNoteUrl(fileBaseUrl, currentFile);
466
+ return isImageUrl(currentFile) || isImageUrl(currentUrl);
467
+ });
468
+ const nextIndex = Math.max(0, imageFiles.findIndex(currentFile => String(currentFile) === String(targetFile)));
469
+ setViewerImages(imageFiles.map(currentFile => ({
470
+ uri: resolveNoteUrl(fileBaseUrl, currentFile),
471
+ headers: Object.keys(headers).length ? headers : undefined
472
+ })));
473
+ setViewerIndex(nextIndex);
339
474
  setViewerOpen(true);
340
475
  return;
341
476
  }
342
- const isPdf = String(file).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
477
+ const isPdf = String(targetFile).toLowerCase().endsWith('.pdf') || url.toLowerCase().endsWith('.pdf');
343
478
  if (Pdf && isPdf) {
344
479
  if (!RNFS) {
345
480
  setPdfHeaders(headers);
@@ -353,7 +488,7 @@ function NoteEditor(props) {
353
488
  const local = await downloadPdfToLocal({
354
489
  RNFS,
355
490
  url,
356
- fileName: fileBaseName(file),
491
+ fileName: fileBaseName(targetFile),
357
492
  headers
358
493
  });
359
494
  setPdfHeaders({});
@@ -373,7 +508,7 @@ function NoteEditor(props) {
373
508
  }
374
509
  setDownloading(true);
375
510
  try {
376
- const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(file)}`;
511
+ const localFile = `${RNFS.DocumentDirectoryPath}/${fileBaseName(targetFile)}`;
377
512
  await RNFS.downloadFile({
378
513
  fromUrl: url,
379
514
  toFile: localFile,
@@ -387,10 +522,23 @@ function NoteEditor(props) {
387
522
  } finally {
388
523
  setDownloading(false);
389
524
  }
390
- }, [FileViewer, ImageView, Pdf, RNFS, file, fileBaseUrl, resolveHeaders]);
525
+ }, [FileViewer, ImageView, Pdf, RNFS, activeFile, fileBaseUrl, fileList, resolveHeaders]);
391
526
  const submit = useCallback(async () => {
392
- if (!title || !description || !classOpt?.value || !sectionOpt?.value || !subjectOpt?.value || !file) {
393
- Alert.alert('Error', 'Please enter all the details');
527
+ const selectedClassId = classOpt?.value;
528
+ const selectedSectionId = sectionOpt?.value;
529
+ const selectedSubjectId = subjectOpt?.value;
530
+ const missing = [];
531
+ if (!title) missing.push('title');
532
+ if (!description) missing.push('description');
533
+ if (!selectedClassId) missing.push('class');
534
+ if (!selectedSectionId) missing.push('section');
535
+ if (!selectedSubjectId) missing.push('subject');
536
+ if (props.mode === 'edit' && !fileList.length) missing.push('file');
537
+ if (missing.length) {
538
+ if (props.mode === 'edit' && !fileList.length) {
539
+ setFileError('File is required');
540
+ }
541
+ Alert.alert('Error', `Please fill: ${missing.join(', ')}`);
394
542
  return;
395
543
  }
396
544
  const payload = {
@@ -398,13 +546,13 @@ function NoteEditor(props) {
398
546
  title,
399
547
  description,
400
548
  remark: description,
401
- class_id: classOpt.value,
402
- section_id: sectionOpt.value,
403
- sub_id: subjectOpt.value,
404
- file,
549
+ class_id: selectedClassId,
550
+ section_id: selectedSectionId,
551
+ sub_id: selectedSubjectId,
552
+ file: fileList.join(','),
405
553
  status
406
554
  };
407
- setSaving(true);
555
+ setIsSubmitting(true);
408
556
  try {
409
557
  const res = props.mode === 'create' ? await createNote(api, payload) : await updateNote(api, {
410
558
  ...payload,
@@ -420,13 +568,13 @@ function NoteEditor(props) {
420
568
  } catch (e) {
421
569
  Alert.alert('Error', String(e?.message ?? 'Could not save note'));
422
570
  } finally {
423
- setSaving(false);
571
+ setIsSubmitting(false);
424
572
  }
425
- }, [api, classOpt?.value, description, file, props, sectionOpt?.value, status, subjectOpt?.value, title]);
573
+ }, [api, classOpt?.value, description, fileList, props, sectionOpt?.value, status, subjectOpt?.value, title]);
426
574
  if (loading) {
427
575
  return /*#__PURE__*/_jsx(LoadingState, {});
428
576
  }
429
- const fileUrl = file ? resolveNoteUrl(fileBaseUrl, file) : '';
577
+ const fileUrl = activeFile ? resolveNoteUrl(fileBaseUrl, activeFile) : '';
430
578
  const headers = authToken || schoolCode ? {
431
579
  ...(authToken ? {
432
580
  Authorization: authToken
@@ -484,7 +632,11 @@ function NoteEditor(props) {
484
632
  value: classOpt,
485
633
  options: classes,
486
634
  placeholder: "Select class",
487
- onChange: setClassOpt
635
+ onChange: next => {
636
+ setClassOpt(next);
637
+ setSectionOpt(null);
638
+ setSubjectOpt(null);
639
+ }
488
640
  }), /*#__PURE__*/_jsx(SelectField, {
489
641
  label: "Section",
490
642
  value: sectionOpt,
@@ -530,24 +682,69 @@ function NoteEditor(props) {
530
682
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
531
683
  style: styles.secondaryBtn,
532
684
  onPress: () => pickDocument().catch(() => {}),
685
+ disabled: isUploading || isSubmitting,
533
686
  children: /*#__PURE__*/_jsx(Text, {
534
687
  style: styles.secondaryBtnText,
535
- children: file ? 'Change File' : 'Upload File'
688
+ children: fileList.length ? 'Add More Files' : 'Upload File'
536
689
  })
537
690
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
538
691
  style: styles.secondaryBtn,
539
692
  onPress: () => pickCamera().catch(() => {}),
693
+ disabled: isUploading || isSubmitting,
540
694
  children: /*#__PURE__*/_jsx(Text, {
541
695
  style: styles.secondaryBtnText,
542
696
  children: "Camera"
543
697
  })
544
698
  })]
545
- }), file ? /*#__PURE__*/_jsxs(Text, {
699
+ }), isUploading ? /*#__PURE__*/_jsx(StatusBanner, {
700
+ text: uploadStatus || 'Uploading file...',
701
+ busy: true,
702
+ tone: uploadTone
703
+ }) : uploadStatus ? /*#__PURE__*/_jsx(StatusBanner, {
704
+ text: uploadStatus,
705
+ tone: uploadTone
706
+ }) : null, fileList.length ? /*#__PURE__*/_jsxs(Text, {
546
707
  style: styles.fileText,
547
- children: ["Selected: ", fileBaseName(file)]
548
- }) : null, file ? isImageUrl(file) ? /*#__PURE__*/_jsx(TouchableOpacity, {
708
+ children: ["Selected: ", fileList.length, " file", fileList.length > 1 ? 's' : '']
709
+ }) : null, fileList.length ? /*#__PURE__*/_jsx(View, {
710
+ style: styles.attachmentList,
711
+ children: fileList.map((currentFile, index) => /*#__PURE__*/_jsxs(View, {
712
+ style: styles.attachmentRow,
713
+ children: [/*#__PURE__*/_jsx(Text, {
714
+ style: styles.attachmentName,
715
+ numberOfLines: 1,
716
+ children: fileBaseName(currentFile)
717
+ }), /*#__PURE__*/_jsxs(View, {
718
+ style: styles.attachmentActions,
719
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
720
+ style: styles.inlineActionBtn,
721
+ onPress: () => {
722
+ setActiveFile(currentFile);
723
+ viewAttachment(currentFile).catch(() => {});
724
+ },
725
+ children: /*#__PURE__*/_jsx(Text, {
726
+ style: styles.inlineActionText,
727
+ children: "View"
728
+ })
729
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
730
+ style: styles.inlineRemoveBtn,
731
+ onPress: () => {
732
+ setFileList(prev => {
733
+ const next = prev.filter((_, itemIndex) => itemIndex !== index);
734
+ setActiveFile(current => current === currentFile ? next[0] ?? '' : current);
735
+ return next;
736
+ });
737
+ },
738
+ children: /*#__PURE__*/_jsx(Text, {
739
+ style: styles.inlineRemoveText,
740
+ children: "Remove"
741
+ })
742
+ })]
743
+ })]
744
+ }, `${currentFile}-${index}`))
745
+ }) : null, activeFile ? isImageUrl(activeFile) || isImageUrl(fileUrl) ? /*#__PURE__*/_jsx(TouchableOpacity, {
549
746
  style: styles.previewWrap,
550
- onPress: () => setViewerOpen(true),
747
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
551
748
  activeOpacity: 0.9,
552
749
  children: /*#__PURE__*/_jsx(Image, {
553
750
  source: {
@@ -559,31 +756,37 @@ function NoteEditor(props) {
559
756
  })
560
757
  }) : /*#__PURE__*/_jsx(TouchableOpacity, {
561
758
  style: styles.documentBtn,
562
- onPress: () => viewAttachment().catch(() => {}),
759
+ onPress: () => viewAttachment(activeFile).catch(() => {}),
563
760
  disabled: downloading,
564
761
  children: /*#__PURE__*/_jsx(Text, {
565
762
  style: styles.documentBtnText,
566
763
  children: downloading ? 'Opening Document...' : 'View Document'
567
764
  })
765
+ }) : null, fileError ? /*#__PURE__*/_jsx(Text, {
766
+ style: styles.errorText,
767
+ children: fileError
568
768
  }) : null]
569
769
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
570
770
  style: styles.primaryBtn,
571
771
  onPress: () => submit().catch(() => {}),
572
- disabled: saving,
573
- children: saving ? /*#__PURE__*/_jsx(ActivityIndicator, {
574
- color: "#FFFFFF"
772
+ disabled: isUploading || isSubmitting,
773
+ children: isSubmitting ? /*#__PURE__*/_jsxs(View, {
774
+ style: styles.loadingRow,
775
+ children: [/*#__PURE__*/_jsx(ActivityIndicator, {
776
+ color: "#FFFFFF"
777
+ }), /*#__PURE__*/_jsx(Text, {
778
+ style: styles.primaryBtnText,
779
+ children: props.mode === 'create' ? 'Creating...' : 'Updating...'
780
+ })]
575
781
  }) : /*#__PURE__*/_jsx(Text, {
576
782
  style: styles.primaryBtnText,
577
783
  children: props.mode === 'create' ? 'Create Note' : 'Update Note'
578
784
  })
579
785
  })]
580
786
  })]
581
- }), ImageView && file && isImageUrl(file) ? /*#__PURE__*/_jsx(ImageView, {
582
- images: [{
583
- uri: fileUrl,
584
- headers
585
- }],
586
- imageIndex: 0,
787
+ }), ImageView && viewerImages.length ? /*#__PURE__*/_jsx(ImageView, {
788
+ images: viewerImages,
789
+ imageIndex: viewerIndex,
587
790
  visible: viewerOpen,
588
791
  onRequestClose: () => setViewerOpen(false)
589
792
  }) : null, /*#__PURE__*/_jsx(Modal, {
@@ -652,7 +855,10 @@ export function NotesScreen(props) {
652
855
  const [loadingMore, setLoadingMore] = useState(false);
653
856
  const [error, setError] = useState(null);
654
857
  const [selectedNote, setSelectedNote] = useState(null);
858
+ const [selectedActiveFile, setSelectedActiveFile] = useState('');
655
859
  const [viewerOpen, setViewerOpen] = useState(false);
860
+ const [viewerIndex, setViewerIndex] = useState(0);
861
+ const [viewerImages, setViewerImages] = useState([]);
656
862
  const [pdfVisible, setPdfVisible] = useState(false);
657
863
  const [pdfUri, setPdfUri] = useState('');
658
864
  const [pdfHeaders, setPdfHeaders] = useState({});
@@ -734,15 +940,26 @@ export function NotesScreen(props) {
734
940
  if (mode !== 'list') return;
735
941
  loadList(props.page ?? 1, true).catch(() => {});
736
942
  }, [loadList, mode, props.page]);
737
- const viewNoteAttachment = useCallback(async () => {
738
- const file = String(selectedNote?.file ?? '');
943
+ const selectedNoteFiles = useMemo(() => splitFileCsv(selectedNote?.file ?? ''), [selectedNote?.file]);
944
+ const viewNoteAttachment = useCallback(async selectedFile => {
945
+ const file = String(selectedFile ?? selectedActiveFile ?? selectedNoteFiles[0] ?? '');
739
946
  if (!file) {
740
947
  Alert.alert('No Attachment');
741
948
  return;
742
949
  }
743
950
  const url = resolveNoteUrl(fileBaseUrl, file);
744
951
  const headers = await resolveHeaders();
745
- if (ImageView && isImageUrl(file)) {
952
+ if (ImageView && (isImageUrl(file) || isImageUrl(url))) {
953
+ const imageFiles = selectedNoteFiles.filter(currentFile => {
954
+ const currentUrl = resolveNoteUrl(fileBaseUrl, currentFile);
955
+ return isImageUrl(currentFile) || isImageUrl(currentUrl);
956
+ });
957
+ const nextIndex = Math.max(0, imageFiles.findIndex(currentFile => String(currentFile) === String(file)));
958
+ setViewerImages(imageFiles.map(currentFile => ({
959
+ uri: resolveNoteUrl(fileBaseUrl, currentFile),
960
+ headers: Object.keys(headers).length ? headers : undefined
961
+ })));
962
+ setViewerIndex(nextIndex);
746
963
  setViewerOpen(true);
747
964
  return;
748
965
  }
@@ -794,7 +1011,7 @@ export function NotesScreen(props) {
794
1011
  } finally {
795
1012
  setDownloading(false);
796
1013
  }
797
- }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedNote?.file]);
1014
+ }, [FileViewer, ImageView, Pdf, RNFS, fileBaseUrl, resolveHeaders, selectedActiveFile, selectedNoteFiles]);
798
1015
  if (mode === 'create') {
799
1016
  return /*#__PURE__*/_jsx(NoteEditor, {
800
1017
  mode: "create",
@@ -919,7 +1136,10 @@ export function NotesScreen(props) {
919
1136
  style: styles.row,
920
1137
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
921
1138
  style: styles.secondaryBtn,
922
- onPress: () => setSelectedNote(item),
1139
+ onPress: () => {
1140
+ setSelectedNote(item);
1141
+ setSelectedActiveFile(splitFileCsv(item?.file ?? '')[0] ?? '');
1142
+ },
923
1143
  children: /*#__PURE__*/_jsx(Text, {
924
1144
  style: styles.secondaryBtnText,
925
1145
  children: "View"
@@ -982,42 +1202,65 @@ export function NotesScreen(props) {
982
1202
  }), /*#__PURE__*/_jsx(Text, {
983
1203
  style: styles.noteDescFull,
984
1204
  children: String(selectedNote?.description ?? selectedNote?.remark ?? '-')
985
- }), selectedNote?.file ? isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/_jsx(TouchableOpacity, {
986
- style: styles.previewWrap,
987
- onPress: () => setViewerOpen(true),
988
- activeOpacity: 0.9,
989
- children: /*#__PURE__*/_jsx(Image, {
990
- source: {
991
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file)),
992
- headers: authToken || schoolCode ? {
993
- ...(authToken ? {
994
- Authorization: authToken
995
- } : {}),
996
- ...(schoolCode ? {
997
- school_code: schoolCode
998
- } : {})
999
- } : undefined
1000
- },
1001
- style: styles.previewImage,
1002
- resizeMode: "contain"
1003
- })
1004
- }) : /*#__PURE__*/_jsx(TouchableOpacity, {
1005
- style: styles.documentBtn,
1006
- onPress: () => viewNoteAttachment().catch(() => {}),
1007
- disabled: downloading,
1008
- children: /*#__PURE__*/_jsx(Text, {
1009
- style: styles.documentBtnText,
1010
- children: downloading ? 'Opening Document...' : 'View Document'
1011
- })
1205
+ }), selectedNoteFiles.length ? /*#__PURE__*/_jsxs(_Fragment, {
1206
+ children: [/*#__PURE__*/_jsxs(Text, {
1207
+ style: styles.fileText,
1208
+ children: ["Selected: ", selectedNoteFiles.length, " file", selectedNoteFiles.length > 1 ? 's' : '']
1209
+ }), /*#__PURE__*/_jsx(View, {
1210
+ style: styles.attachmentList,
1211
+ children: selectedNoteFiles.map((currentFile, index) => /*#__PURE__*/_jsxs(View, {
1212
+ style: styles.attachmentRow,
1213
+ children: [/*#__PURE__*/_jsx(Text, {
1214
+ style: styles.attachmentName,
1215
+ numberOfLines: 1,
1216
+ children: fileBaseName(currentFile)
1217
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
1218
+ style: styles.inlineActionBtn,
1219
+ onPress: () => {
1220
+ setSelectedActiveFile(currentFile);
1221
+ viewNoteAttachment(currentFile).catch(() => {});
1222
+ },
1223
+ children: /*#__PURE__*/_jsx(Text, {
1224
+ style: styles.inlineActionText,
1225
+ children: "View"
1226
+ })
1227
+ })]
1228
+ }, `${currentFile}-${index}`))
1229
+ }), selectedActiveFile ? isImageUrl(selectedActiveFile) || isImageUrl(resolveNoteUrl(fileBaseUrl, selectedActiveFile)) ? /*#__PURE__*/_jsx(TouchableOpacity, {
1230
+ style: styles.previewWrap,
1231
+ onPress: () => viewNoteAttachment(selectedActiveFile).catch(() => {}),
1232
+ activeOpacity: 0.9,
1233
+ children: /*#__PURE__*/_jsx(Image, {
1234
+ source: {
1235
+ uri: resolveNoteUrl(fileBaseUrl, selectedActiveFile),
1236
+ headers: authToken || schoolCode ? {
1237
+ ...(authToken ? {
1238
+ Authorization: authToken
1239
+ } : {}),
1240
+ ...(schoolCode ? {
1241
+ school_code: schoolCode
1242
+ } : {})
1243
+ } : undefined
1244
+ },
1245
+ style: styles.previewImage,
1246
+ resizeMode: "contain"
1247
+ })
1248
+ }) : /*#__PURE__*/_jsx(TouchableOpacity, {
1249
+ style: styles.documentBtn,
1250
+ onPress: () => viewNoteAttachment(selectedActiveFile).catch(() => {}),
1251
+ disabled: downloading,
1252
+ children: /*#__PURE__*/_jsx(Text, {
1253
+ style: styles.documentBtnText,
1254
+ children: downloading ? 'Opening Document...' : 'View Document'
1255
+ })
1256
+ }) : null]
1012
1257
  }) : null]
1013
1258
  }) : null]
1014
1259
  })
1015
1260
  })
1016
- }), ImageView && selectedNote?.file && isImageUrl(String(selectedNote.file)) ? /*#__PURE__*/_jsx(ImageView, {
1017
- images: [{
1018
- uri: resolveNoteUrl(fileBaseUrl, String(selectedNote.file))
1019
- }],
1020
- imageIndex: 0,
1261
+ }), ImageView && viewerImages.length ? /*#__PURE__*/_jsx(ImageView, {
1262
+ images: viewerImages,
1263
+ imageIndex: viewerIndex,
1021
1264
  visible: viewerOpen,
1022
1265
  onRequestClose: () => setViewerOpen(false)
1023
1266
  }) : null, /*#__PURE__*/_jsx(Modal, {
@@ -1189,6 +1432,51 @@ const styles = StyleSheet.create({
1189
1432
  color: '#374151',
1190
1433
  marginTop: 8
1191
1434
  },
1435
+ attachmentList: {
1436
+ width: '100%',
1437
+ marginTop: 8
1438
+ },
1439
+ attachmentRow: {
1440
+ flexDirection: 'row',
1441
+ alignItems: 'center',
1442
+ justifyContent: 'space-between',
1443
+ paddingVertical: 8,
1444
+ borderBottomWidth: 1,
1445
+ borderBottomColor: '#E5E7EB',
1446
+ gap: 10
1447
+ },
1448
+ attachmentName: {
1449
+ flex: 1,
1450
+ color: '#111827',
1451
+ fontSize: 13
1452
+ },
1453
+ attachmentActions: {
1454
+ flexDirection: 'row',
1455
+ alignItems: 'center',
1456
+ gap: 8
1457
+ },
1458
+ inlineActionBtn: {
1459
+ paddingHorizontal: 10,
1460
+ paddingVertical: 6,
1461
+ borderRadius: 8,
1462
+ backgroundColor: '#DBEAFE'
1463
+ },
1464
+ inlineActionText: {
1465
+ color: '#1D4ED8',
1466
+ fontWeight: '700',
1467
+ fontSize: 12
1468
+ },
1469
+ inlineRemoveBtn: {
1470
+ paddingHorizontal: 10,
1471
+ paddingVertical: 6,
1472
+ borderRadius: 8,
1473
+ backgroundColor: '#FEE2E2'
1474
+ },
1475
+ inlineRemoveText: {
1476
+ color: '#B91C1C',
1477
+ fontWeight: '700',
1478
+ fontSize: 12
1479
+ },
1192
1480
  previewWrap: {
1193
1481
  marginTop: 12,
1194
1482
  borderWidth: 1,
@@ -1277,5 +1565,31 @@ const styles = StyleSheet.create({
1277
1565
  color: '#6B7280',
1278
1566
  textAlign: 'center',
1279
1567
  paddingVertical: 16
1568
+ },
1569
+ statusBanner: {
1570
+ marginTop: 10,
1571
+ paddingHorizontal: 12,
1572
+ paddingVertical: 10,
1573
+ borderRadius: 12,
1574
+ borderWidth: 1,
1575
+ flexDirection: 'row',
1576
+ alignItems: 'center',
1577
+ gap: 10
1578
+ },
1579
+ statusBannerText: {
1580
+ flex: 1,
1581
+ fontSize: 12,
1582
+ fontWeight: '700'
1583
+ },
1584
+ errorText: {
1585
+ marginTop: 6,
1586
+ fontSize: 12,
1587
+ color: '#DC2626',
1588
+ fontWeight: '600'
1589
+ },
1590
+ loadingRow: {
1591
+ flexDirection: 'row',
1592
+ alignItems: 'center',
1593
+ gap: 8
1280
1594
  }
1281
1595
  });