@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.
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appcorp/fusion-storybook",
3
- "version": "0.4.39",
3
+ "version": "0.4.40",
4
4
  "scripts": {
5
5
  "build-storybook": "storybook build",
6
6
  "build:next": "next build",