@appcorp/fusion-storybook 0.4.39 → 0.4.40
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.
- package/base-modules/bulk-operations/constants.d.ts +12 -0
- package/base-modules/bulk-operations/constants.js +20 -0
- package/base-modules/bulk-operations/context.d.ts +172 -0
- package/base-modules/bulk-operations/context.js +283 -0
- package/base-modules/bulk-operations/filter.d.ts +1 -0
- package/base-modules/bulk-operations/filter.js +31 -0
- package/base-modules/bulk-operations/page.d.ts +33 -0
- package/base-modules/bulk-operations/page.js +144 -0
- package/base-modules/bulk-operations/view.d.ts +1 -0
- package/base-modules/bulk-operations/view.js +35 -0
- package/constants.d.ts +4 -0
- package/constants.js +4 -0
- package/package.json +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/bulk-operations.d.ts +48 -0
- package/types/bulk-operations.js +8 -0
- package/types/index.d.ts +1 -0
- package/types/index.js +1 -0
- package/utils/resolve-rbac-permissions.js +10 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk Operations Page Component
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around GenericModulePage. All handlers, header actions, and row
|
|
5
|
+
* actions live in context.tsx — this file only owns:
|
|
6
|
+
* - tableBodyCols (static, module-level constant)
|
|
7
|
+
* - bulkOperationsConfig (memoised on locale + drawer change only)
|
|
8
|
+
* - permission guard
|
|
9
|
+
*
|
|
10
|
+
* Exported utilities: `BulkOperationsPage`
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import { useMemo } from "react";
|
|
15
|
+
import { COMPONENT_TYPE } from "@appcorp/shadcn/components/enhanced-table";
|
|
16
|
+
import { createGenericModulePage, } from "@react-pakistan/util-functions/factory/generic-component-factory";
|
|
17
|
+
import { useBulkOperationsModule, BulkOperationsProvider, BULK_OPERATIONS_ACTION_TYPES } from "./context";
|
|
18
|
+
import { BulkOperationsFilter } from "./filter";
|
|
19
|
+
import { BulkOperationsView } from "./view";
|
|
20
|
+
import { resolveRbacPermissions } from "../../utils/resolve-rbac-permissions";
|
|
21
|
+
import { RbacNoAccess } from "../../components/rbac-no-access";
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// TABLE COLUMN CONFIGURATION (static — no runtime deps)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const tableBodyCols = [
|
|
26
|
+
{ componentType: COMPONENT_TYPE.ID, key: "jobId" },
|
|
27
|
+
{ componentType: COMPONENT_TYPE.TEXT, key: "type" },
|
|
28
|
+
{ componentType: COMPONENT_TYPE.TEXT, key: "status" },
|
|
29
|
+
{
|
|
30
|
+
componentType: COMPONENT_TYPE.TEXT,
|
|
31
|
+
key: "progress",
|
|
32
|
+
textFormatter: (_value, row) => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const job = row;
|
|
35
|
+
return `${(_a = job.processed) !== null && _a !== void 0 ? _a : 0} / ${(_b = job.total) !== null && _b !== void 0 ? _b : 0}`;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
componentType: COMPONENT_TYPE.CREATED_UPDATED_AT,
|
|
40
|
+
key: "createdAt",
|
|
41
|
+
},
|
|
42
|
+
{ componentType: COMPONENT_TYPE.ACTIONS },
|
|
43
|
+
];
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// COMPONENT INSTANCES (memoized — no runtime deps)
|
|
46
|
+
// ============================================================================
|
|
47
|
+
const createComponentInstances = () => ({
|
|
48
|
+
filter: _jsx(BulkOperationsFilter, {}),
|
|
49
|
+
view: _jsx(BulkOperationsView, {}),
|
|
50
|
+
});
|
|
51
|
+
const createBulkOperationsConfig = ({ cancelLabel, dispatch, drawerFilterDescription, drawerFilterTitle, drawerViewDescription, drawerViewTitle, labelActions, labelId, labelProgress, labelState, labelStatus, labelType, saveLabel, searchPlaceholder, tableDescription, tableTitle, components, }) => {
|
|
52
|
+
return {
|
|
53
|
+
moduleName: "bulk-operations",
|
|
54
|
+
tableColumns: [
|
|
55
|
+
{ label: labelId, width: "20%" },
|
|
56
|
+
{ label: labelType, width: "22%" },
|
|
57
|
+
{ label: labelStatus, width: "12%" },
|
|
58
|
+
{ label: labelProgress, width: "12%" },
|
|
59
|
+
{ label: labelState, width: "20%" },
|
|
60
|
+
{ label: labelActions, width: "14%" },
|
|
61
|
+
],
|
|
62
|
+
cancelLabel,
|
|
63
|
+
drawerFilterDescription,
|
|
64
|
+
drawerFilterTitle,
|
|
65
|
+
drawerFormDescription: "",
|
|
66
|
+
drawerFormTitle: "",
|
|
67
|
+
drawerMoreActionsDescription: "",
|
|
68
|
+
drawerMoreActionsTitle: "",
|
|
69
|
+
drawerViewDescription,
|
|
70
|
+
drawerViewTitle,
|
|
71
|
+
filterContent: components.filter,
|
|
72
|
+
formContent: null,
|
|
73
|
+
moreActionsContent: null,
|
|
74
|
+
saveLabel,
|
|
75
|
+
searchPlaceholder,
|
|
76
|
+
tableDescription,
|
|
77
|
+
tableTitle,
|
|
78
|
+
viewContent: components.view,
|
|
79
|
+
onClearFilters: () => {
|
|
80
|
+
dispatch({
|
|
81
|
+
type: BULK_OPERATIONS_ACTION_TYPES.SET_FILTERS,
|
|
82
|
+
payload: { filters: { filterType: "", filterStatus: "" } },
|
|
83
|
+
});
|
|
84
|
+
dispatch({
|
|
85
|
+
type: BULK_OPERATIONS_ACTION_TYPES.SET_CURRENT_PAGE,
|
|
86
|
+
payload: { currentPage: 1 },
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// STABLE PAGE COMPONENT (created once, outside render)
|
|
93
|
+
// ============================================================================
|
|
94
|
+
const GenericBulkOperationsPage = createGenericModulePage();
|
|
95
|
+
const BulkOperationsPageInner = (props) => {
|
|
96
|
+
const context = useBulkOperationsModule();
|
|
97
|
+
const components = useMemo(() => createComponentInstances(), []);
|
|
98
|
+
const bulkOperationsConfig = useMemo(() => createBulkOperationsConfig({
|
|
99
|
+
dispatch: context.dispatch,
|
|
100
|
+
cancelLabel: props.cancelLabel,
|
|
101
|
+
drawerViewTitle: props.drawerViewTitle,
|
|
102
|
+
drawerViewDescription: props.drawerViewDescription,
|
|
103
|
+
drawerFilterTitle: props.drawerFilterTitle,
|
|
104
|
+
drawerFilterDescription: props.drawerFilterDescription,
|
|
105
|
+
labelActions: props.labelActions,
|
|
106
|
+
labelId: props.labelId,
|
|
107
|
+
labelProgress: props.labelProgress,
|
|
108
|
+
labelState: props.labelState,
|
|
109
|
+
labelStatus: props.labelStatus,
|
|
110
|
+
labelType: props.labelType,
|
|
111
|
+
saveLabel: props.saveLabel,
|
|
112
|
+
searchPlaceholder: props.searchPlaceholder,
|
|
113
|
+
tableDescription: props.tableDescription,
|
|
114
|
+
tableTitle: props.tableTitle,
|
|
115
|
+
components,
|
|
116
|
+
}), [
|
|
117
|
+
context.dispatch,
|
|
118
|
+
props.cancelLabel,
|
|
119
|
+
props.drawerViewTitle,
|
|
120
|
+
props.drawerViewDescription,
|
|
121
|
+
props.drawerFilterTitle,
|
|
122
|
+
props.drawerFilterDescription,
|
|
123
|
+
props.labelActions,
|
|
124
|
+
props.labelId,
|
|
125
|
+
props.labelProgress,
|
|
126
|
+
props.labelState,
|
|
127
|
+
props.labelStatus,
|
|
128
|
+
props.labelType,
|
|
129
|
+
props.saveLabel,
|
|
130
|
+
props.searchPlaceholder,
|
|
131
|
+
props.tableDescription,
|
|
132
|
+
props.tableTitle,
|
|
133
|
+
components,
|
|
134
|
+
]);
|
|
135
|
+
const hasPermission = resolveRbacPermissions({
|
|
136
|
+
userRole: props.userRole,
|
|
137
|
+
moduleName: "Bulk Operations",
|
|
138
|
+
});
|
|
139
|
+
if (!hasPermission) {
|
|
140
|
+
return _jsx(RbacNoAccess, { moduleName: "Bulk Operations" });
|
|
141
|
+
}
|
|
142
|
+
return (_jsx("div", { className: "p-4", children: _jsx(GenericBulkOperationsPage, { overrideConfig: bulkOperationsConfig, context: context, tableBodyCols: tableBodyCols }) }));
|
|
143
|
+
};
|
|
144
|
+
export const BulkOperationsPage = (props) => (_jsx(BulkOperationsProvider, { children: _jsx(BulkOperationsPageInner, Object.assign({}, props)) }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BulkOperationsView: () => import("react").JSX.Element | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Bulk Operations View
|
|
5
|
+
*
|
|
6
|
+
* Read-only details view for a single bulk job record. The row data is spread
|
|
7
|
+
* into top-level module state (SET_FORM_DATA) when the row's View action fires.
|
|
8
|
+
*/
|
|
9
|
+
import { useTranslations } from "next-intl";
|
|
10
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@appcorp/shadcn/components/ui/card";
|
|
11
|
+
import { Badge } from "@appcorp/shadcn/components/ui/badge";
|
|
12
|
+
import { Separator } from "@appcorp/shadcn/components/ui/separator";
|
|
13
|
+
import { CheckCircle2, ClipboardList, Clock, Layers, ListChecks, XCircle, } from "lucide-react";
|
|
14
|
+
import { DATE_FORMATS, formatDate, } from "@react-pakistan/util-functions/general/format-date";
|
|
15
|
+
import { useBulkOperationsContext } from "./context";
|
|
16
|
+
const STATUS_VARIANT = {
|
|
17
|
+
completed: "default",
|
|
18
|
+
running: "secondary",
|
|
19
|
+
queued: "outline",
|
|
20
|
+
failed: "destructive",
|
|
21
|
+
};
|
|
22
|
+
export const BulkOperationsView = () => {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
24
|
+
const { state } = useBulkOperationsContext();
|
|
25
|
+
const { createdAt, jobId, processed, results, status, total, type, updatedAt, webhookUrl, } = state;
|
|
26
|
+
const t = useTranslations("bulkOperations");
|
|
27
|
+
if (!jobId)
|
|
28
|
+
return null;
|
|
29
|
+
const fmt = (v) => v ? formatDate(v, DATE_FORMATS.LOCALE_DATE) : "—";
|
|
30
|
+
const jobResults = (results !== null && results !== void 0 ? results : null);
|
|
31
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ClipboardList, { className: "text-primary h-5 w-5" }), _jsx(CardTitle, { className: "text-lg", children: t("viewSectionJobDetails") })] }), _jsx(CardDescription, { children: t("viewSectionJobDescription") })] }), _jsx(Separator, {}), _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "grid grid-cols-1 gap-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldJobId") }), _jsx("p", { className: "font-mono text-right text-sm break-all", children: jobId })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldType") }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Layers, { className: "text-muted-foreground h-4 w-4" }), _jsx("p", { className: "text-right text-sm", children: type })] })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldStatus") }), _jsx(Badge, { variant: (_a = STATUS_VARIANT[status !== null && status !== void 0 ? status : "queued"]) !== null && _a !== void 0 ? _a : "secondary", children: status || "—" })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldProgress") }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ListChecks, { className: "text-muted-foreground h-4 w-4" }), _jsxs("p", { className: "text-right text-sm", children: [processed !== null && processed !== void 0 ? processed : 0, " / ", total !== null && total !== void 0 ? total : 0] })] })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldCreated") }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Clock, { className: "text-muted-foreground h-4 w-4" }), _jsx("p", { className: "text-right text-sm", children: fmt(createdAt) })] })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldUpdated") }), _jsx("p", { className: "text-right text-sm", children: fmt(updatedAt) })] }), webhookUrl ? (_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldWebhook") }), _jsx("p", { className: "text-right text-sm break-all", children: webhookUrl })] })) : null] }) })] }), jobResults ? (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ListChecks, { className: "text-primary h-5 w-5" }), _jsx(CardTitle, { className: "text-lg", children: t("viewSectionResults") })] }), _jsx(CardDescription, { children: t("viewSectionResultsDescription") })] }), _jsx(Separator, {}), _jsxs(CardContent, { className: "pt-6", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldTotal") }), _jsx("p", { className: "text-right text-sm", children: (_b = jobResults.total) !== null && _b !== void 0 ? _b : 0 })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CheckCircle2, { className: "text-green-600 h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldCreated") })] }), _jsx("p", { className: "text-right text-sm", children: (_c = jobResults.created) !== null && _c !== void 0 ? _c : 0 })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CheckCircle2, { className: "text-blue-600 h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldUpdated") })] }), _jsx("p", { className: "text-right text-sm", children: (_d = jobResults.updated) !== null && _d !== void 0 ? _d : 0 })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldSkipped") }), _jsx("p", { className: "text-right text-sm", children: (_e = jobResults.skipped) !== null && _e !== void 0 ? _e : 0 })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(XCircle, { className: "text-red-600 h-4 w-4" }), _jsx("p", { className: "text-muted-foreground text-sm font-medium", children: t("viewFieldErrors") })] }), _jsx("p", { className: "text-right text-sm", children: (_g = (_f = jobResults.errors) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0 })] })] }), jobResults.errors && jobResults.errors.length > 0 ? (_jsxs("div", { className: "mt-4 space-y-2", children: [jobResults.errors.slice(0, 5).map((err, i) => {
|
|
32
|
+
var _a;
|
|
33
|
+
return (_jsxs("div", { className: "bg-destructive/10 text-destructive rounded-md px-3 py-2 text-xs", children: [_jsxs("span", { className: "font-medium", children: ["Row ", (_a = err.row) !== null && _a !== void 0 ? _a : "?", err.studentProfileId ? ` • ${err.studentProfileId}` : "", ":"] }), " ", err.error] }, i));
|
|
34
|
+
}), jobResults.errors.length > 5 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: t("viewErrorsMore", { count: jobResults.errors.length - 5 }) })) : null] })) : null] })] })) : null] }));
|
|
35
|
+
};
|
package/constants.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare const ATTENDANCE_API_ROUTES: {
|
|
|
13
13
|
readonly BULK: "/api/v1/attendance/bulk";
|
|
14
14
|
readonly BULK_STATUS: (jobId: string) => string;
|
|
15
15
|
};
|
|
16
|
+
export declare const BULK_OPERATIONS_API_ROUTES: {
|
|
17
|
+
readonly LIST: "/api/v1/bulk-operations";
|
|
18
|
+
};
|
|
16
19
|
export declare const ADMISSION_API_ROUTES: {
|
|
17
20
|
readonly UNIT: "/api/v1/admission";
|
|
18
21
|
readonly BULK: "/api/v1/admission/bulk";
|
|
@@ -119,6 +122,7 @@ export declare const DASHBOARD_ROUTES: {
|
|
|
119
122
|
AI_ASSISTANT: string;
|
|
120
123
|
AI_ATTENDANCE: string;
|
|
121
124
|
ATTENDANCE: string;
|
|
125
|
+
BULK_OPERATIONS: string;
|
|
122
126
|
CLASS: string;
|
|
123
127
|
COMMUNICATION: string;
|
|
124
128
|
COURSE: string;
|
package/constants.js
CHANGED
|
@@ -16,6 +16,9 @@ export const ATTENDANCE_API_ROUTES = {
|
|
|
16
16
|
BULK: "/api/v1/attendance/bulk",
|
|
17
17
|
BULK_STATUS: (jobId) => `/api/v1/attendance/bulk/${jobId}`,
|
|
18
18
|
};
|
|
19
|
+
export const BULK_OPERATIONS_API_ROUTES = {
|
|
20
|
+
LIST: "/api/v1/bulk-operations",
|
|
21
|
+
};
|
|
19
22
|
export const ADMISSION_API_ROUTES = {
|
|
20
23
|
UNIT: "/api/v1/admission",
|
|
21
24
|
BULK: "/api/v1/admission/bulk",
|
|
@@ -140,6 +143,7 @@ export const DASHBOARD_ROUTES = {
|
|
|
140
143
|
AI_ASSISTANT: "/tools/ai-assistant",
|
|
141
144
|
AI_ATTENDANCE: "/tools/ai-attendance",
|
|
142
145
|
ATTENDANCE: "/operational-tracking/attendance",
|
|
146
|
+
BULK_OPERATIONS: "/apps/bulk-operations",
|
|
143
147
|
CLASS: "/academic-structure/class",
|
|
144
148
|
COMMUNICATION: "/communication",
|
|
145
149
|
COURSE: "/academic-operations/course",
|