@striae-org/striae 5.4.5 → 5.5.1

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.
@@ -1,6 +1,8 @@
1
- import { useContext, useMemo } from 'react';
1
+ import { useCallback, useContext, useMemo, useState } from 'react';
2
2
  import { AuthContext } from '~/contexts/auth.context';
3
3
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
+ import { exportAuditPDF } from '~/components/actions/export-audit-pdf';
5
+ import { Toast, type ToastType } from '~/components/toast/toast';
4
6
  import { AuditViewerHeader } from './viewer/audit-viewer-header';
5
7
  import { AuditUserInfoCard } from './viewer/audit-user-info-card';
6
8
  import { AuditActivitySummary } from './viewer/audit-activity-summary';
@@ -20,6 +22,12 @@ interface UserAuditViewerProps {
20
22
 
21
23
  export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAuditViewerProps) => {
22
24
  const { user } = useContext(AuthContext);
25
+ const [isExportingPdf, setIsExportingPdf] = useState(false);
26
+ const [showToast, setShowToast] = useState(false);
27
+ const [toastType, setToastType] = useState<ToastType>('success');
28
+ const [toastMessage, setToastMessage] = useState('');
29
+ const [toastDuration, setToastDuration] = useState(4000);
30
+
23
31
  const {
24
32
  filterAction,
25
33
  setFilterAction,
@@ -79,107 +87,157 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
79
87
  });
80
88
 
81
89
  const userBadgeId = userData?.badgeId?.trim() || '';
90
+ const isCaseScopedViewer = Boolean(effectiveCaseNumber?.trim());
91
+
92
+ const handleExportPdf = useCallback(async () => {
93
+ if (!user || !effectiveCaseNumber || isExportingPdf) {
94
+ return;
95
+ }
96
+
97
+ const displayName = user.displayName?.trim() || '';
98
+ const [firstFromDisplayName, ...lastFromDisplayNameParts] = displayName.split(/\s+/).filter(Boolean);
99
+
100
+ await exportAuditPDF({
101
+ user,
102
+ caseNumber: effectiveCaseNumber,
103
+ userCompany: userData?.company,
104
+ userFirstName: firstFromDisplayName || userData?.firstName || '',
105
+ userLastName: lastFromDisplayNameParts.join(' ') || userData?.lastName || '',
106
+ userBadgeId,
107
+ setIsExportingPDF: setIsExportingPdf,
108
+ setToastType,
109
+ setToastMessage,
110
+ setShowToast,
111
+ setToastDuration
112
+ });
113
+ }, [
114
+ effectiveCaseNumber,
115
+ isExportingPdf,
116
+ user,
117
+ userBadgeId,
118
+ userData?.company,
119
+ userData?.firstName,
120
+ userData?.lastName
121
+ ]);
82
122
 
83
123
  if (!isOpen) return null;
84
124
 
