@optifye/dashboard-core 6.11.36 → 6.11.38

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/index.js CHANGED
@@ -58812,6 +58812,50 @@ var UserUsageStats = ({
58812
58812
  ] })
58813
58813
  ] });
58814
58814
  };
58815
+
58816
+ // src/lib/utils/teamUsage.ts
58817
+ var ISO_DATE_KEY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
58818
+ function formatLocalDateKey(date) {
58819
+ const year = date.getFullYear();
58820
+ const month = String(date.getMonth() + 1).padStart(2, "0");
58821
+ const day = String(date.getDate()).padStart(2, "0");
58822
+ return `${year}-${month}-${day}`;
58823
+ }
58824
+ function parseUsageDate(dateLike) {
58825
+ if (ISO_DATE_KEY_REGEX.test(dateLike)) {
58826
+ const [year, month, day] = dateLike.split("-").map(Number);
58827
+ return new Date(year, month - 1, day);
58828
+ }
58829
+ return new Date(dateLike);
58830
+ }
58831
+ function normalizeUsageDateKey(dateLike) {
58832
+ if (!dateLike) return void 0;
58833
+ if (ISO_DATE_KEY_REGEX.test(dateLike)) return dateLike;
58834
+ const parsed = parseUsageDate(dateLike);
58835
+ if (Number.isNaN(parsed.getTime())) {
58836
+ return dateLike.split("T")[0] || dateLike;
58837
+ }
58838
+ return formatLocalDateKey(parsed);
58839
+ }
58840
+ function getCurrentWeekToDateUsageRange(today = /* @__PURE__ */ new Date()) {
58841
+ const normalizedToday = new Date(today);
58842
+ normalizedToday.setHours(0, 0, 0, 0);
58843
+ const dayOfWeek = normalizedToday.getDay();
58844
+ const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
58845
+ const monday = new Date(normalizedToday);
58846
+ monday.setDate(normalizedToday.getDate() - daysSinceMonday);
58847
+ return {
58848
+ startDate: formatLocalDateKey(monday),
58849
+ endDate: formatLocalDateKey(normalizedToday),
58850
+ daysElapsed: daysSinceMonday + 1
58851
+ };
58852
+ }
58853
+ function calculateAverageDailyDuration(durationMs, dayCount) {
58854
+ if (!Number.isFinite(durationMs) || dayCount <= 0) {
58855
+ return 0;
58856
+ }
58857
+ return Math.round(durationMs / dayCount);
58858
+ }
58815
58859
  var UserUsageDetailModal = ({
58816
58860
  userId,
58817
58861
  userName,
@@ -58821,7 +58865,7 @@ var UserUsageDetailModal = ({
58821
58865
  endDate
58822
58866
  }) => {
58823
58867
  const [currentWeekStart, setCurrentWeekStart] = React143.useState(() => {
58824
- if (startDate) return new Date(startDate);
58868
+ if (startDate) return parseUsageDate(startDate);
58825
58869
  const { start } = getCurrentWeekRange();
58826
58870
  return start;
58827
58871
  });
@@ -58831,8 +58875,8 @@ var UserUsageDetailModal = ({
58831
58875
  return end;
58832
58876
  }, [currentWeekStart]);
58833
58877
  const { data, isLoading, error } = useUserUsage(userId, {
58834
- startDate: currentWeekStart.toISOString().slice(0, 10),
58835
- endDate: currentWeekEnd.toISOString().slice(0, 10)
58878
+ startDate: formatLocalDateKey(currentWeekStart),
58879
+ endDate: formatLocalDateKey(currentWeekEnd)
58836
58880
  });
58837
58881
  const handlePrevWeek = () => {
58838
58882
  setCurrentWeekStart((prev) => {
@@ -58925,8 +58969,8 @@ function UsageContent({
58925
58969
  } else {
58926
58970
  daysToCount = 7;
58927
58971
  }
58928
- const weekTotalMs = weekData.reduce((sum, d) => sum + d.total_duration_ms, 0);
58929
- const weekAvgMs = daysToCount > 0 ? weekTotalMs / daysToCount : 0;
58972
+ const weekActiveMs = weekData.reduce((sum, d) => sum + d.active_duration_ms, 0);
58973
+ const weekAvgMs = calculateAverageDailyDuration(weekActiveMs, daysToCount);
58930
58974
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
58931
58975
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 bg-gray-50 rounded-lg p-1 border border-gray-100", children: [
58932
58976
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -58938,7 +58982,7 @@ function UsageContent({
58938
58982
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "w-4 h-4" })
58939
58983
  }
58940
58984
  ),
58941
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 min-w-[140px] text-center", children: formatDateRange(currentWeekStart.toISOString().slice(0, 10), currentWeekEnd.toISOString().slice(0, 10)) }),
58985
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 min-w-[140px] text-center", children: formatDateRange(formatLocalDateKey(currentWeekStart), formatLocalDateKey(currentWeekEnd)) }),
58942
58986
  /* @__PURE__ */ jsxRuntime.jsx(
58943
58987
  "button",
58944
58988
  {
@@ -58970,7 +59014,7 @@ function DailyBarChart({
58970
59014
  const weekData = React143.useMemo(() => buildWeekData(dailyData, getDateStringsInRange(weekStart, weekEnd)), [dailyData, weekStart, weekEnd]);
58971
59015
  const chartData = React143.useMemo(() => {
58972
59016
  return weekData.map((day, index) => {
58973
- const date = new Date(day.date);
59017
+ const date = parseUsageDate(day.date);
58974
59018
  return {
58975
59019
  ...day,
58976
59020
  index,
@@ -58980,7 +59024,7 @@ function DailyBarChart({
58980
59024
  // green-500
58981
59025
  passiveColor: "#fbbf24",
58982
59026
  // amber-400
58983
- isToday: day.date === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
59027
+ isToday: day.date === formatLocalDateKey(/* @__PURE__ */ new Date())
58984
59028
  };
58985
59029
  });
58986
59030
  }, [weekData]);
@@ -59000,7 +59044,7 @@ function DailyBarChart({
59000
59044
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
59001
59045
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 rounded-full bg-gray-300" }),
59002
59046
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium text-gray-600", children: [
59003
- "Average Daily usage: ",
59047
+ "Average Daily active usage: ",
59004
59048
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900 font-semibold", children: avgDailyMs > 0 ? formatDuration2(avgDailyMs) : "0m" })
59005
59049
  ] })
59006
59050
  ] }),
@@ -59165,17 +59209,14 @@ function getCurrentWeekRange() {
59165
59209
  function getDateStringsInRange(start, end) {
59166
59210
  const result = [];
59167
59211
  for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
59168
- const year = d.getFullYear();
59169
- const month = String(d.getMonth() + 1).padStart(2, "0");
59170
- const day = String(d.getDate()).padStart(2, "0");
59171
- result.push(`${year}-${month}-${day}`);
59212
+ result.push(formatLocalDateKey(d));
59172
59213
  }
59173
59214
  return result;
59174
59215
  }
59175
59216
  function buildWeekData(dailyData, weekDates) {
59176
59217
  const dataMap = new Map(
59177
59218
  (dailyData || []).map((d) => {
59178
- const key = normalizeDateKey(d.date);
59219
+ const key = normalizeUsageDateKey(d.date);
59179
59220
  return [key, { ...d, date: key }];
59180
59221
  })
59181
59222
  );
@@ -59190,22 +59231,10 @@ function buildWeekData(dailyData, weekDates) {
59190
59231
  };
59191
59232
  });
59192
59233
  }
59193
- function normalizeDateKey(dateLike) {
59194
- if (!dateLike) return void 0;
59195
- try {
59196
- const date = new Date(dateLike);
59197
- const year = date.getFullYear();
59198
- const month = String(date.getMonth() + 1).padStart(2, "0");
59199
- const day = String(date.getDate()).padStart(2, "0");
59200
- return `${year}-${month}-${day}`;
59201
- } catch {
59202
- return dateLike.split("T")[0] || dateLike;
59203
- }
59204
- }
59205
59234
  function formatDateRange(startDate, endDate) {
59206
59235
  try {
59207
- const start = new Date(startDate);
59208
- const end = new Date(endDate);
59236
+ const start = parseUsageDate(startDate);
59237
+ const end = parseUsageDate(endDate);
59209
59238
  const opts = { month: "short", day: "numeric" };
59210
59239
  const yearOpts = { year: "numeric" };
59211
59240
  return `${start.toLocaleDateString("en-US", opts)} - ${end.toLocaleDateString("en-US", opts)}, ${end.toLocaleDateString("en-US", yearOpts)}`;
@@ -59216,7 +59245,7 @@ function formatDateRange(startDate, endDate) {
59216
59245
  function formatDate(dateStr) {
59217
59246
  if (!dateStr) return "";
59218
59247
  try {
59219
- const date = new Date(dateStr);
59248
+ const date = parseUsageDate(dateStr);
59220
59249
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
59221
59250
  } catch {
59222
59251
  return dateStr;
@@ -59494,7 +59523,7 @@ var UserManagementTable = ({
59494
59523
  }
59495
59524
  ),
59496
59525
  /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Assignments" }),
59497
- showUsageStats && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Average Daily usage" }),
59526
+ showUsageStats && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Average Daily Active Usage" }),
59498
59527
  /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Actions" })
59499
59528
  ] }) }),
59500
59529
  /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "bg-white divide-y divide-gray-200", children: filteredAndSortedUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: showUsageStats ? 5 : 4, className: "px-6 py-12", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center text-gray-400 gap-2", children: [
@@ -73791,39 +73820,8 @@ var TeamManagementView = ({
73791
73820
  const [users, setUsers] = React143.useState([]);
73792
73821
  const [availableLines, setAvailableLines] = React143.useState([]);
73793
73822
  const [availableFactories, setAvailableFactories] = React143.useState([]);
73794
- const [stats, setStats] = React143.useState({
73795
- totalUsers: 0,
73796
- owners: 0,
73797
- it: 0,
73798
- plantHeads: 0,
73799
- industrialEngineers: 0,
73800
- supervisors: 0,
73801
- optifye: 0
73802
- });
73823
+ const [usageSummaryByUser, setUsageSummaryByUser] = React143.useState({});
73803
73824
  const [isAddUserDialogOpen, setIsAddUserDialogOpen] = React143.useState(false);
73804
- const normalizeIds = React143.useCallback((value) => {
73805
- if (Array.isArray(value)) {
73806
- return value.filter((id3) => typeof id3 === "string" && id3.length > 0);
73807
- }
73808
- if (typeof value === "string" && value.length > 0) {
73809
- return [value];
73810
- }
73811
- return [];
73812
- }, []);
73813
- const factoryScopedRoleFactoryIds = React143.useMemo(() => {
73814
- if (!isFactoryScopedRole(user?.role_level)) return [];
73815
- const scopedFactoryIds = normalizeIds(user?.access_scope?.factory_ids);
73816
- if (scopedFactoryIds.length > 0) {
73817
- return Array.from(new Set(scopedFactoryIds));
73818
- }
73819
- const propertyFactoryIds = normalizeIds(
73820
- user?.properties?.factory_ids ?? user?.properties?.factory_id
73821
- );
73822
- if (propertyFactoryIds.length > 0) {
73823
- return Array.from(new Set(propertyFactoryIds));
73824
- }
73825
- return entityConfig?.factoryId ? [entityConfig.factoryId] : [];
73826
- }, [user, entityConfig?.factoryId, normalizeIds]);
73827
73825
  const notifyScopeRefresh = React143.useCallback(() => {
73828
73826
  if (typeof window !== "undefined") {
73829
73827
  window.dispatchEvent(new Event("rbac:refresh-scope"));
@@ -73832,37 +73830,17 @@ var TeamManagementView = ({
73832
73830
  const canAddUsers = canRoleManageUsers(user?.role_level);
73833
73831
  const canViewUsageStats = canRoleViewUsageStats(user?.role_level);
73834
73832
  const pageCompanyId = entityConfig?.companyId || user?.properties?.company_id;
73835
- const companyIdForUsage = pageCompanyId;
73836
- const usageDateRange = React143.useMemo(() => {
73837
- const today = /* @__PURE__ */ new Date();
73838
- const dayOfWeek = today.getDay();
73839
- const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
73840
- const monday = new Date(today);
73841
- monday.setDate(today.getDate() - daysSinceMonday);
73842
- return {
73843
- startDate: monday.toISOString().slice(0, 10),
73844
- endDate: today.toISOString().slice(0, 10),
73845
- daysElapsed: daysSinceMonday + 1
73846
- // Include today
73847
- };
73848
- }, []);
73849
- const {
73850
- data: usageData,
73851
- isLoading: isUsageLoading
73852
- } = useCompanyUsersUsage(canViewUsageStats ? companyIdForUsage : void 0, {
73853
- startDate: usageDateRange.startDate,
73854
- endDate: usageDateRange.endDate
73855
- });
73833
+ const usageDateRange = React143.useMemo(() => getCurrentWeekToDateUsageRange(), []);
73856
73834
  const avgDailyUsageMap = React143.useMemo(() => {
73857
- if (!usageData?.users) {
73835
+ if (!canViewUsageStats) {
73858
73836
  return {};
73859
73837
  }
73860
73838
  const daysElapsed = usageDateRange.daysElapsed;
73861
- return usageData.users.reduce((acc, user2) => {
73862
- acc[user2.user_id] = Math.round(user2.total_duration_ms / daysElapsed);
73839
+ return Object.entries(usageSummaryByUser).reduce((acc, [userId, usage]) => {
73840
+ acc[userId] = calculateAverageDailyDuration(usage.active_duration_ms, daysElapsed);
73863
73841
  return acc;
73864
73842
  }, {});
73865
- }, [usageData, usageDateRange.daysElapsed]);
73843
+ }, [canViewUsageStats, usageSummaryByUser, usageDateRange.daysElapsed]);
73866
73844
  const pageTitle = "Team Management";
73867
73845
  const pageDescription = canViewUsageStats ? "Manage team members and view their daily usage" : "Manage supervisors in your factory";
73868
73846
  const hasAccess = user ? user.role_level === void 0 || canRoleAccessTeamManagement(user.role_level) : false;
@@ -73886,7 +73864,6 @@ var TeamManagementView = ({
73886
73864
  setIsLoading(true);
73887
73865
  setError(void 0);
73888
73866
  try {
73889
- const userManagementService = createUserManagementService(supabase);
73890
73867
  const { data: { session } } = await supabase.auth.getSession();
73891
73868
  if (!session?.access_token) {
73892
73869
  throw new Error("No authentication token available");
@@ -73896,116 +73873,31 @@ var TeamManagementView = ({
73896
73873
  if (!backendUrl) {
73897
73874
  throw new Error("Backend URL is not configured. Please set NEXT_PUBLIC_BACKEND_URL in your environment.");
73898
73875
  }
73899
- if ((user?.role_level === "optifye" || user?.role_level === "owner" || user?.role_level === "it") && companyId) {
73900
- const { data: factories } = await supabase.from("factories").select("id, factory_name, company_id").eq("company_id", companyId).order("factory_name");
73901
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73876
+ const params = new URLSearchParams({
73877
+ start_date: usageDateRange.startDate,
73878
+ end_date: usageDateRange.endDate
73879
+ });
73880
+ const bootstrapResponse = await fetch(
73881
+ `${backendUrl}/api/users/company/${companyId}/bootstrap?${params.toString()}`,
73882
+ {
73902
73883
  headers: {
73903
73884
  "Authorization": `Bearer ${token}`,
73904
73885
  "Content-Type": "application/json"
73905
73886
  }
73906
- });
73907
- if (!linesResponse.ok) {
73908
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73909
- }
73910
- const linesData = await linesResponse.json();
73911
- const lines = linesData.lines || [];
73912
- setAvailableFactories(factories || []);
73913
- setAvailableLines(lines);
73914
- console.log("[TeamManagementView] Company-scoped team view - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
73915
- } else if (isFactoryScopedRole(user?.role_level)) {
73916
- if (factoryScopedRoleFactoryIds.length > 0) {
73917
- if (companyId) {
73918
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73919
- headers: {
73920
- "Authorization": `Bearer ${token}`,
73921
- "Content-Type": "application/json"
73922
- }
73923
- });
73924
- if (!linesResponse.ok) {
73925
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73926
- }
73927
- const linesData = await linesResponse.json();
73928
- const allLines = linesData.lines || [];
73929
- const lines = allLines.filter((line) => factoryScopedRoleFactoryIds.includes(line.factory_id));
73930
- setAvailableLines(lines);
73931
- console.log("[TeamManagementView] Factory-scoped team view - Factories:", factoryScopedRoleFactoryIds, "Loaded lines:", lines?.length);
73932
- }
73933
- } else if (entityConfig?.factoryId) {
73934
- const linesResponse = await fetch(`${backendUrl}/api/lines?factory_id=${entityConfig.factoryId}`, {
73935
- headers: {
73936
- "Authorization": `Bearer ${token}`,
73937
- "Content-Type": "application/json"
73938
- }
73939
- });
73940
- if (!linesResponse.ok) {
73941
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73942
- }
73943
- const linesData = await linesResponse.json();
73944
- const lines = linesData.lines || [];
73945
- setAvailableLines(lines);
73946
- console.log("[TeamManagementView] Plant Head - Using entityConfig factory:", entityConfig.factoryId, "Loaded lines:", lines?.length);
73947
- } else {
73948
- setAvailableLines([]);
73949
- console.warn("[TeamManagementView] Plant Head has no factory assignments");
73950
73887
  }
73951
- setAvailableFactories([]);
73952
- } else if (companyId) {
73953
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73954
- headers: {
73955
- "Authorization": `Bearer ${token}`,
73956
- "Content-Type": "application/json"
73957
- }
73958
- });
73959
- if (!linesResponse.ok) {
73960
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73961
- }
73962
- const linesData = await linesResponse.json();
73963
- const lines = linesData.lines || [];
73964
- setAvailableLines(lines);
73965
- setAvailableFactories([]);
73966
- console.log("[TeamManagementView] Fallback - Company:", companyId, "Loaded lines:", lines?.length);
73967
- }
73968
- const usersPromise = isFactoryScopedRole(user?.role_level) ? (async () => {
73969
- if (factoryScopedRoleFactoryIds.length === 0) {
73970
- return [];
73971
- }
73972
- const results = await Promise.allSettled(
73973
- factoryScopedRoleFactoryIds.map((factoryId) => userManagementService.getFactoryUsers(factoryId))
73974
- );
73975
- const successful = results.filter(
73976
- (result) => result.status === "fulfilled"
73977
- ).flatMap((result) => result.value || []);
73978
- if (successful.length > 0) {
73979
- const byUserId = /* @__PURE__ */ new Map();
73980
- successful.forEach((u) => {
73981
- if (u?.user_id) {
73982
- byUserId.set(u.user_id, u);
73983
- }
73984
- });
73985
- return Array.from(byUserId.values());
73986
- }
73987
- const firstRejected = results.find(
73988
- (result) => result.status === "rejected"
73989
- );
73990
- if (firstRejected) {
73991
- throw firstRejected.reason;
73992
- }
73993
- return [];
73994
- })() : userManagementService.getCompanyUsers(companyId);
73995
- const [usersData, userStats] = await Promise.all([
73996
- usersPromise,
73997
- userManagementService.getUserStats(companyId)
73998
- ]);
73999
- setUsers(usersData);
74000
- setStats({
74001
- totalUsers: userStats.total,
74002
- owners: userStats.owners,
74003
- it: userStats.it,
74004
- plantHeads: userStats.plant_heads,
74005
- industrialEngineers: userStats.industrial_engineers,
74006
- supervisors: userStats.supervisors,
74007
- optifye: 0
74008
- });
73888
+ );
73889
+ if (!bootstrapResponse.ok) {
73890
+ const errorData = await bootstrapResponse.json().catch(() => ({}));
73891
+ throw new Error(errorData.detail || `Failed to fetch team bootstrap: ${bootstrapResponse.statusText}`);
73892
+ }
73893
+ const bootstrapData = await bootstrapResponse.json();
73894
+ setAvailableFactories((bootstrapData.factories || []).map((factory) => ({
73895
+ id: factory.id,
73896
+ factory_name: factory.factory_name
73897
+ })));
73898
+ setAvailableLines(bootstrapData.lines || []);
73899
+ setUsers(bootstrapData.users || []);
73900
+ setUsageSummaryByUser(bootstrapData.usage_summary_by_user || {});
74009
73901
  } catch (err) {
74010
73902
  console.error("Error loading team management data:", err);
74011
73903
  setError(err instanceof Error ? err.message : "Failed to load data");
@@ -74013,7 +73905,7 @@ var TeamManagementView = ({
74013
73905
  } finally {
74014
73906
  setIsLoading(false);
74015
73907
  }
74016
- }, [supabase, user, pageCompanyId, entityConfig?.factoryId, factoryScopedRoleFactoryIds]);
73908
+ }, [supabase, user, pageCompanyId, usageDateRange.endDate, usageDateRange.startDate]);
74017
73909
  React143.useEffect(() => {
74018
73910
  const companyId = pageCompanyId;
74019
73911
  const canLoad = hasAccess && user && !!companyId;
@@ -74194,7 +74086,7 @@ var TeamManagementView = ({
74194
74086
  availableLines,
74195
74087
  availableFactories,
74196
74088
  avgDailyUsage: avgDailyUsageMap,
74197
- isUsageLoading,
74089
+ isUsageLoading: false,
74198
74090
  showUsageStats: canViewUsageStats
74199
74091
  }
74200
74092
  ) }),
@@ -79017,6 +78909,12 @@ var buildDeltaBadge = (delta, options) => {
79017
78909
  text: `${options.formatter(delta)} vs ${options.comparisonLabel}`
79018
78910
  };
79019
78911
  };
78912
+ var parseSpecificShiftId = (shiftMode) => {
78913
+ const rawMode = String(shiftMode || "").trim().toLowerCase();
78914
+ if (!rawMode.startsWith("shift:")) return null;
78915
+ const parsed = Number(rawMode.split(":", 2)[1]);
78916
+ return Number.isFinite(parsed) ? parsed : null;
78917
+ };
79020
78918
  var normalizeShiftLabel = (shiftName, shiftMode) => {
79021
78919
  if (shiftMode === "all") {
79022
78920
  return "All Shifts";
@@ -79028,6 +78926,10 @@ var normalizeShiftLabel = (shiftName, shiftMode) => {
79028
78926
  if (normalizedName === "night") return "Night Shift";
79029
78927
  return /shift/i.test(trimmedName) ? trimmedName : `${trimmedName} Shift`;
79030
78928
  }
78929
+ const specificShiftId = parseSpecificShiftId(shiftMode);
78930
+ if (specificShiftId !== null) {
78931
+ return `Shift ${specificShiftId}`;
78932
+ }
79031
78933
  if (shiftMode === "night") return "Night Shift";
79032
78934
  return "Day Shift";
79033
78935
  };
@@ -79100,6 +79002,7 @@ var OperationsOverviewHeader = React143__namespace.default.memo(({
79100
79002
  dateRange,
79101
79003
  displayDateRange,
79102
79004
  trendMode,
79005
+ shiftFilterOptions,
79103
79006
  isLiveScope,
79104
79007
  liveShiftName,
79105
79008
  lineOptions,
@@ -79353,7 +79256,7 @@ var OperationsOverviewHeader = React143__namespace.default.memo(({
79353
79256
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
79354
79257
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
79355
79258
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-gray-500 uppercase tracking-wide ml-1", children: "Shift" }),
79356
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsxs(
79259
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(
79357
79260
  "select",
79358
79261
  {
79359
79262
  value: trendMode,
@@ -79365,11 +79268,7 @@ var OperationsOverviewHeader = React143__namespace.default.memo(({
79365
79268
  backgroundRepeat: "no-repeat",
79366
79269
  backgroundSize: "1.2em 1.2em"
79367
79270
  },
79368
- children: [
79369
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Shifts" }),
79370
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "day", children: "Day Shift" }),
79371
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "night", children: "Night Shift" })
79372
- ]
79271
+ children: shiftFilterOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option.value, children: option.label }, option.value))
79373
79272
  }
79374
79273
  ) })
79375
79274
  ] }),
@@ -80439,6 +80338,24 @@ var normalizeShiftId = (value) => {
80439
80338
  }
80440
80339
  return null;
80441
80340
  };
80341
+ var buildSpecificShiftMode = (value) => {
80342
+ const normalizedShiftId = normalizeShiftId(value);
80343
+ return normalizedShiftId === null ? null : `shift:${normalizedShiftId}`;
80344
+ };
80345
+ var parseSpecificShiftMode = (value) => {
80346
+ const rawValue = String(value || "").trim().toLowerCase();
80347
+ if (!rawValue.startsWith("shift:")) return null;
80348
+ const parsed = Number(rawValue.split(":", 2)[1]);
80349
+ return Number.isFinite(parsed) ? parsed : null;
80350
+ };
80351
+ var formatShiftOptionLabel = (shiftName, shiftId) => {
80352
+ const trimmedName = shiftName?.trim();
80353
+ if (trimmedName) {
80354
+ return /shift/i.test(trimmedName) ? trimmedName : `${trimmedName} Shift`;
80355
+ }
80356
+ const normalizedShiftId = normalizeShiftId(shiftId);
80357
+ return normalizedShiftId === null ? "Unknown Shift" : `Shift ${normalizedShiftId}`;
80358
+ };
80442
80359
  var classifyShiftBucket = ({
80443
80360
  shiftName,
80444
80361
  shiftId,
@@ -80525,6 +80442,26 @@ var normalizeShiftWindowMinutes = (startTime, endTime) => {
80525
80442
  }
80526
80443
  return { startMinutes, endMinutes };
80527
80444
  };
80445
+ var getOperationalSortStartMinutes = (startTime, endTime) => {
80446
+ const normalizedWindow = normalizeShiftWindowMinutes(startTime, endTime);
80447
+ if (!normalizedWindow) return null;
80448
+ return normalizedWindow.startMinutes < 6 * 60 ? normalizedWindow.startMinutes + 24 * 60 : normalizedWindow.startMinutes;
80449
+ };
80450
+ var doesShiftMatchTrendMode = ({
80451
+ trendMode,
80452
+ shiftName,
80453
+ shiftId,
80454
+ startTime,
80455
+ endTime
80456
+ }) => {
80457
+ if (trendMode === "all") return true;
80458
+ const specificShiftId = parseSpecificShiftMode(trendMode);
80459
+ if (specificShiftId !== null) {
80460
+ return normalizeShiftId(shiftId) === specificShiftId;
80461
+ }
80462
+ const bucket = classifyShiftBucket({ shiftName, shiftId, startTime, endTime });
80463
+ return bucket === trendMode;
80464
+ };
80528
80465
  var PlantHeadView = () => {
80529
80466
  const supabase = useSupabase();
80530
80467
  const entityConfig = useEntityConfig();
@@ -80655,6 +80592,45 @@ var PlantHeadView = () => {
80655
80592
  shiftConfigMap,
80656
80593
  isLoading: isShiftConfigLoading
80657
80594
  } = useMultiLineShiftConfigs(scopedLineIds, staticShiftConfig);
80595
+ const shiftFilterOptions = React143__namespace.default.useMemo(() => {
80596
+ const optionsById = /* @__PURE__ */ new Map();
80597
+ scopedLineIds.forEach((lineId) => {
80598
+ const shiftConfig = shiftConfigMap.get(lineId) || staticShiftConfig;
80599
+ getShiftWindowsForConfig(shiftConfig).forEach((shift) => {
80600
+ const shiftId = normalizeShiftId(shift.shiftId);
80601
+ if (shiftId === null) return;
80602
+ const sortKey = getOperationalSortStartMinutes(shift.startTime, shift.endTime) ?? 24 * 60 + shiftId;
80603
+ const existing = optionsById.get(shiftId);
80604
+ const label = formatShiftOptionLabel(shift.shiftName, shiftId);
80605
+ if (!existing) {
80606
+ optionsById.set(shiftId, {
80607
+ value: buildSpecificShiftMode(shiftId),
80608
+ label,
80609
+ shiftId,
80610
+ shiftName: shift.shiftName || null,
80611
+ startTime: shift.startTime || null,
80612
+ endTime: shift.endTime || null,
80613
+ sortKey
80614
+ });
80615
+ return;
80616
+ }
80617
+ if ((!existing.shiftName || existing.shiftName === `Shift ${shiftId}`) && shift.shiftName) {
80618
+ existing.shiftName = shift.shiftName;
80619
+ existing.label = label;
80620
+ }
80621
+ if (sortKey < existing.sortKey) {
80622
+ existing.sortKey = sortKey;
80623
+ existing.startTime = shift.startTime || null;
80624
+ existing.endTime = shift.endTime || null;
80625
+ }
80626
+ });
80627
+ });
80628
+ const dynamicOptions = Array.from(optionsById.values()).sort((a, b) => a.sortKey - b.sortKey || (a.shiftId || 0) - (b.shiftId || 0)).map(({ sortKey, ...option }) => option);
80629
+ return [
80630
+ { value: "all", label: "All Shifts" },
80631
+ ...dynamicOptions
80632
+ ];
80633
+ }, [appTimezone, scopedLineIds, shiftConfigMap, staticShiftConfig]);
80658
80634
  React143__namespace.default.useEffect(() => {
80659
80635
  if (scopedLineIds.length === 0 || isShiftConfigLoading) {
80660
80636
  return;
@@ -80717,18 +80693,20 @@ var PlantHeadView = () => {
80717
80693
  if (!activeShift) {
80718
80694
  return [];
80719
80695
  }
80720
- const trendBucket = classifyShiftBucket({
80696
+ const bucketTrendMode = classifyShiftBucket({
80721
80697
  shiftName: activeShift.shiftName,
80722
80698
  shiftId: activeShift.shiftId,
80723
80699
  startTime: activeShift.startTime,
80724
80700
  endTime: activeShift.endTime
80725
80701
  });
80726
- if (!trendBucket || trendBucket === "all") {
80702
+ const exactTrendMode = buildSpecificShiftMode(activeShift.shiftId);
80703
+ if (!bucketTrendMode && !exactTrendMode) {
80727
80704
  return [];
80728
80705
  }
80729
80706
  return [{
80730
80707
  lineId,
80731
- trendMode: trendBucket,
80708
+ exactTrendMode,
80709
+ bucketTrendMode,
80732
80710
  shiftId: activeShift.shiftId,
80733
80711
  shiftName: activeShift.shiftName || null,
80734
80712
  startTime: activeShift.startTime || null,
@@ -80737,14 +80715,12 @@ var PlantHeadView = () => {
80737
80715
  }];
80738
80716
  });
80739
80717
  }, [appTimezone, scopedLineIds, shiftConfigMap, shiftResolutionNow, staticShiftConfig]);
80740
- const hasActiveDayShiftLine = React143__namespace.default.useMemo(
80741
- () => activeLineShiftStates.some((shift) => shift.trendMode === "day" && shift.date === resolvedOperationalToday),
80742
- [activeLineShiftStates, resolvedOperationalToday]
80743
- );
80744
- const hasActiveNightShiftLine = React143__namespace.default.useMemo(
80745
- () => activeLineShiftStates.some((shift) => shift.trendMode === "night" && shift.date === resolvedOperationalToday),
80746
- [activeLineShiftStates, resolvedOperationalToday]
80747
- );
80718
+ const uniformActiveTrendMode = React143__namespace.default.useMemo(() => {
80719
+ const activeModes = Array.from(new Set(
80720
+ activeLineShiftStates.filter((shift) => shift.date === resolvedOperationalToday).map((shift) => shift.exactTrendMode).filter((mode) => !!mode)
80721
+ ));
80722
+ return activeModes.length === 1 ? activeModes[0] : null;
80723
+ }, [activeLineShiftStates, resolvedOperationalToday]);
80748
80724
  const resolvedTrendMode = isInitialScopeReady ? trendMode : "all";
80749
80725
  const hourlyWindowStartTime = React143__namespace.default.useMemo(() => {
80750
80726
  if (scopedLineIds.length === 0) {
@@ -80755,13 +80731,19 @@ var PlantHeadView = () => {
80755
80731
  scopedLineIds.forEach((lineId) => {
80756
80732
  const shiftConfig = shiftConfigMap.get(lineId) || staticShiftConfig;
80757
80733
  getShiftWindowsForConfig(shiftConfig).forEach((shift) => {
80758
- const bucket = classifyShiftBucket({
80734
+ classifyShiftBucket({
80759
80735
  shiftName: shift.shiftName,
80760
80736
  shiftId: shift.shiftId,
80761
80737
  startTime: shift.startTime,
80762
80738
  endTime: shift.endTime
80763
80739
  });
80764
- if (resolvedTrendMode !== "all" && bucket !== resolvedTrendMode) {
80740
+ if (!doesShiftMatchTrendMode({
80741
+ trendMode: resolvedTrendMode,
80742
+ shiftName: shift.shiftName,
80743
+ shiftId: shift.shiftId,
80744
+ startTime: shift.startTime,
80745
+ endTime: shift.endTime
80746
+ })) {
80765
80747
  return;
80766
80748
  }
80767
80749
  const normalizedWindow = normalizeShiftWindowMinutes(shift.startTime, shift.endTime);
@@ -80835,11 +80817,11 @@ var PlantHeadView = () => {
80835
80817
  endKey: nextStartKey
80836
80818
  };
80837
80819
  });
80838
- setTrendMode("all");
80820
+ setTrendMode(uniformActiveTrendMode || "all");
80839
80821
  setUsesThisWeekComparison(false);
80840
80822
  hasAutoInitializedScopeRef.current = true;
80841
80823
  setIsInitialScopeReady(true);
80842
- }, [fallbackOperationalDate, isShiftScopeResolved, resolvedOperationalToday, scopedLineIds.length]);
80824
+ }, [fallbackOperationalDate, isShiftScopeResolved, resolvedOperationalToday, scopedLineIds.length, uniformActiveTrendMode]);
80843
80825
  const handleDateRangeChange = React143__namespace.default.useCallback((range, meta) => {
80844
80826
  hasUserAdjustedScopeRef.current = true;
80845
80827
  setIsInitialScopeReady(true);
@@ -80926,6 +80908,33 @@ var PlantHeadView = () => {
80926
80908
  () => resolvedTrendMode,
80927
80909
  [resolvedTrendMode]
80928
80910
  );
80911
+ const hasActiveSelectedShiftLine = React143__namespace.default.useMemo(
80912
+ () => activeLineShiftStates.some((shift) => {
80913
+ if (shift.date !== resolvedOperationalToday) return false;
80914
+ if (effectiveTrendMode === "all") return true;
80915
+ const specificShiftId = parseSpecificShiftMode(effectiveTrendMode);
80916
+ if (specificShiftId !== null) {
80917
+ return shift.exactTrendMode === effectiveTrendMode;
80918
+ }
80919
+ return shift.bucketTrendMode === effectiveTrendMode;
80920
+ }),
80921
+ [activeLineShiftStates, effectiveTrendMode, resolvedOperationalToday]
80922
+ );
80923
+ const activeLiveShiftName = React143__namespace.default.useMemo(
80924
+ () => {
80925
+ if (effectiveTrendMode === "all") return null;
80926
+ const matchingShift = activeLineShiftStates.find((shift) => {
80927
+ if (shift.date !== resolvedOperationalToday) return false;
80928
+ const specificShiftId = parseSpecificShiftMode(effectiveTrendMode);
80929
+ if (specificShiftId !== null) {
80930
+ return shift.exactTrendMode === effectiveTrendMode;
80931
+ }
80932
+ return shift.bucketTrendMode === effectiveTrendMode;
80933
+ });
80934
+ return matchingShift?.shiftName || null;
80935
+ },
80936
+ [activeLineShiftStates, effectiveTrendMode, resolvedOperationalToday]
80937
+ );
80929
80938
  const hourlyLabelStartTime = React143__namespace.default.useMemo(() => {
80930
80939
  if (scopedLineIds.length === 0) {
80931
80940
  return null;
@@ -80937,12 +80946,11 @@ var PlantHeadView = () => {
80937
80946
  [effectiveDateRange.endKey, effectiveDateRange.startKey]
80938
80947
  );
80939
80948
  const isLiveScope = React143__namespace.default.useMemo(
80940
- () => isSingleDayScope && effectiveDateRange.startKey === resolvedOperationalToday && (effectiveTrendMode === "all" || effectiveTrendMode === "day" && hasActiveDayShiftLine || effectiveTrendMode === "night" && hasActiveNightShiftLine),
80949
+ () => isSingleDayScope && effectiveDateRange.startKey === resolvedOperationalToday && hasActiveSelectedShiftLine,
80941
80950
  [
80942
80951
  effectiveDateRange.startKey,
80943
80952
  effectiveTrendMode,
80944
- hasActiveDayShiftLine,
80945
- hasActiveNightShiftLine,
80953
+ hasActiveSelectedShiftLine,
80946
80954
  isSingleDayScope,
80947
80955
  resolvedOperationalToday
80948
80956
  ]
@@ -80979,8 +80987,9 @@ var PlantHeadView = () => {
80979
80987
  dateRange,
80980
80988
  displayDateRange: headerDateRange,
80981
80989
  trendMode,
80990
+ shiftFilterOptions,
80982
80991
  isLiveScope,
80983
- liveShiftName: isLiveScope && trendMode !== "all" ? trendMode : null,
80992
+ liveShiftName: isLiveScope && trendMode !== "all" ? activeLiveShiftName : null,
80984
80993
  lineOptions,
80985
80994
  supervisorOptions,
80986
80995
  selectedSupervisorId,