indas-ui 0.1.3 → 0.1.5
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/dist/auth.d.ts +1 -0
- package/dist/auth.js +5 -0
- package/dist/cjs/auth.js +10 -0
- package/dist/cjs/components/chat/ai-messages.js +3 -2
- package/dist/cjs/components/chat/chat.js +4 -3
- package/dist/cjs/components/dashboard/FilterBar.js +31 -8
- package/dist/cjs/components/dashboard/charts/Area.js +6 -4
- package/dist/cjs/components/dashboard/charts/Bar.js +6 -4
- package/dist/cjs/components/dashboard/charts/FlowMap.js +2 -1
- package/dist/cjs/components/dashboard/charts/Funnel.js +2 -1
- package/dist/cjs/components/dashboard/charts/GeoMap.js +2 -1
- package/dist/cjs/components/dashboard/charts/Line.js +5 -3
- package/dist/cjs/components/dashboard/charts/Pareto.js +2 -1
- package/dist/cjs/components/dashboard/charts/Sankey.js +4 -3
- package/dist/cjs/components/dashboard/charts/Sparkline.js +2 -1
- package/dist/cjs/components/dashboard/charts/Treemap.js +2 -1
- package/dist/cjs/components/dashboard/charts/index.js +7 -1
- package/dist/cjs/components/dashboard/kpi/KpiBreakdown.js +3 -2
- package/dist/cjs/components/dashboard/kpi/KpiComparison.js +2 -1
- package/dist/cjs/components/dashboard/kpi/KpiQuickStats.js +2 -1
- package/dist/cjs/components/dashboard/kpi/KpiRating.js +3 -2
- package/dist/cjs/components/dashboard/time-range.js +71 -27
- package/dist/cjs/components/datagrid/columns/ColumnContextMenu.js +2 -1
- package/dist/cjs/components/datagrid/controls/menu/view/CardView.js +2 -1
- package/dist/cjs/components/datagrid/controls/menu/view/ChartView.js +2 -1
- package/dist/cjs/components/datagrid/utils/grid-helpers.js +6 -5
- package/dist/cjs/components/layout/footer.js +2 -1
- package/dist/cjs/components/layout/side-panel.js +4 -1
- package/dist/cjs/components/layout/sidebar.js +17 -2
- package/dist/cjs/components/messaging/messaging-panel-content.js +2 -2
- package/dist/cjs/components/ui/display/stats-card.js +2 -1
- package/dist/cjs/components/ui/navigation/tabs.js +4 -1
- package/dist/cjs/contexts/EmailContext.js +3 -2
- package/dist/cjs/contexts/MessagingContext.js +2 -2
- package/dist/cjs/contexts/NavigationContext.js +17 -2
- package/dist/cjs/contexts/NotificationContext.js +2 -2
- package/dist/cjs/contexts/SessionAdapterContext.js +8 -0
- package/dist/cjs/hooks/useApproval.js +2 -2
- package/dist/cjs/hooks/useBusinessAudit.js +2 -2
- package/dist/cjs/hooks/useDraft.js +2 -2
- package/dist/cjs/lib/api/utility/menu.js +8 -7
- package/dist/cjs/lib/number-format.js +60 -0
- package/dist/cjs/lib/utils.js +4 -3
- package/dist/cjs/providers.js +3 -3
- package/dist/components/auth/index.js +0 -1
- package/dist/components/chat/ai-messages.js +3 -2
- package/dist/components/chat/chat.js +4 -3
- package/dist/components/chat/index.js +0 -1
- package/dist/components/dashboard/FilterBar.d.ts +5 -1
- package/dist/components/dashboard/FilterBar.js +31 -8
- package/dist/components/dashboard/charts/Area.d.ts +5 -1
- package/dist/components/dashboard/charts/Area.js +6 -4
- package/dist/components/dashboard/charts/Bar.d.ts +5 -1
- package/dist/components/dashboard/charts/Bar.js +6 -4
- package/dist/components/dashboard/charts/FlowMap.js +2 -1
- package/dist/components/dashboard/charts/Funnel.js +2 -1
- package/dist/components/dashboard/charts/GeoMap.js +2 -1
- package/dist/components/dashboard/charts/Line.d.ts +5 -1
- package/dist/components/dashboard/charts/Line.js +5 -3
- package/dist/components/dashboard/charts/Pareto.js +2 -1
- package/dist/components/dashboard/charts/Sankey.js +4 -3
- package/dist/components/dashboard/charts/Sparkline.js +2 -1
- package/dist/components/dashboard/charts/Treemap.js +2 -1
- package/dist/components/dashboard/charts/index.d.ts +1 -0
- package/dist/components/dashboard/charts/index.js +2 -1
- package/dist/components/dashboard/index.js +0 -1
- package/dist/components/dashboard/kpi/KpiBreakdown.js +3 -2
- package/dist/components/dashboard/kpi/KpiComparison.js +2 -1
- package/dist/components/dashboard/kpi/KpiQuickStats.js +2 -1
- package/dist/components/dashboard/kpi/KpiRating.js +3 -2
- package/dist/components/dashboard/kpi/index.js +0 -1
- package/dist/components/dashboard/time-range.d.ts +12 -1
- package/dist/components/dashboard/time-range.js +71 -27
- package/dist/components/datagrid/cells/index.js +0 -1
- package/dist/components/datagrid/columns/ColumnContextMenu.js +2 -1
- package/dist/components/datagrid/controls/menu/grouping/index.js +0 -1
- package/dist/components/datagrid/controls/menu/index.js +0 -1
- package/dist/components/datagrid/controls/menu/view/CardView.js +2 -1
- package/dist/components/datagrid/controls/menu/view/ChartView.js +2 -1
- package/dist/components/datagrid/index.js +0 -1
- package/dist/components/datagrid/utils/grid-helpers.d.ts +1 -1
- package/dist/components/datagrid/utils/grid-helpers.js +3 -2
- package/dist/components/forms/date-picker/index.js +0 -1
- package/dist/components/forms/dropdown/index.js +0 -1
- package/dist/components/forms/file-attachment/index.js +0 -1
- package/dist/components/forms/index.js +0 -1
- package/dist/components/forms/remark-input/index.js +0 -1
- package/dist/components/index.js +0 -1
- package/dist/components/layout/footer.js +2 -1
- package/dist/components/layout/index.js +0 -1
- package/dist/components/layout/side-panel.js +4 -1
- package/dist/components/layout/sidebar.js +17 -2
- package/dist/components/master/editable-cells/index.js +0 -1
- package/dist/components/master/index.js +0 -1
- package/dist/components/messaging/messaging-panel-content.js +2 -2
- package/dist/components/modals/index.js +0 -1
- package/dist/components/ui/display/stats-card.js +2 -1
- package/dist/components/ui/feedback/badge.d.ts +1 -1
- package/dist/components/ui/index.js +0 -1
- package/dist/components/ui/navigation/tabs.js +4 -1
- package/dist/contexts/EmailContext.js +3 -2
- package/dist/contexts/MessagingContext.js +2 -2
- package/dist/contexts/NavigationContext.js +17 -2
- package/dist/contexts/NotificationContext.js +2 -2
- package/dist/contexts/SessionAdapterContext.d.ts +5 -0
- package/dist/contexts/SessionAdapterContext.js +7 -0
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/useApproval.js +2 -2
- package/dist/hooks/useBusinessAudit.js +2 -2
- package/dist/hooks/useDraft.js +2 -2
- package/dist/index.js +0 -1
- package/dist/lib/api/activity/audit/index.js +0 -1
- package/dist/lib/api/activity/email/index.js +0 -1
- package/dist/lib/api/activity/index.js +0 -1
- package/dist/lib/api/activity/notifications/index.js +0 -1
- package/dist/lib/api/activity/remarks/index.js +0 -1
- package/dist/lib/api/approval/index.js +0 -1
- package/dist/lib/api/auth/index.js +0 -1
- package/dist/lib/api/core/index.js +0 -1
- package/dist/lib/api/draft/index.js +0 -1
- package/dist/lib/api/enquiry/index.js +0 -1
- package/dist/lib/api/estimation/index.js +0 -1
- package/dist/lib/api/index.js +0 -1
- package/dist/lib/api/master/index.js +0 -1
- package/dist/lib/api/utility/index.js +0 -1
- package/dist/lib/api/utility/menu.d.ts +7 -3
- package/dist/lib/api/utility/menu.js +8 -7
- package/dist/lib/number-format.d.ts +18 -0
- package/dist/lib/number-format.js +54 -0
- package/dist/lib/utils.js +4 -3
- package/dist/providers.d.ts +0 -1
- package/dist/providers.js +2 -2
- package/package.json +23 -8
package/dist/components/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "../../lib/utils.js";
|
|
4
4
|
import { useTranslation } from "../../hooks/useTranslation.js";
|
|
5
|
+
import { formatNumber } from '../../lib/number-format.js';
|
|
5
6
|
export function Footer({ kpis, actions, iconButtons, leftContent, className, sticky = false, compact = false, padding, variant = 'page', gradient = false }) {
|
|
6
7
|
if (!kpis && !actions && !leftContent && !iconButtons) {
|
|
7
8
|
return null;
|
|
@@ -41,7 +42,7 @@ export function Footer({ kpis, actions, iconButtons, leftContent, className, sti
|
|
|
41
42
|
padding === 'normal' ? "py-3 sm:py-4" :
|
|
42
43
|
compact ? "py-1" :
|
|
43
44
|
isModal ? "py-1.5 sm:py-2" : "py-3 sm:py-4"), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 sm:gap-4 w-full sm:w-auto", children: [leftContent, kpis && kpis.length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-3 sm:gap-6 md:gap-8", children: kpis.map((kpi) => (_jsxs("div", { className: cn("flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm", kpi.onClick && "cursor-pointer hover:text-[rgb(var(--color-primary-hover))] transition-colors"), onClick: kpi.onClick, children: [_jsxs("span", { className: "text-[rgb(var(--fg-muted))] whitespace-nowrap", children: [kpi.label, ":"] }), _jsx("span", { className: "font-medium text-[rgb(var(--fg-default))]", children: typeof kpi.value === 'number'
|
|
44
|
-
? kpi.value
|
|
45
|
+
? formatNumber(kpi.value)
|
|
45
46
|
: kpi.value }), kpi.trend && (_jsxs("span", { className: cn("text-xs font-medium", kpi.trend.direction === 'up'
|
|
46
47
|
? "text-[rgb(var(--color-success))]"
|
|
47
48
|
: "text-[rgb(var(--color-error))]"), children: [kpi.trend.direction === 'up' ? '↑' : '↓', Math.abs(kpi.trend.value), "%"] }))] }, kpi.id))) }))] }), iconButtons && iconButtons.length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2", children: iconButtons.map((button) => {
|
|
@@ -107,7 +107,10 @@ export function SidePanel({ title, icon: Icon, categories, activeItemId, onItemS
|
|
|
107
107
|
return _jsx(ItemIcon, { className: "w-4 h-4 shrink-0 opacity-50" });
|
|
108
108
|
})(), _jsx("span", { className: "truncate flex-1", children: item.label }), item.badge !== undefined && item.badge !== 0 && (_jsx("span", { className: cn("text-xs px-1.5 py-0.5 rounded-full shrink-0 tabular-nums", activeItemId === item.id
|
|
109
109
|
? "bg-[rgb(var(--color-primary))] text-white"
|
|
110
|
-
: "bg-[rgb(var(--bg-subtle))] text-[rgb(var(--fg-muted))]"), children: item.badge }))] }), itemContextMenu && (
|
|
110
|
+
: "bg-[rgb(var(--bg-subtle))] text-[rgb(var(--fg-muted))]"), children: item.badge }))] }), itemContextMenu && (
|
|
111
|
+
// Named for screen readers, and shown on keyboard focus as well as hover:
|
|
112
|
+
// it was an unlabelled button nobody tabbing could see.
|
|
113
|
+
_jsx("button", { type: "button", onClick: (e) => handleMenuToggle(e, item.id), "aria-label": `More actions for ${item.label}`, "aria-haspopup": "menu", "aria-expanded": menuOpen === item.id, className: "absolute right-1.5 top-1/2 -translate-y-1/2 w-6 h-6 rounded-md text-[rgb(var(--fg-muted))]/50 flex items-center justify-center hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-all opacity-0 group-hover:opacity-100 focus-visible:opacity-100", children: _jsx(MoreVertical, { className: "w-3.5 h-3.5" }) }))] }, item.id))) }))] }, category.id))))] })) }), footer && !isCollapsed && (_jsx("div", { className: cn("shrink-0 border-t border-[rgb(var(--bd-default))] transition-opacity duration-200", isAnimating ? "opacity-0" : "opacity-100"), children: footer }))] }), menuOpen !== null && itemContextMenu && (_jsx("div", { ref: menuRef, className: "fixed bg-[rgb(var(--bg-surface))] border border-[rgb(var(--bd-default))] rounded-lg min-w-[170px] shadow-lg overflow-hidden z-[9999]", style: {
|
|
111
114
|
left: `${menuPosition.left}px`,
|
|
112
115
|
top: `${menuPosition.top}px`,
|
|
113
116
|
transform: 'translateY(-50%)'
|
|
@@ -13,6 +13,7 @@ import '../../styles/sidebar-scrollbar.css';
|
|
|
13
13
|
import { createPortal } from 'react-dom';
|
|
14
14
|
import { SidebarContextMenu, SidebarGroupContextMenu } from './SidebarContextMenu.js';
|
|
15
15
|
import { useSidebarPreferences } from '../../contexts/SidebarPreferencesContext.js';
|
|
16
|
+
import { useOptionalSessionAdapter } from '../../contexts/SessionAdapterContext.js';
|
|
16
17
|
// Route mapping imported from shared utility: getModuleRoutePath
|
|
17
18
|
// Routes that support quick-create via sidebar "+" button.
|
|
18
19
|
// When clicked, navigates to {route}?create=true — the page opens its create modal.
|
|
@@ -123,6 +124,14 @@ export function DynamicSidebar({ isCollapsed = false, onMenuItemClick, onToggleS
|
|
|
123
124
|
const [expandedSubModules, setExpandedSubModules] = React.useState([]);
|
|
124
125
|
const [loading, setLoading] = React.useState(true);
|
|
125
126
|
const [error, setError] = React.useState(null);
|
|
127
|
+
// The menu API needs the signed-in session (company credentials). Read from the session adapter,
|
|
128
|
+
// not next-auth, so the library does not require next-auth; held in a ref so a fresh session
|
|
129
|
+
// object does not refetch the menu, only the session arriving or leaving does.
|
|
130
|
+
const sessionAdapter = useOptionalSessionAdapter();
|
|
131
|
+
const sessionRef = React.useRef(sessionAdapter?.data ?? null);
|
|
132
|
+
sessionRef.current = sessionAdapter?.data ?? null;
|
|
133
|
+
const hasSession = Boolean(sessionAdapter?.data);
|
|
134
|
+
const sessionStatus = sessionAdapter?.status ?? 'unauthenticated';
|
|
126
135
|
const [needsScroll, setNeedsScroll] = React.useState(false);
|
|
127
136
|
const navRef = React.useRef(null);
|
|
128
137
|
const [hoveredGroup, setHoveredGroup] = React.useState(null);
|
|
@@ -164,6 +173,12 @@ export function DynamicSidebar({ isCollapsed = false, onMenuItemClick, onToggleS
|
|
|
164
173
|
if (!companyId || !userId) {
|
|
165
174
|
return;
|
|
166
175
|
}
|
|
176
|
+
// No session yet: wait for it (this runs again when it arrives); none at all: no menu.
|
|
177
|
+
if (!hasSession) {
|
|
178
|
+
if (sessionStatus !== 'loading')
|
|
179
|
+
setLoading(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
167
182
|
let isCancelled = false;
|
|
168
183
|
const fetchNavigation = async () => {
|
|
169
184
|
// Check cache first - include session start time to invalidate cache across logins
|
|
@@ -189,7 +204,7 @@ export function DynamicSidebar({ isCollapsed = false, onMenuItemClick, onToggleS
|
|
|
189
204
|
try {
|
|
190
205
|
setLoading(true);
|
|
191
206
|
setError(null);
|
|
192
|
-
const navigation = await getDynamicNavigation(companyId, userId);
|
|
207
|
+
const navigation = await getDynamicNavigation(companyId, userId, sessionRef.current);
|
|
193
208
|
if (!isCancelled) {
|
|
194
209
|
setGroupedModules(navigation);
|
|
195
210
|
// Cache the result
|
|
@@ -217,7 +232,7 @@ export function DynamicSidebar({ isCollapsed = false, onMenuItemClick, onToggleS
|
|
|
217
232
|
clearTimeout(hoverTimeoutRef.current);
|
|
218
233
|
}
|
|
219
234
|
};
|
|
220
|
-
}, [companyId, userId]);
|
|
235
|
+
}, [companyId, userId, hasSession, sessionStatus]);
|
|
221
236
|
// Auto-open parent items based on current path (optimized)
|
|
222
237
|
React.useEffect(() => {
|
|
223
238
|
if (groupedModules.length === 0)
|
|
@@ -5,14 +5,14 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import { MessageSquare, Send, RefreshCw } from 'lucide-react';
|
|
6
6
|
import { cn } from '../../lib/utils.js';
|
|
7
7
|
import { useLanguage } from '../../contexts/LanguageContext.js';
|
|
8
|
-
import {
|
|
8
|
+
import { useOptionalSessionAdapter } from '../../contexts/SessionAdapterContext.js';
|
|
9
9
|
import { useMessaging } from '../../contexts/MessagingContext.js';
|
|
10
10
|
import { parseParticipants } from '../../lib/api/activity/messaging/messaging.js';
|
|
11
11
|
import { getAvatarColor, getInitials, formatRelativeTime } from './conversation-list.js';
|
|
12
12
|
import { Button } from '../ui/index.js';
|
|
13
13
|
export function MessagingPanelContent({ onClose }) {
|
|
14
14
|
const { t } = useLanguage();
|
|
15
|
-
const
|
|
15
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
16
16
|
const { state, actions } = useMessaging();
|
|
17
17
|
const router = useRouter();
|
|
18
18
|
const currentUserId = String(session?.user?.UserID || session?.user?.userID || '');
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { TrendingUp, TrendingDown } from "lucide-react";
|
|
4
4
|
import { cn } from "../../../lib/utils.js";
|
|
5
5
|
import { Card } from "./card.js";
|
|
6
|
+
import { formatNumber } from '../../../lib/number-format.js';
|
|
6
7
|
const iconVariants = {
|
|
7
8
|
default: "bg-primary text-white",
|
|
8
9
|
accent: "bg-accent text-white",
|
|
@@ -12,7 +13,7 @@ const iconVariants = {
|
|
|
12
13
|
export function StatsCard({ title, value, icon: Icon, trend, description, className, variant = 'default' }) {
|
|
13
14
|
const TrendIcon = trend?.direction === 'up' ? TrendingUp : TrendingDown;
|
|
14
15
|
const trendColor = trend?.direction === 'up' ? 'text-success' : 'text-error';
|
|
15
|
-
return (_jsx(Card, { className: cn("p-6 min-h-[140px] w-full", className), children: _jsxs("div", { className: "flex items-start gap-4", children: [Icon && (_jsx("div", { className: cn("flex-shrink-0 p-3 rounded-lg", iconVariants[variant]), children: _jsx(Icon, { className: "h-6 w-6" }) })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-baseline flex-wrap gap-2", children: [_jsx("p", { className: "text-2xl font-semibold text-fg-default truncate", children: typeof value === 'number' ? value
|
|
16
|
+
return (_jsx(Card, { className: cn("p-6 min-h-[140px] w-full", className), children: _jsxs("div", { className: "flex items-start gap-4", children: [Icon && (_jsx("div", { className: cn("flex-shrink-0 p-3 rounded-lg", iconVariants[variant]), children: _jsx(Icon, { className: "h-6 w-6" }) })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-baseline flex-wrap gap-2", children: [_jsx("p", { className: "text-2xl font-semibold text-fg-default truncate", children: typeof value === 'number' ? formatNumber(value) : value }), trend && (_jsxs("div", { className: cn("flex items-center text-sm font-semibold", trendColor), children: [_jsx(TrendIcon, { className: "h-4 w-4 flex-shrink-0" }), _jsxs("span", { className: "ml-1", children: [Math.abs(trend.value), "%", trend.label && ` ${trend.label}`] })] }))] }), _jsx("p", { className: "text-sm font-medium text-fg-muted mt-2 line-clamp-2", children: title }), description && (_jsx("p", { className: "text-sm text-fg-subtle mt-1 line-clamp-1", children: description }))] })] }) }));
|
|
16
17
|
}
|
|
17
18
|
export function StatsGrid({ children, columns = 4, className }) {
|
|
18
19
|
const gridCols = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: {
|
|
4
|
-
variant?: "secondary" | "outline" | "destructive" | "warning" | "success" | "orange" | "cyan" | "indigo" | "lime" | "pink" | "purple" | "teal" | "violet" | "default" | "
|
|
4
|
+
variant?: "secondary" | "outline" | "destructive" | "warning" | "success" | "orange" | "cyan" | "indigo" | "lime" | "pink" | "purple" | "teal" | "violet" | "default" | "module" | "info" | "department" | "danger" | "amber" | "emerald" | "sky" | "rose" | "primary-selected" | "warning-selected" | "success-selected" | "orange-selected" | "error-selected" | "primary-unselected" | "warning-unselected" | "success-unselected" | "orange-unselected" | "error-unselected";
|
|
5
5
|
size?: "sm" | "lg" | "default";
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) => string;
|
|
7
7
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
@@ -38,7 +38,10 @@ export function Tabs({ tabs, activeTab, onTabChange, fullWidth = false, classNam
|
|
|
38
38
|
? 'bg-[rgb(var(--color-primary))] text-white border-[rgb(var(--color-primary))]'
|
|
39
39
|
: 'bg-[rgb(var(--bg-surface))] text-[rgb(var(--fg-muted))] border-[rgb(var(--bd-default))] hover:border-[rgb(var(--color-primary))] hover:text-[rgb(var(--color-primary))]'), children: q.label }, q.value));
|
|
40
40
|
}) })) : undefined;
|
|
41
|
-
return (_jsx(Dropdown, { options: tab.dropdownOptions,
|
|
41
|
+
return (_jsx(Dropdown, { options: tab.dropdownOptions,
|
|
42
|
+
// Periods are listed in time order (Apr, May, Jun… / Week 1, 2, 3…); sorted by label,
|
|
43
|
+
// the months came out Apr, Aug, Dec, Feb.
|
|
44
|
+
preserveOrder: true, value: selected, onValueChange: (sel) => {
|
|
42
45
|
const arr = Array.isArray(sel) ? sel.map(String) : [String(sel)];
|
|
43
46
|
// Ticking a named option clears any quick preset (This/Last) — they're exclusive.
|
|
44
47
|
const quickVals = (tab.quickOptions || []).map(q => q.value);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useReducer, useEffect, useCallback, useMemo } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useOptionalSessionAdapter } from './SessionAdapterContext.js';
|
|
5
5
|
import { createEmailAPI, DEFAULT_FOLDERS, DEFAULT_CATEGORIES } from '../lib/api/activity/email/index.js';
|
|
6
6
|
const FOLDER_CACHE_MAX_AGE = 5 * 60 * 1000; // 5 minutes
|
|
7
7
|
// Initial State
|
|
@@ -145,11 +145,12 @@ function emailReducer(state, action) {
|
|
|
145
145
|
const EmailContext = createContext(undefined);
|
|
146
146
|
// Email Provider Component
|
|
147
147
|
export function EmailProvider({ children }) {
|
|
148
|
-
const
|
|
148
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
149
149
|
const [state, dispatch] = useReducer(emailReducer, initialState);
|
|
150
150
|
// Memoize on stable identity — useSession() returns a new object each render, so an
|
|
151
151
|
// unmemoized api would churn every useCallback([api]) and trigger repeated fetches.
|
|
152
152
|
const sessionIdentity = `${session?.user?.CompanyID ?? ''}:${session?.user?.UserID ?? ''}`;
|
|
153
|
+
// The adapter carries the next-auth session as AuthSessionProvider received it, so it has the shape the email API reads.
|
|
153
154
|
const api = useMemo(() => createEmailAPI(session), [sessionIdentity]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
154
155
|
// Initialize email system when session is available
|
|
155
156
|
// NOTE: Initialization is now manual - call actions.fetchEmails() when needed
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
4
4
|
import { HubConnectionBuilder, HubConnectionState, LogLevel } from '@microsoft/signalr';
|
|
5
|
-
import {
|
|
5
|
+
import { useOptionalSessionAdapter } from './SessionAdapterContext.js';
|
|
6
6
|
import { MessagingAPI } from '../lib/api/activity/messaging/messaging.js';
|
|
7
7
|
import { parseReactions } from '../lib/api/activity/messaging/messaging.js';
|
|
8
8
|
import { getBasicAuthHeader } from '../lib/api/core/config.js';
|
|
@@ -192,7 +192,7 @@ function messagingReducer(state, action) {
|
|
|
192
192
|
const MessagingContext = createContext(undefined);
|
|
193
193
|
// ─── Provider ───────────────────────────────────────────────────────────────
|
|
194
194
|
export function MessagingProvider({ children }) {
|
|
195
|
-
const
|
|
195
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
196
196
|
const [state, dispatch] = useReducer(messagingReducer, initialState);
|
|
197
197
|
const stateRef = useRef(state);
|
|
198
198
|
stateRef.current = state;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { getDynamicNavigation } from '../lib/api/index.js';
|
|
5
5
|
import { getModuleRoutePath } from '../lib/utils.js';
|
|
6
|
+
import { useOptionalSessionAdapter } from './SessionAdapterContext.js';
|
|
6
7
|
/**
|
|
7
8
|
* Filter grouped modules to only those with MobileSupported = 1/true.
|
|
8
9
|
* Returns grouped structure with empty groups removed.
|
|
@@ -20,11 +21,25 @@ export function NavigationProvider({ children, companyId, userId }) {
|
|
|
20
21
|
const [groupedModules, setGroupedModules] = React.useState([]);
|
|
21
22
|
const [loading, setLoading] = React.useState(true);
|
|
22
23
|
const [error, setError] = React.useState(null);
|
|
24
|
+
// The menu API needs the signed-in session (company credentials). Read from the session adapter,
|
|
25
|
+
// not next-auth, so the library does not require next-auth; held in a ref so a fresh session
|
|
26
|
+
// object does not refetch the menu, only the session arriving or leaving does.
|
|
27
|
+
const sessionAdapter = useOptionalSessionAdapter();
|
|
28
|
+
const sessionRef = React.useRef(sessionAdapter?.data ?? null);
|
|
29
|
+
sessionRef.current = sessionAdapter?.data ?? null;
|
|
30
|
+
const hasSession = Boolean(sessionAdapter?.data);
|
|
31
|
+
const sessionStatus = sessionAdapter?.status ?? 'unauthenticated';
|
|
23
32
|
const fetchNavigation = React.useCallback(async () => {
|
|
24
33
|
if (!companyId || !userId) {
|
|
25
34
|
setLoading(false);
|
|
26
35
|
return;
|
|
27
36
|
}
|
|
37
|
+
// No session yet: wait for it (this runs again when it arrives); none at all: no menu.
|
|
38
|
+
if (!hasSession) {
|
|
39
|
+
if (sessionStatus !== 'loading')
|
|
40
|
+
setLoading(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
28
43
|
const cacheKey = `navigation_${companyId}_${userId}`;
|
|
29
44
|
const cacheTimeKey = `${cacheKey}_time`;
|
|
30
45
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
@@ -41,7 +56,7 @@ export function NavigationProvider({ children, companyId, userId }) {
|
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
setLoading(true);
|
|
44
|
-
const navigation = await getDynamicNavigation(companyId, userId);
|
|
59
|
+
const navigation = await getDynamicNavigation(companyId, userId, sessionRef.current);
|
|
45
60
|
setGroupedModules(navigation);
|
|
46
61
|
localStorage.setItem(cacheKey, JSON.stringify(navigation));
|
|
47
62
|
localStorage.setItem(cacheTimeKey, Date.now().toString());
|
|
@@ -53,7 +68,7 @@ export function NavigationProvider({ children, companyId, userId }) {
|
|
|
53
68
|
finally {
|
|
54
69
|
setLoading(false);
|
|
55
70
|
}
|
|
56
|
-
}, [companyId, userId]);
|
|
71
|
+
}, [companyId, userId, hasSession, sessionStatus]);
|
|
57
72
|
React.useEffect(() => {
|
|
58
73
|
fetchNavigation();
|
|
59
74
|
}, [fetchNavigation]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useOptionalSessionAdapter } from './SessionAdapterContext.js';
|
|
5
5
|
import { NotificationsAPI } from '../lib/api/activity/index.js';
|
|
6
6
|
import { fromBackendNotification } from '../lib/api/activity/notifications/types.js';
|
|
7
7
|
import { useSignalR } from '../hooks/useSignalR.js';
|
|
@@ -11,7 +11,7 @@ const NotificationContext = createContext(undefined);
|
|
|
11
11
|
* app. Consumers read shared state via `useNotifications()` instead of each mounting their own.
|
|
12
12
|
*/
|
|
13
13
|
export function NotificationProvider({ children }) {
|
|
14
|
-
const
|
|
14
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
15
15
|
const [notifications, setNotifications] = useState([]);
|
|
16
16
|
const [notificationsLoading, setNotificationsLoading] = useState(false);
|
|
17
17
|
const [showNotificationDropdown, setShowNotificationDropdown] = useState(false);
|
|
@@ -35,5 +35,10 @@ export interface SessionAdapterProviderProps {
|
|
|
35
35
|
children: ReactNode;
|
|
36
36
|
}
|
|
37
37
|
export declare function SessionAdapterProvider({ user, status, signOut, children }: SessionAdapterProviderProps): import("react").JSX.Element;
|
|
38
|
+
/**
|
|
39
|
+
* The session when a provider is present, null when not: for components that also render in apps
|
|
40
|
+
* without one, such as the dynamic sidebar, which simply loads no menu then.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useOptionalSessionAdapter(): SessionAdapterValue | null;
|
|
38
43
|
/** Drop-in replacement for next-auth's `useSession()` inside library components. */
|
|
39
44
|
export declare function useSessionAdapter(): SessionAdapterValue;
|
|
@@ -10,6 +10,13 @@ export function SessionAdapterProvider({ user, status, signOut, children }) {
|
|
|
10
10
|
}), [user, status, signOut]);
|
|
11
11
|
return _jsx(SessionAdapterContext.Provider, { value: value, children: children });
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The session when a provider is present, null when not: for components that also render in apps
|
|
15
|
+
* without one, such as the dynamic sidebar, which simply loads no menu then.
|
|
16
|
+
*/
|
|
17
|
+
export function useOptionalSessionAdapter() {
|
|
18
|
+
return useContext(SessionAdapterContext);
|
|
19
|
+
}
|
|
13
20
|
/** Drop-in replacement for next-auth's `useSession()` inside library components. */
|
|
14
21
|
export function useSessionAdapter() {
|
|
15
22
|
const ctx = useContext(SessionAdapterContext);
|
package/dist/hooks/index.js
CHANGED
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
import { useState, useEffect, useCallback } from 'react';
|
|
23
|
-
import {
|
|
23
|
+
import { useOptionalSessionAdapter } from '../contexts/SessionAdapterContext.js';
|
|
24
24
|
import { ApprovalAPI } from '../lib/api/approval/index.js';
|
|
25
25
|
/**
|
|
26
26
|
* useApproval Hook
|
|
27
27
|
*/
|
|
28
28
|
export function useApproval() {
|
|
29
|
-
const
|
|
29
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
30
30
|
// State
|
|
31
31
|
const [pendingApprovals, setPendingApprovals] = useState([]);
|
|
32
32
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
import { useState, useCallback } from 'react';
|
|
17
|
-
import {
|
|
17
|
+
import { useOptionalSessionAdapter } from '../contexts/SessionAdapterContext.js';
|
|
18
18
|
import { BusinessAuditAPI } from '../lib/api/activity/audit/index.js';
|
|
19
19
|
export function useBusinessAudit() {
|
|
20
|
-
const
|
|
20
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
21
21
|
const [isLogging, setIsLogging] = useState(false);
|
|
22
22
|
const logAction = useCallback(async (action, module, details) => {
|
|
23
23
|
if (!session)
|
package/dist/hooks/useDraft.js
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
import { useState, useEffect, useCallback } from 'react';
|
|
22
|
-
import {
|
|
22
|
+
import { useOptionalSessionAdapter } from '../contexts/SessionAdapterContext.js';
|
|
23
23
|
import { DraftAPI } from '../lib/api/draft/index.js';
|
|
24
24
|
/**
|
|
25
25
|
* useDraft Hook - Milestone Based
|
|
26
26
|
*/
|
|
27
27
|
export function useDraft(module, currentState) {
|
|
28
|
-
const
|
|
28
|
+
const session = useOptionalSessionAdapter()?.data ?? null;
|
|
29
29
|
// State
|
|
30
30
|
const [drafts, setDrafts] = useState([]);
|
|
31
31
|
const [isLoadingDrafts, setIsLoadingDrafts] = useState(false);
|
package/dist/index.js
CHANGED
package/dist/lib/api/index.js
CHANGED
|
@@ -18,9 +18,13 @@ export interface GroupedModule {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Fetch dynamic menu modules from external API
|
|
21
|
-
* Now uses APIClient for proper authentication with company credentials
|
|
21
|
+
* Now uses APIClient for proper authentication with company credentials.
|
|
22
|
+
*
|
|
23
|
+
* The caller passes the session (from useSessionAdapter, which AuthSessionProvider fills from
|
|
24
|
+
* next-auth) rather than this module calling next-auth's getSession: importing next-auth here made
|
|
25
|
+
* every app installing indas-ui install next-auth too, since nearly every entry reaches this file.
|
|
22
26
|
*/
|
|
23
|
-
export declare function fetchDynamicMenuModules(companyId: number, userId: number): Promise<DynamicModule[]>;
|
|
27
|
+
export declare function fetchDynamicMenuModules(companyId: number, userId: number, session?: unknown): Promise<DynamicModule[]>;
|
|
24
28
|
/**
|
|
25
29
|
* Group modules by ModuleHeadName and sort by ModuleDisplayOrder
|
|
26
30
|
* Now supports hierarchical sub-modules
|
|
@@ -29,4 +33,4 @@ export declare function groupAndSortModules(modules: DynamicModule[]): GroupedMo
|
|
|
29
33
|
/**
|
|
30
34
|
* Get dynamic navigation structure for sidebar
|
|
31
35
|
*/
|
|
32
|
-
export declare function getDynamicNavigation(companyId?: number, userId?: number): Promise<GroupedModule[]>;
|
|
36
|
+
export declare function getDynamicNavigation(companyId?: number, userId?: number, session?: unknown): Promise<GroupedModule[]>;
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
// Dynamic Menu API
|
|
3
3
|
// Handles dynamic menu and navigation
|
|
4
4
|
import APIClient from '../core/client.js';
|
|
5
|
-
import { getSession } from 'next-auth/react';
|
|
6
5
|
/**
|
|
7
6
|
* Fetch dynamic menu modules from external API
|
|
8
|
-
* Now uses APIClient for proper authentication with company credentials
|
|
7
|
+
* Now uses APIClient for proper authentication with company credentials.
|
|
8
|
+
*
|
|
9
|
+
* The caller passes the session (from useSessionAdapter, which AuthSessionProvider fills from
|
|
10
|
+
* next-auth) rather than this module calling next-auth's getSession: importing next-auth here made
|
|
11
|
+
* every app installing indas-ui install next-auth too, since nearly every entry reaches this file.
|
|
9
12
|
*/
|
|
10
|
-
export async function fetchDynamicMenuModules(companyId, userId) {
|
|
13
|
+
export async function fetchDynamicMenuModules(companyId, userId, session) {
|
|
11
14
|
try {
|
|
12
|
-
// Get session to pass to APIClient for company credentials
|
|
13
|
-
const session = await getSession();
|
|
14
15
|
if (!session) {
|
|
15
16
|
return [];
|
|
16
17
|
}
|
|
@@ -119,10 +120,10 @@ export function groupAndSortModules(modules) {
|
|
|
119
120
|
/**
|
|
120
121
|
* Get dynamic navigation structure for sidebar
|
|
121
122
|
*/
|
|
122
|
-
export async function getDynamicNavigation(companyId, userId) {
|
|
123
|
+
export async function getDynamicNavigation(companyId, userId, session) {
|
|
123
124
|
if (!companyId || !userId) {
|
|
124
125
|
return [];
|
|
125
126
|
}
|
|
126
|
-
const modules = await fetchDynamicMenuModules(companyId, userId);
|
|
127
|
+
const modules = await fetchDynamicMenuModules(companyId, userId, session);
|
|
127
128
|
return groupAndSortModules(modules);
|
|
128
129
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How every number in the library is written. Indian grouping by default (20,00,000, not
|
|
3
|
+
* 2,000,000): the products built on this library keep Indian books, and a bare `toLocaleString()`
|
|
4
|
+
* follows whatever locale the browser happens to be set to, so the same figure read differently on
|
|
5
|
+
* two machines.
|
|
6
|
+
*
|
|
7
|
+
* A product that needs another convention calls `setNumberLocale()` once at start-up.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setNumberLocale(locale: string): void;
|
|
10
|
+
export declare function getNumberLocale(): string;
|
|
11
|
+
/** A full number with grouping, up to two decimals. Empty for null, undefined or NaN. */
|
|
12
|
+
export declare function formatNumber(value: number | string | null | undefined, maximumFractionDigits?: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* A short number for chart axes, where a full figure runs into its neighbour: 12,500 · 20 L ·
|
|
15
|
+
* 1.4 Cr in Indian grouping; 12.5K · 2M · 1.4B otherwise. Below a lakh (or a thousand) it is the
|
|
16
|
+
* full number.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatCompactNumber(value: number | string | null | undefined): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* How every number in the library is written. Indian grouping by default (20,00,000, not
|
|
4
|
+
* 2,000,000): the products built on this library keep Indian books, and a bare `toLocaleString()`
|
|
5
|
+
* follows whatever locale the browser happens to be set to, so the same figure read differently on
|
|
6
|
+
* two machines.
|
|
7
|
+
*
|
|
8
|
+
* A product that needs another convention calls `setNumberLocale()` once at start-up.
|
|
9
|
+
*/
|
|
10
|
+
let numberLocale = 'en-IN';
|
|
11
|
+
export function setNumberLocale(locale) {
|
|
12
|
+
numberLocale = locale;
|
|
13
|
+
}
|
|
14
|
+
export function getNumberLocale() {
|
|
15
|
+
return numberLocale;
|
|
16
|
+
}
|
|
17
|
+
/** A full number with grouping, up to two decimals. Empty for null, undefined or NaN. */
|
|
18
|
+
export function formatNumber(value, maximumFractionDigits = 2) {
|
|
19
|
+
if (value == null || value === '')
|
|
20
|
+
return '';
|
|
21
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
22
|
+
if (!Number.isFinite(n))
|
|
23
|
+
return typeof value === 'string' ? value : '';
|
|
24
|
+
return n.toLocaleString(numberLocale, { maximumFractionDigits });
|
|
25
|
+
}
|
|
26
|
+
const trim = (n) => n.toFixed(n >= 100 ? 0 : n >= 10 ? 1 : 2).replace(/\.?0+$/, '');
|
|
27
|
+
/**
|
|
28
|
+
* A short number for chart axes, where a full figure runs into its neighbour: 12,500 · 20 L ·
|
|
29
|
+
* 1.4 Cr in Indian grouping; 12.5K · 2M · 1.4B otherwise. Below a lakh (or a thousand) it is the
|
|
30
|
+
* full number.
|
|
31
|
+
*/
|
|
32
|
+
export function formatCompactNumber(value) {
|
|
33
|
+
if (value == null || value === '')
|
|
34
|
+
return '';
|
|
35
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
36
|
+
if (!Number.isFinite(n))
|
|
37
|
+
return String(value);
|
|
38
|
+
const sign = n < 0 ? '-' : '';
|
|
39
|
+
const a = Math.abs(n);
|
|
40
|
+
if (numberLocale === 'en-IN') {
|
|
41
|
+
if (a >= 1e7)
|
|
42
|
+
return `${sign}${trim(a / 1e7)} Cr`;
|
|
43
|
+
if (a >= 1e5)
|
|
44
|
+
return `${sign}${trim(a / 1e5)} L`;
|
|
45
|
+
return formatNumber(n, 0);
|
|
46
|
+
}
|
|
47
|
+
if (a >= 1e9)
|
|
48
|
+
return `${sign}${trim(a / 1e9)}B`;
|
|
49
|
+
if (a >= 1e6)
|
|
50
|
+
return `${sign}${trim(a / 1e6)}M`;
|
|
51
|
+
if (a >= 1e3)
|
|
52
|
+
return `${sign}${trim(a / 1e3)}K`;
|
|
53
|
+
return formatNumber(n, 0);
|
|
54
|
+
}
|