85
125
  return (
86
- <div
87
- className={styles.overlay}
88
- aria-label="Close audit trail dialog"
89
- {...overlayProps}
90
- >
91
- <div className={styles.modal}>
92
- <AuditViewerHeader
93
- title={title || (effectiveCaseNumber ? `Audit Trail - Case ${effectiveCaseNumber}` : 'My Audit Trail')}
94
- onClose={requestClose}
95
- />
96
-
97
- <div className={styles.content}>
98
- {loading && (
99
- <div className={styles.loading}>
100
- <div className={styles.spinner}></div>
101
- <p>Loading your audit trail...this may take a while for longer time ranges</p>
102
- </div>
103
- )}
104
-
105
- {error && (
106
- <div className={styles.error}>
107
- <p>Error: {error}</p>
108
- <button onClick={loadAuditData} className={styles.retryButton}>
109
- Retry
110
- </button>
111
- </div>
112
- )}
113
-
114
- {!loading && !error && (
115
- <>
116
- {isArchivedReadOnlyCase && (
117
- <div className={bundledAuditWarning ? styles.archivedWarning : styles.archivedNotice}>
118
- <p>
119
- {bundledAuditWarning || 'Viewing bundled audit trail data from this imported archived case package.'}
120
- </p>
121
- </div>
122
- )}
123
-
124
- {/* User Information Section */}
125
- {user && (
126
- <AuditUserInfoCard user={user} userData={userData} userBadgeId={userBadgeId} />
127
- )}
128
-
129
- {/* Summary Section */}
130
- <AuditActivitySummary
131
- caseNumber={caseNumber}
132
- filterCaseNumber={filterCaseNumber}
133
- dateRangeDisplay={dateRangeDisplay}
134
- summary={auditSummary}
135
- />
136
-
137
- {/* Filters */}
138
- <AuditFiltersPanel
139
- dateRange={dateRange}
140
- customStartDate={customStartDate}
141
- customEndDate={customEndDate}
142
- customStartDateInput={customStartDateInput}
143
- customEndDateInput={customEndDateInput}
144
- caseNumber={caseNumber}
145
- filterCaseNumber={filterCaseNumber}
146
- caseNumberInput={caseNumberInput}
147
- filterBadgeId={filterBadgeId}
148
- badgeIdInput={badgeIdInput}
149
- filterAction={filterAction}
150
- filterResult={filterResult}
151
- onDateRangeChange={handleDateRangeChange}
152
- onCustomStartDateInputChange={setCustomStartDateInput}
153
- onCustomEndDateInputChange={setCustomEndDateInput}
154
- onApplyCustomDateRange={handleApplyCustomDateRange}
155
- onClearCustomDateRange={handleClearCustomDateRange}
156
- onCaseNumberInputChange={setCaseNumberInput}
157
- onApplyCaseFilter={handleApplyCaseFilter}
158
- onClearCaseFilter={handleClearCaseFilter}
159
- onBadgeIdInputChange={setBadgeIdInput}
160
- onApplyBadgeFilter={handleApplyBadgeFilter}
161
- onClearBadgeFilter={handleClearBadgeFilter}
162
- onFilterActionChange={setFilterAction}
163
- onFilterResultChange={setFilterResult}
164
- />
165
-
166
- {/* Entries List */}
167
- <AuditEntriesList entries={filteredEntries} />
168
-
169
- </>
170
- )}
171
-
172
- {auditEntries.length === 0 && !loading && !error && (
173
- <div className={styles.noData}>
174
- <p>
175
- {isArchivedReadOnlyCase
176
- ? 'No bundled audit trail entries are available for this imported archived case.'
177
- : 'No audit trail available. Your activities will appear here as you use Striae.'}
178
- </p>
179
- </div>
180
- )}
126
+ <>
127
+ <Toast
128
+ message={toastMessage}
129
+ type={toastType}
130
+ isVisible={showToast}
131
+ onClose={() => setShowToast(false)}
132
+ duration={toastDuration}
133
+ />
134
+ <div
135
+ className={styles.overlay}
136
+ aria-label="Close audit trail dialog"
137
+ {...overlayProps}
138
+ >
139
+ <div className={styles.modal}>
140
+ <AuditViewerHeader
141
+ title={title || (effectiveCaseNumber ? `Audit Trail - Case ${effectiveCaseNumber}` : 'My Audit Trail')}
142
+ onClose={requestClose}
143
+ onExportPdf={isCaseScopedViewer ? () => void handleExportPdf() : undefined}
144
+ canExportPdf={isCaseScopedViewer && auditEntries.length > 0 && !loading && !error}
145
+ isExportingPdf={isExportingPdf}
146
+ />
147
+
148
+ <div className={styles.content}>
149
+ {loading && (
150
+ <div className={styles.loading}>
151
+ <div className={styles.spinner}></div>
152
+ <p>Loading your audit trail...this may take a while for longer time ranges</p>
153
+ </div>
154
+ )}
155
+
156
+ {error && (
157
+ <div className={styles.error}>
158
+ <p>Error: {error}</p>
159
+ <button onClick={loadAuditData} className={styles.retryButton}>
160
+ Retry
161
+ </button>
162
+ </div>
163
+ )}
164
+
165
+ {!loading && !error && (
166
+ <>
167
+ {isArchivedReadOnlyCase && (
168
+ <div className={bundledAuditWarning ? styles.archivedWarning : styles.archivedNotice}>
169
+ <p>
170
+ {bundledAuditWarning || 'Viewing bundled audit trail data from this imported archived case package.'}
171
+ </p>
172
+ </div>
173
+ )}
174
+
175
+ {/* User Information Section */}
176
+ {user && (
177
+ <AuditUserInfoCard user={user} userData={userData} userBadgeId={userBadgeId} />
178
+ )}
179
+
180
+ {/* Summary Section */}
181
+ <AuditActivitySummary
182
+ caseNumber={caseNumber}
183
+ filterCaseNumber={filterCaseNumber}
184
+ dateRangeDisplay={dateRangeDisplay}
185
+ summary={auditSummary}
186
+ />
187
+
188
+ {isCaseScopedViewer && (
189
+ <div className={styles.exportScopeNote}>
190
+ Export PDF always includes full case history from case creation through now, regardless of current filters.
191
+ </div>
192
+ )}
193
+
194
+ {/* Filters */}
195
+ <AuditFiltersPanel
196
+ dateRange={dateRange}
197
+ customStartDate={customStartDate}
198
+ customEndDate={customEndDate}
199
+ customStartDateInput={customStartDateInput}
200
+ customEndDateInput={customEndDateInput}
201
+ caseNumber={caseNumber}
202
+ filterCaseNumber={filterCaseNumber}
203
+ caseNumberInput={caseNumberInput}
204
+ filterBadgeId={filterBadgeId}
205
+ badgeIdInput={badgeIdInput}
206
+ filterAction={filterAction}
207
+ filterResult={filterResult}
208
+ onDateRangeChange={handleDateRangeChange}
209
+ onCustomStartDateInputChange={setCustomStartDateInput}
210
+ onCustomEndDateInputChange={setCustomEndDateInput}
211
+ onApplyCustomDateRange={handleApplyCustomDateRange}
212
+ onClearCustomDateRange={handleClearCustomDateRange}
213
+ onCaseNumberInputChange={setCaseNumberInput}
214
+ onApplyCaseFilter={handleApplyCaseFilter}
215
+ onClearCaseFilter={handleClearCaseFilter}
216
+ onBadgeIdInputChange={setBadgeIdInput}
217
+ onApplyBadgeFilter={handleApplyBadgeFilter}
218
+ onClearBadgeFilter={handleClearBadgeFilter}
219
+ onFilterActionChange={setFilterAction}
220
+ onFilterResultChange={setFilterResult}
221
+ />
222
+
223
+ {/* Entries List */}
224
+ <AuditEntriesList entries={filteredEntries} />
225
+
226
+ </>
227
+ )}
228
+
229
+ {auditEntries.length === 0 && !loading && !error && (
230
+ <div className={styles.noData}>
231
+ <p>
232
+ {isArchivedReadOnlyCase
233
+ ? 'No bundled audit trail entries are available for this imported archived case.'
234
+ : 'No audit trail available. Your activities will appear here as you use Striae.'}
235
+ </p>
236
+ </div>
237
+ )}
238
+ </div>
181
239
  </div>
182
240
  </div>
183
- </div>
241
+ </>
184
242
  );
