@appcorp/fusion-storybook 0.4.10 → 0.4.12

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.
@@ -34,7 +34,7 @@ import { getCachedWorkspaceSync } from "../workspace/cache";
34
34
  export const formatStudentName = (_, row) => {
35
35
  var _a, _b, _c;
36
36
  const fee = row;
37
- const member = (_b = (_a = fee === null || fee === void 0 ? void 0 : fee.family) === null || _a === void 0 ? void 0 : _a.members) === null || _b === void 0 ? void 0 : _b.find(({ id }) => fee.studentProfileId === id);
37
+ const member = (_b = (_a = fee === null || fee === void 0 ? void 0 : fee.family) === null || _a === void 0 ? void 0 : _a.members) === null || _b === void 0 ? void 0 : _b.find(({ studentProfile }) => fee.studentProfileId === (studentProfile === null || studentProfile === void 0 ? void 0 : studentProfile.id));
38
38
  return member
39
39
  ? `${member.firstName} ${member.lastName} - ${(_c = member.studentProfile) === null || _c === void 0 ? void 0 : _c.computerNumber.split(":")[1]}`
40
40
  : "N/A";
@@ -1,62 +1,32 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { downloadFromUrl } from "@react-pakistan/util-functions/general/download-from-url";
4
3
  import { showErrorToast, showSuccessToast, showInfoToast, } from "@appcorp/shadcn/lib/toast-utils";
5
4
  import { getCachedWorkspaceSync } from "../workspace/cache";
6
- import converter from "json-2-csv";
7
- import { Timeline } from "../../components/timeline";
8
5
  import { useTranslations } from "next-intl";
9
6
  import { STUDENT_FEE_API_ROUTES } from "../../constants";
10
7
  import { STUDENT_FEE_ACTION_TYPES, useStudentFeeModule } from "./context";
11
- import { useRef, useEffect, useCallback } from "react";
8
+ import { useRef, useEffect, useCallback, useState } from "react";
12
9
  import { Button } from "@appcorp/shadcn/components/ui/button";
13
10
  import { Separator } from "@appcorp/shadcn/components/ui/separator";
11
+ import { EnhancedInput } from "@appcorp/shadcn/components/enhanced-input";
12
+ import { EnhancedTextarea } from "@appcorp/shadcn/components/enhanced-textarea";
14
13
  const workspace = getCachedWorkspaceSync();
15
14
  const POLL_INTERVAL_MS = 2000;
16
15
  const POLL_TIMEOUT_MS = 300000;
17
16
  const PAGE_LIMIT = 5000;
18
- const handleGetAllRecords = async (schoolId, pageLimit) => {
19
- const response = await fetch(`${STUDENT_FEE_API_ROUTES.UNIT}?pageLimit=${pageLimit}&currentPage=1&schoolId=${schoolId}&studentComputerNumber=true&status=PENDING`);
20
- const result = await response.json();
21
- const csv = await converter.json2csv(result.items || [], {});
22
- const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
23
- await downloadFromUrl(blob, "student-fees.csv");
24
- };
25
- async function submitBulkJob(csvData, method, signal) {
26
- var _a;
27
- const schoolId = ((_a = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _a === void 0 ? void 0 : _a.id) || "";
28
- if (!schoolId)
29
- throw new Error("School ID not found");
30
- const res = await fetch(STUDENT_FEE_API_ROUTES.BULK, {
31
- method,
32
- headers: {
33
- "Content-Type": "application/json",
34
- },
35
- body: JSON.stringify({ schoolId, csvData }),
36
- signal,
37
- });
38
- if (!res.ok) {
39
- const errorData = (await res.json().catch(() => ({})));
40
- throw new Error(errorData.error || `Bulk operation failed with status ${res.status}`);
41
- }
42
- const data = (await res.json());
43
- return data.jobId;
44
- }
45
- async function pollBulkJob(jobId, signal, onProgress) {
17
+ async function pollJob(statusUrl, signal, onProgress) {
46
18
  const startTime = Date.now();
47
19
  let lastProgressToast = 0;
48
20
  while (true) {
49
21
  if (signal.aborted)
50
22
  throw new Error("Polling cancelled");
51
23
  if (Date.now() - startTime > POLL_TIMEOUT_MS) {
52
- throw new Error("Bulk operation timed out");
24
+ throw new Error("Operation timed out");
53
25
  }
54
26
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
55
27
  if (signal.aborted)
56
28
  throw new Error("Polling cancelled");
57
- const res = await fetch(STUDENT_FEE_API_ROUTES.BULK_STATUS(jobId), {
58
- signal,
59
- });
29
+ const res = await fetch(statusUrl, { signal });
60
30
  if (!res.ok)
61
31
  continue;
62
32
  const data = (await res.json());
@@ -76,127 +46,127 @@ t, errors) {
76
46
  return "";
77
47
  const lines = errors
78
48
  .slice(0, 5)
79
- .map((e) => t("messagesBulkRowError", { row: e.row, error: e.error }));
49
+ .map((e) => t("messagesAutoPostRowError", {
50
+ computerNumber: e.computerNumber,
51
+ error: e.error,
52
+ }));
80
53
  const remaining = errors.length - 5;
81
54
  if (remaining > 0)
82
- lines.push(t("messagesBulkMoreRows", { count: remaining }));
55
+ lines.push(t("messagesAutoPostMoreRows", { count: remaining }));
83
56
  return lines.join("\n");
84
57
  }
85
58
  export const StudentFeeMoreActions = () => {
86
59
  const t = useTranslations("studentFee");
87
60
  const { dispatch, handleCreateMonthly, handleCreateSingleEntry } = useStudentFeeModule();
88
61
  const abortRef = useRef(null);
62
+ const [autoPostDate, setAutoPostDate] = useState("");
63
+ const [computerNumbersText, setComputerNumbersText] = useState("");
64
+ const [submitting, setSubmitting] = useState(false);
89
65
  useEffect(() => {
90
66
  return () => {
91
67
  var _a;
92
68
  (_a = abortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
93
69
  };
94
70
  }, []);
95
- const handleBulkFlow = useCallback(async (files, method) => {
96
- var _a, _b, _c, _d;
97
- const closeDrawer = () => {
71
+ const closeDrawer = useCallback(() => {
72
+ dispatch({
73
+ type: STUDENT_FEE_ACTION_TYPES.SET_DRAWER,
74
+ payload: { drawer: null },
75
+ });
76
+ }, [dispatch]);
77
+ const refreshList = useCallback(() => {
78
+ var _a;
79
+ const schoolId = ((_a = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _a === void 0 ? void 0 : _a.id) || "";
80
+ fetch(`${STUDENT_FEE_API_ROUTES.UNIT}?currentPage=1&pageLimit=${PAGE_LIMIT}&schoolId=${schoolId}`, {
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ },
84
+ })
85
+ .then(async (res) => {
86
+ var _a, _b;
87
+ if (!res.ok)
88
+ return;
89
+ const data = await res.json();
98
90
  dispatch({
99
- type: STUDENT_FEE_ACTION_TYPES.SET_DRAWER,
100
- payload: { drawer: null },
91
+ type: STUDENT_FEE_ACTION_TYPES.SET_ITEMS,
92
+ payload: {
93
+ items: (_a = data.items) !== null && _a !== void 0 ? _a : [],
94
+ count: (_b = data.count) !== null && _b !== void 0 ? _b : 0,
95
+ },
101
96
  });
102
- };
103
- (_a = abortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
104
- const controller = new AbortController();
105
- abortRef.current = controller;
106
- const { signal } = controller;
107
- const label = method === "POST" ? "create" : "update";
108
- const file = files[0];
109
- const text = await file.text();
110
- const records = converter.csv2json(text);
111
- if (!Array.isArray(records) || records.length === 0) {
112
- showErrorToast(t("messagesBulkCsvEmpty"));
97
+ })
98
+ .catch(() => { });
99
+ }, [dispatch]);
100
+ const handleAutoPost = useCallback(async () => {
101
+ var _a, _b, _c, _d;
102
+ const schoolId = ((_a = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _a === void 0 ? void 0 : _a.id) || "";
103
+ const numbers = computerNumbersText
104
+ .split(",")
105
+ .map((n) => n.trim())
106
+ .filter(Boolean);
107
+ const uniqueNumbers = [...new Set(numbers)];
108
+ if (!autoPostDate) {
109
+ showErrorToast(t("validationAutoPostDateRequired"));
113
110
  return;
114
111
  }
115
- const isEmpty = (v) => v === null || v === undefined || String(v).trim() === "";
116
- const validationErrors = [];
117
- for (let i = 0; i < records.length; i++) {
118
- const row = records[i];
119
- const msgs = [];
120
- if (isEmpty(row.id))
121
- msgs.push(t("validationRequiredIdForUpdate"));
122
- if (msgs.length > 0) {
123
- validationErrors.push({ row: i + 1, messages: msgs });
124
- }
125
- }
126
- if (validationErrors.length > 0) {
127
- const errorsList = validationErrors
128
- .slice(0, 5)
129
- .map((e) => t("messagesBulkRowError", {
130
- row: e.row,
131
- error: e.messages.join("; "),
132
- }))
133
- .join("\n");
134
- const remaining = validationErrors.length - 5;
135
- const errorsStr = remaining > 0
136
- ? `${errorsList}\n${t("messagesBulkMoreRows", { count: remaining })}`
137
- : errorsList;
138
- showErrorToast(t("messagesBulkValidationFailed", {
139
- count: validationErrors.length,
140
- errors: errorsStr,
141
- }));
112
+ if (uniqueNumbers.length === 0) {
113
+ showErrorToast(t("validationAutoPostComputerNumbersRequired"));
142
114
  return;
143
115
  }
116
+ setSubmitting(true);
117
+ (_b = abortRef.current) === null || _b === void 0 ? void 0 : _b.abort();
118
+ const controller = new AbortController();
119
+ abortRef.current = controller;
120
+ const { signal } = controller;
144
121
  try {
145
- showInfoToast(t("messagesBulkJobQueued", { action: label, count: records.length }));
122
+ showInfoToast(t("messagesAutoPostJobQueued", { count: uniqueNumbers.length }));
146
123
  let jobId;
147
124
  try {
148
- jobId = await submitBulkJob(text, method, signal);
125
+ const res = await fetch(STUDENT_FEE_API_ROUTES.AUTO_POST, {
126
+ method: "POST",
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ },
130
+ body: JSON.stringify({
131
+ schoolId,
132
+ computerNumbers: uniqueNumbers,
133
+ paidAt: autoPostDate,
134
+ }),
135
+ signal,
136
+ });
137
+ if (!res.ok) {
138
+ const errorData = (await res.json().catch(() => ({})));
139
+ throw new Error(errorData.error || `Auto-post failed with status ${res.status}`);
140
+ }
141
+ const data = (await res.json());
142
+ jobId = data.jobId;
149
143
  }
150
144
  catch (submitError) {
151
- showErrorToast(t("messagesBulkJobSubmitFailed", {
152
- action: label,
145
+ showErrorToast(t("messagesAutoPostJobSubmitFailed", {
153
146
  error: submitError instanceof Error
154
147
  ? submitError.message
155
148
  : t("unknownError"),
156
149
  }));
157
150
  return;
158
151
  }
159
- const status = await pollBulkJob(jobId, signal, (processed, total) => {
160
- showInfoToast(t("messagesBulkProgress", { processed, total }));
152
+ const status = await pollJob(STUDENT_FEE_API_ROUTES.AUTO_POST_STATUS(jobId), signal, (processed, total) => {
153
+ showInfoToast(t("messagesAutoPostProgress", { processed, total }));
161
154
  });
162
155
  if (signal.aborted)
163
156
  return;
164
157
  if (status.status === "completed") {
165
158
  const r = status.results;
166
- if (r && ((_b = r.errors) === null || _b === void 0 ? void 0 : _b.length) > 0) {
159
+ if (r && ((_c = r.errors) === null || _c === void 0 ? void 0 : _c.length) > 0) {
167
160
  const summary = formatErrorSummary(t, r.errors);
168
- showSuccessToast(`${t("messagesBulkResults", { created: r.created, updated: r.updated, skipped: r.skipped })}\n${summary}`);
161
+ showSuccessToast(`${t("messagesAutoPostResults", { paid: r.paid, skipped: r.skipped })}\n${summary}`);
169
162
  }
170
163
  else if (r) {
171
- showSuccessToast(t("messagesBulkResults", {
172
- created: r.created,
173
- updated: r.updated,
174
- skipped: r.skipped,
175
- }));
164
+ showSuccessToast(t("messagesAutoPostResults", { paid: r.paid, skipped: r.skipped }));
176
165
  }
177
166
  else {
178
- showSuccessToast(t("messagesBulkSuccess"));
167
+ showSuccessToast(t("messagesAutoPostSuccess"));
179
168
  }
180
- const schoolId = ((_c = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _c === void 0 ? void 0 : _c.id) || "";
181
- fetch(`${STUDENT_FEE_API_ROUTES.UNIT}?currentPage=1&pageLimit=${PAGE_LIMIT}&schoolId=${schoolId}`, {
182
- headers: {
183
- "Content-Type": "application/json",
184
- },
185
- })
186
- .then(async (res) => {
187
- var _a, _b;
188
- if (!res.ok)
189
- return;
190
- const data = await res.json();
191
- dispatch({
192
- type: STUDENT_FEE_ACTION_TYPES.SET_ITEMS,
193
- payload: {
194
- items: (_a = data.items) !== null && _a !== void 0 ? _a : [],
195
- count: (_b = data.count) !== null && _b !== void 0 ? _b : 0,
196
- },
197
- });
198
- })
199
- .catch(() => { });
169
+ refreshList();
200
170
  closeDrawer();
201
171
  }
202
172
  else {
@@ -204,33 +174,22 @@ export const StudentFeeMoreActions = () => {
204
174
  const detail = ((_d = r === null || r === void 0 ? void 0 : r.errors) === null || _d === void 0 ? void 0 : _d.length)
205
175
  ? formatErrorSummary(t, r.errors)
206
176
  : t("unknownError");
207
- showErrorToast(`${t("messagesBulkFailed", { action: label })}\n${detail}`);
177
+ showErrorToast(`${t("messagesAutoPostFailed")}\n${detail}`);
208
178
  }
209
179
  }
210
180
  catch (error) {
211
181
  if (error.message === "Polling cancelled")
212
182
  return;
213
- showErrorToast(t("messagesBulkFailedDetail", {
214
- action: label,
183
+ showErrorToast(t("messagesAutoPostFailedDetail", {
215
184
  error: error instanceof Error ? error.message : t("unknownError"),
216
185
  }));
217
186
  }
218
- }, [dispatch, t]);
219
- const handleBulkUpdate = useCallback((files) => handleBulkFlow(files, "PUT"), [handleBulkFlow]);
220
- const update = [
221
- {
222
- id: "1",
223
- title: t("downloadPopulatedCsvTemplate"),
224
- handleOnClick: async () => {
225
- var _a;
226
- await handleGetAllRecords(((_a = workspace === null || workspace === void 0 ? void 0 : workspace.school) === null || _a === void 0 ? void 0 : _a.id) || "", PAGE_LIMIT);
227
- },
228
- },
229
- { id: "2", title: t("updateYourDataInTheCsv") },
230
- {
231
- id: "3",
232
- title: t("uploadTheCompletedCsvToTheSystem"),
233
- },
234
- ];
235
- return (_jsxs("div", { className: "space-y-4", children: [_jsx(Button, { disabled: new Date().getDate() !== 1, onClick: handleCreateMonthly, children: t("actionsButtonCreateMonthly") }), _jsx(Button, { onClick: handleCreateSingleEntry, children: t("actionsButtonSingleEntry") }), _jsx(Separator, {}), _jsx(Timeline, { events: update, heading: t("bulkUpdate"), handleOnBulkCreate: handleBulkUpdate })] }));
187
+ finally {
188
+ setSubmitting(false);
189
+ }
190
+ }, [t, dispatch, closeDrawer, refreshList, computerNumbersText, autoPostDate]);
191
+ const canSubmit = autoPostDate.trim() !== "" &&
192
+ computerNumbersText.trim() !== "" &&
193
+ !submitting;
194
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(Button, { disabled: new Date().getDate() !== 1, onClick: handleCreateMonthly, children: t("actionsButtonCreateMonthly") }), _jsx(Button, { onClick: handleCreateSingleEntry, children: t("actionsButtonSingleEntry") }), _jsx(Separator, {}), _jsxs("div", { className: "space-y-4", children: [_jsx(EnhancedInput, { id: "autoPostDate", info: t("formAutoPostDateInfo"), label: t("formAutoPostDateLabel"), onChange: (e) => setAutoPostDate(e.target.value), onClick: (e) => { var _a, _b; return (_b = (_a = e.currentTarget).showPicker) === null || _b === void 0 ? void 0 : _b.call(_a); }, type: "date", value: autoPostDate }), _jsx(EnhancedTextarea, { id: "autoPostComputerNumbers", info: t("formAutoPostComputerNumbersInfo"), label: t("formAutoPostComputerNumbersLabel"), onChange: (e) => setComputerNumbersText(e.target.value), placeholder: t("formAutoPostComputerNumbersPlaceholder"), rows: 8, value: computerNumbersText }), _jsx(Button, { disabled: !canSubmit, onClick: handleAutoPost, children: t("actionsButtonAutoPost") })] })] }));
236
195
  };
package/constants.d.ts CHANGED
@@ -81,6 +81,8 @@ export declare const STUDENT_FEE_API_ROUTES: {
81
81
  readonly CREATE_MONTHLY: "/api/v1/student-fee/create-monthly";
82
82
  readonly BY_COMPUTER_NUMBER: "/api/v1/student-fee-by-student-computer-number";
83
83
  readonly SINGLE_ENTRY: "/api/v1/student-fee-single-entry";
84
+ readonly AUTO_POST: "/api/v1/student-fee/auto-post";
85
+ readonly AUTO_POST_STATUS: (jobId: string) => string;
84
86
  };
85
87
  export declare const STUDENT_PROFILE_API_ROUTES: {
86
88
  readonly LIST: "/api/v1/student-profile";
package/constants.js CHANGED
@@ -92,6 +92,8 @@ export const STUDENT_FEE_API_ROUTES = {
92
92
  CREATE_MONTHLY: "/api/v1/student-fee/create-monthly",
93
93
  BY_COMPUTER_NUMBER: "/api/v1/student-fee-by-student-computer-number",
94
94
  SINGLE_ENTRY: "/api/v1/student-fee-single-entry",
95
+ AUTO_POST: "/api/v1/student-fee/auto-post",
96
+ AUTO_POST_STATUS: (jobId) => `/api/v1/student-fee/auto-post/${jobId}`,
95
97
  };
96
98
  export const STUDENT_PROFILE_API_ROUTES = {
97
99
  LIST: "/api/v1/student-profile",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appcorp/fusion-storybook",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "scripts": {
5
5
  "build-storybook": "storybook build",
6
6
  "build:next": "next build",