185
243
  };
@@ -72,6 +72,12 @@
72
72
  transform: translateY(0);
73
73
  }
74
74
 
75
+ .exportButton:disabled {
76
+ opacity: 0.65;
77
+ cursor: not-allowed;
78
+ box-shadow: none;
79
+ }
80
+
75
81
  .title {
76
82
  margin: 0;
77
83
  color: var(--textTitle);
@@ -178,6 +184,17 @@
178
184
  border: 1px solid color-mix(in lab, var(--textLight) 20%, transparent);
179
185
  }
180
186
 
187
+ .exportScopeNote {
188
+ margin: -12px 0 16px;
189
+ padding: 10px 12px;
190
+ border-radius: 6px;
191
+ border: 1px solid color-mix(in lab, var(--primary) 30%, transparent);
192
+ background: color-mix(in lab, var(--primary) 10%, transparent);
193
+ color: var(--textBody);
194
+ font-size: 0.85rem;
195
+ line-height: 1.45;
196
+ }
197
+
181
198
  /* Common heading styles */
182
199
  .summary h3,
183
200
  .entriesList h3 {
@@ -3,16 +3,32 @@ import styles from '../user-audit.module.css';
3
3
  interface AuditViewerHeaderProps {
4
4
  title: string;
5
5
  onClose: () => void;
6
+ onExportPdf?: () => void;
7
+ canExportPdf?: boolean;
8
+ isExportingPdf?: boolean;
6
9
  }
7
10
 
8
11
  export const AuditViewerHeader = ({
9
12
  title,
10
13
  onClose,
14
+ onExportPdf,
15
+ canExportPdf = false,
16
+ isExportingPdf = false,
11
17
  }: AuditViewerHeaderProps) => {
12
18
  return (
13
19
  <div className={styles.header}>
14
20
  <h2 className={styles.title}>{title}</h2>
15
21
  <div className={styles.headerActions}>
22
+ {onExportPdf && (
23
+ <button
24
+ type="button"
25
+ className={styles.exportButton}
26
+ onClick={onExportPdf}
27
+ disabled={!canExportPdf || isExportingPdf}
28
+ >
29
+ {isExportingPdf ? 'Exporting PDF...' : 'Export PDF'}
30
+ </button>
31
+ )}
16
32
  <button className={styles.closeButton} onClick={onClose}>
17
33
  ×
18
34
  </button>
@@ -118,10 +118,10 @@ const ClassDetailsModalContent = ({
118
118
  />
119
119
  )}
120
120
  </div>
121
- <div className={styles.modalButtons}>
121
+ <div className={`${styles.modalButtons} ${styles.classDetailsModalButtons}`}>
122
122
  <button
123
123
  onClick={handleSave}
124
- className={styles.saveButton}
124
+ className={`${styles.saveButton} ${styles.classDetailsModalAction}`}
125
125
  disabled={isSaving || isReadOnly}
126
126
  aria-busy={isSaving}
127
127
  >
@@ -129,7 +129,7 @@ const ClassDetailsModalContent = ({
129
129
  </button>
130
130
  <button
131
131
  onClick={requestClose}
132
- className={styles.cancelButton}
132
+ className={`${styles.cancelButton} ${styles.classDetailsModalAction}`}
133
133
  disabled={isSaving}
134
134
  >
135
135
  Cancel