@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.mjs CHANGED
@@ -58783,6 +58783,50 @@ var UserUsageStats = ({
58783
58783
  ] })
58784
58784
  ] });
58785
58785
  };
58786
+
58787
+ // src/lib/utils/teamUsage.ts
58788
+ var ISO_DATE_KEY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
58789
+ function formatLocalDateKey(date) {
58790
+ const year = date.getFullYear();
58791
+ const month = String(date.getMonth() + 1).padStart(2, "0");
58792
+ const day = String(date.getDate()).padStart(2, "0");
58793
+ return `${year}-${month}-${day}`;
58794
+ }
58795
+ function parseUsageDate(dateLike) {
58796
+ if (ISO_DATE_KEY_REGEX.test(dateLike)) {
58797
+ const [year, month, day] = dateLike.split("-").map(Number);
58798
+ return new Date(year, month - 1, day);
58799
+ }
58800
+ return new Date(dateLike);
58801
+ }
58802
+ function normalizeUsageDateKey(dateLike) {
58803
+ if (!dateLike) return void 0;
58804
+ if (ISO_DATE_KEY_REGEX.test(dateLike)) return dateLike;
58805
+ const parsed = parseUsageDate(dateLike);
58806
+ if (Number.isNaN(parsed.getTime())) {
58807
+ return dateLike.split("T")[0] || dateLike;
58808
+ }
58809
+ return formatLocalDateKey(parsed);
58810
+ }
58811
+ function getCurrentWeekToDateUsageRange(today = /* @__PURE__ */ new Date()) {
58812
+ const normalizedToday = new Date(today);
58813
+ normalizedToday.setHours(0, 0, 0, 0);
58814
+ const dayOfWeek = normalizedToday.getDay();
58815
+ const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
58816
+ const monday = new Date(normalizedToday);
58817
+ monday.setDate(normalizedToday.getDate() - daysSinceMonday);
58818
+ return {
58819
+ startDate: formatLocalDateKey(monday),
58820
+ endDate: formatLocalDateKey(normalizedToday),
58821
+ daysElapsed: daysSinceMonday + 1
58822
+ };
58823
+ }
58824
+ function calculateAverageDailyDuration(durationMs, dayCount) {
58825
+ if (!Number.isFinite(durationMs) || dayCount <= 0) {
58826
+ return 0;
58827
+ }
58828
+ return Math.round(durationMs / dayCount);
58829
+ }
58786
58830
  var UserUsageDetailModal = ({
58787
58831
  userId,
58788
58832
  userName,
@@ -58792,7 +58836,7 @@ var UserUsageDetailModal = ({
58792
58836
  endDate
58793
58837
  }) => {
58794
58838
  const [currentWeekStart, setCurrentWeekStart] = useState(() => {
58795
- if (startDate) return new Date(startDate);
58839
+ if (startDate) return parseUsageDate(startDate);
58796
58840
  const { start } = getCurrentWeekRange();
58797
58841
  return start;
58798
58842
  });
@@ -58802,8 +58846,8 @@ var UserUsageDetailModal = ({
58802
58846
  return end;
58803
58847
  }, [currentWeekStart]);
58804
58848
  const { data, isLoading, error } = useUserUsage(userId, {
58805
- startDate: currentWeekStart.toISOString().slice(0, 10),
58806
- endDate: currentWeekEnd.toISOString().slice(0, 10)
58849
+ startDate: formatLocalDateKey(currentWeekStart),
58850
+ endDate: formatLocalDateKey(currentWeekEnd)
58807
58851
  });
58808
58852
  const handlePrevWeek = () => {
58809
58853
  setCurrentWeekStart((prev) => {
@@ -58896,8 +58940,8 @@ function UsageContent({
58896
58940
  } else {
58897
58941
  daysToCount = 7;
58898
58942
  }
58899
- const weekTotalMs = weekData.reduce((sum, d) => sum + d.total_duration_ms, 0);
58900
- const weekAvgMs = daysToCount > 0 ? weekTotalMs / daysToCount : 0;
58943
+ const weekActiveMs = weekData.reduce((sum, d) => sum + d.active_duration_ms, 0);
58944
+ const weekAvgMs = calculateAverageDailyDuration(weekActiveMs, daysToCount);
58901
58945
  return /* @__PURE__ */ jsxs("div", { children: [
58902
58946
  /* @__PURE__ */ jsx("div", { className: "flex justify-center mb-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 bg-gray-50 rounded-lg p-1 border border-gray-100", children: [
58903
58947
  /* @__PURE__ */ jsx(
@@ -58909,7 +58953,7 @@ function UsageContent({
58909
58953
  children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-4 h-4" })
58910
58954
  }
58911
58955
  ),
58912
- /* @__PURE__ */ 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)) }),
58956
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-700 min-w-[140px] text-center", children: formatDateRange(formatLocalDateKey(currentWeekStart), formatLocalDateKey(currentWeekEnd)) }),
58913
58957
  /* @__PURE__ */ jsx(
58914
58958
  "button",
58915
58959
  {
@@ -58941,7 +58985,7 @@ function DailyBarChart({
58941
58985
  const weekData = useMemo(() => buildWeekData(dailyData, getDateStringsInRange(weekStart, weekEnd)), [dailyData, weekStart, weekEnd]);
58942
58986
  const chartData = useMemo(() => {
58943
58987
  return weekData.map((day, index) => {
58944
- const date = new Date(day.date);
58988
+ const date = parseUsageDate(day.date);
58945
58989
  return {
58946
58990
  ...day,
58947
58991
  index,
@@ -58951,7 +58995,7 @@ function DailyBarChart({
58951
58995
  // green-500
58952
58996
  passiveColor: "#fbbf24",
58953
58997
  // amber-400
58954
- isToday: day.date === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
58998
+ isToday: day.date === formatLocalDateKey(/* @__PURE__ */ new Date())
58955
58999
  };
58956
59000
  });
58957
59001
  }, [weekData]);
@@ -58971,7 +59015,7 @@ function DailyBarChart({
58971
59015
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
58972
59016
  /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-gray-300" }),
58973
59017
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-gray-600", children: [
58974
- "Average Daily usage: ",
59018
+ "Average Daily active usage: ",
58975
59019
  /* @__PURE__ */ jsx("span", { className: "text-gray-900 font-semibold", children: avgDailyMs > 0 ? formatDuration2(avgDailyMs) : "0m" })
58976
59020
  ] })
58977
59021
  ] }),
@@ -59136,17 +59180,14 @@ function getCurrentWeekRange() {
59136
59180
  function getDateStringsInRange(start, end) {
59137
59181
  const result = [];
59138
59182
  for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
59139
- const year = d.getFullYear();
59140
- const month = String(d.getMonth() + 1).padStart(2, "0");
59141
- const day = String(d.getDate()).padStart(2, "0");
59142
- result.push(`${year}-${month}-${day}`);
59183
+ result.push(formatLocalDateKey(d));
59143
59184
  }
59144
59185
  return result;
59145
59186
  }
59146
59187
  function buildWeekData(dailyData, weekDates) {
59147
59188
  const dataMap = new Map(
59148
59189
  (dailyData || []).map((d) => {
59149
- const key = normalizeDateKey(d.date);
59190
+ const key = normalizeUsageDateKey(d.date);
59150
59191
  return [key, { ...d, date: key }];
59151
59192
  })
59152
59193
  );
@@ -59161,22 +59202,10 @@ function buildWeekData(dailyData, weekDates) {
59161
59202
  };
59162
59203
  });
59163
59204
  }
59164
- function normalizeDateKey(dateLike) {
59165
- if (!dateLike) return void 0;
59166
- try {
59167
- const date = new Date(dateLike);
59168
- const year = date.getFullYear();
59169
- const month = String(date.getMonth() + 1).padStart(2, "0");
59170
- const day = String(date.getDate()).padStart(2, "0");
59171
- return `${year}-${month}-${day}`;
59172
- } catch {
59173
- return dateLike.split("T")[0] || dateLike;
59174
- }
59175
- }
59176
59205
  function formatDateRange(startDate, endDate) {
59177
59206
  try {
59178
- const start = new Date(startDate);
59179
- const end = new Date(endDate);
59207
+ const start = parseUsageDate(startDate);
59208
+ const end = parseUsageDate(endDate);
59180
59209
  const opts = { month: "short", day: "numeric" };
59181
59210
  const yearOpts = { year: "numeric" };
59182
59211
  return `${start.toLocaleDateString("en-US", opts)} - ${end.toLocaleDateString("en-US", opts)}, ${end.toLocaleDateString("en-US", yearOpts)}`;
@@ -59187,7 +59216,7 @@ function formatDateRange(startDate, endDate) {
59187
59216
  function formatDate(dateStr) {
59188
59217
  if (!dateStr) return "";
59189
59218
  try {
59190
- const date = new Date(dateStr);
59219
+ const date = parseUsageDate(dateStr);
59191
59220
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
59192
59221
  } catch {
59193
59222
  return dateStr;
@@ -59465,7 +59494,7 @@ var UserManagementTable = ({
59465
59494
  }
59466
59495
  ),
59467
59496
  /* @__PURE__ */ jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Assignments" }),
59468
- showUsageStats && /* @__PURE__ */ jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Average Daily usage" }),
59497
+ showUsageStats && /* @__PURE__ */ jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Average Daily Active Usage" }),
59469
59498
  /* @__PURE__ */ jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider", children: "Actions" })
59470
59499
  ] }) }),
59471
59500
  /* @__PURE__ */ jsx("tbody", { className: "bg-white divide-y divide-gray-200", children: filteredAndSortedUsers.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: showUsageStats ? 5 : 4, className: "px-6 py-12", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center text-gray-400 gap-2", children: [
@@ -73762,39 +73791,8 @@ var TeamManagementView = ({
73762
73791
  const [users, setUsers] = useState([]);
73763
73792
  const [availableLines, setAvailableLines] = useState([]);
73764
73793
  const [availableFactories, setAvailableFactories] = useState([]);
73765
- const [stats, setStats] = useState({
73766
- totalUsers: 0,
73767
- owners: 0,
73768
- it: 0,
73769
- plantHeads: 0,
73770
- industrialEngineers: 0,
73771
- supervisors: 0,
73772
- optifye: 0
73773
- });
73794
+ const [usageSummaryByUser, setUsageSummaryByUser] = useState({});
73774
73795
  const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false);
73775
- const normalizeIds = useCallback((value) => {
73776
- if (Array.isArray(value)) {
73777
- return value.filter((id3) => typeof id3 === "string" && id3.length > 0);
73778
- }
73779
- if (typeof value === "string" && value.length > 0) {
73780
- return [value];
73781
- }
73782
- return [];
73783
- }, []);
73784
- const factoryScopedRoleFactoryIds = useMemo(() => {
73785
- if (!isFactoryScopedRole(user?.role_level)) return [];
73786
- const scopedFactoryIds = normalizeIds(user?.access_scope?.factory_ids);
73787
- if (scopedFactoryIds.length > 0) {
73788
- return Array.from(new Set(scopedFactoryIds));
73789
- }
73790
- const propertyFactoryIds = normalizeIds(
73791
- user?.properties?.factory_ids ?? user?.properties?.factory_id
73792
- );
73793
- if (propertyFactoryIds.length > 0) {
73794
- return Array.from(new Set(propertyFactoryIds));
73795
- }
73796
- return entityConfig?.factoryId ? [entityConfig.factoryId] : [];
73797
- }, [user, entityConfig?.factoryId, normalizeIds]);
73798
73796
  const notifyScopeRefresh = useCallback(() => {
73799
73797
  if (typeof window !== "undefined") {
73800
73798
  window.dispatchEvent(new Event("rbac:refresh-scope"));
@@ -73803,37 +73801,17 @@ var TeamManagementView = ({
73803
73801
  const canAddUsers = canRoleManageUsers(user?.role_level);
73804
73802
  const canViewUsageStats = canRoleViewUsageStats(user?.role_level);
73805
73803
  const pageCompanyId = entityConfig?.companyId || user?.properties?.company_id;
73806
- const companyIdForUsage = pageCompanyId;
73807
- const usageDateRange = useMemo(() => {
73808
- const today = /* @__PURE__ */ new Date();
73809
- const dayOfWeek = today.getDay();
73810
- const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
73811
- const monday = new Date(today);
73812
- monday.setDate(today.getDate() - daysSinceMonday);
73813
- return {
73814
- startDate: monday.toISOString().slice(0, 10),
73815
- endDate: today.toISOString().slice(0, 10),
73816
- daysElapsed: daysSinceMonday + 1
73817
- // Include today
73818
- };
73819
- }, []);
73820
- const {
73821
- data: usageData,
73822
- isLoading: isUsageLoading
73823
- } = useCompanyUsersUsage(canViewUsageStats ? companyIdForUsage : void 0, {
73824
- startDate: usageDateRange.startDate,
73825
- endDate: usageDateRange.endDate
73826
- });
73804
+ const usageDateRange = useMemo(() => getCurrentWeekToDateUsageRange(), []);
73827
73805
  const avgDailyUsageMap = useMemo(() => {
73828
- if (!usageData?.users) {
73806
+ if (!canViewUsageStats) {
73829
73807
  return {};
73830
73808
  }
73831
73809
  const daysElapsed = usageDateRange.daysElapsed;
73832
- return usageData.users.reduce((acc, user2) => {
73833
- acc[user2.user_id] = Math.round(user2.total_duration_ms / daysElapsed);
73810
+ return Object.entries(usageSummaryByUser).reduce((acc, [userId, usage]) => {
73811
+ acc[userId] = calculateAverageDailyDuration(usage.active_duration_ms, daysElapsed);
73834
73812
  return acc;
73835
73813
  }, {});
73836
- }, [usageData, usageDateRange.daysElapsed]);
73814
+ }, [canViewUsageStats, usageSummaryByUser, usageDateRange.daysElapsed]);
73837
73815
  const pageTitle = "Team Management";
73838
73816
  const pageDescription = canViewUsageStats ? "Manage team members and view their daily usage" : "Manage supervisors in your factory";
73839
73817
  const hasAccess = user ? user.role_level === void 0 || canRoleAccessTeamManagement(user.role_level) : false;
@@ -73857,7 +73835,6 @@ var TeamManagementView = ({
73857
73835
  setIsLoading(true);
73858
73836
  setError(void 0);
73859
73837
  try {
73860
- const userManagementService = createUserManagementService(supabase);
73861
73838
  const { data: { session } } = await supabase.auth.getSession();
73862
73839
  if (!session?.access_token) {
73863
73840
  throw new Error("No authentication token available");
@@ -73867,116 +73844,31 @@ var TeamManagementView = ({
73867
73844
  if (!backendUrl) {
73868
73845
  throw new Error("Backend URL is not configured. Please set NEXT_PUBLIC_BACKEND_URL in your environment.");
73869
73846
  }
73870
- if ((user?.role_level === "optifye" || user?.role_level === "owner" || user?.role_level === "it") && companyId) {
73871
- const { data: factories } = await supabase.from("factories").select("id, factory_name, company_id").eq("company_id", companyId).order("factory_name");
73872
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73847
+ const params = new URLSearchParams({
73848
+ start_date: usageDateRange.startDate,
73849
+ end_date: usageDateRange.endDate
73850
+ });
73851
+ const bootstrapResponse = await fetch(
73852
+ `${backendUrl}/api/users/company/${companyId}/bootstrap?${params.toString()}`,
73853
+ {
73873
73854
  headers: {
73874
73855
  "Authorization": `Bearer ${token}`,
73875
73856
  "Content-Type": "application/json"
73876
73857
  }
73877
- });
73878
- if (!linesResponse.ok) {
73879
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73880
- }
73881
- const linesData = await linesResponse.json();
73882
- const lines = linesData.lines || [];
73883
- setAvailableFactories(factories || []);
73884
- setAvailableLines(lines);
73885
- console.log("[TeamManagementView] Company-scoped team view - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
73886
- } else if (isFactoryScopedRole(user?.role_level)) {
73887
- if (factoryScopedRoleFactoryIds.length > 0) {
73888
- if (companyId) {
73889
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73890
- headers: {
73891
- "Authorization": `Bearer ${token}`,
73892
- "Content-Type": "application/json"
73893
- }
73894
- });
73895
- if (!linesResponse.ok) {
73896
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73897
- }
73898
- const linesData = await linesResponse.json();
73899
- const allLines = linesData.lines || [];
73900
- const lines = allLines.filter((line) => factoryScopedRoleFactoryIds.includes(line.factory_id));
73901
- setAvailableLines(lines);
73902
- console.log("[TeamManagementView] Factory-scoped team view - Factories:", factoryScopedRoleFactoryIds, "Loaded lines:", lines?.length);
73903
- }
73904
- } else if (entityConfig?.factoryId) {
73905
- const linesResponse = await fetch(`${backendUrl}/api/lines?factory_id=${entityConfig.factoryId}`, {
73906
- headers: {
73907
- "Authorization": `Bearer ${token}`,
73908
- "Content-Type": "application/json"
73909
- }
73910
- });
73911
- if (!linesResponse.ok) {
73912
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73913
- }
73914
- const linesData = await linesResponse.json();
73915
- const lines = linesData.lines || [];
73916
- setAvailableLines(lines);
73917
- console.log("[TeamManagementView] Plant Head - Using entityConfig factory:", entityConfig.factoryId, "Loaded lines:", lines?.length);
73918
- } else {
73919
- setAvailableLines([]);
73920
- console.warn("[TeamManagementView] Plant Head has no factory assignments");
73921
73858
  }
73922
- setAvailableFactories([]);
73923
- } else if (companyId) {
73924
- const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
73925
- headers: {
73926
- "Authorization": `Bearer ${token}`,
73927
- "Content-Type": "application/json"
73928
- }
73929
- });
73930
- if (!linesResponse.ok) {
73931
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
73932
- }
73933
- const linesData = await linesResponse.json();
73934
- const lines = linesData.lines || [];
73935
- setAvailableLines(lines);
73936
- setAvailableFactories([]);
73937
- console.log("[TeamManagementView] Fallback - Company:", companyId, "Loaded lines:", lines?.length);
73938
- }
73939
- const usersPromise = isFactoryScopedRole(user?.role_level) ? (async () => {
73940
- if (factoryScopedRoleFactoryIds.length === 0) {
73941
- return [];
73942
- }
73943
- const results = await Promise.allSettled(
73944
- factoryScopedRoleFactoryIds.map((factoryId) => userManagementService.getFactoryUsers(factoryId))
73945
- );
73946
- const successful = results.filter(
73947
- (result) => result.status === "fulfilled"
73948
- ).flatMap((result) => result.value || []);
73949
- if (successful.length > 0) {
73950
- const byUserId = /* @__PURE__ */ new Map();
73951
- successful.forEach((u) => {
73952
- if (u?.user_id) {
73953
- byUserId.set(u.user_id, u);
73954
- }
73955
- });
73956
- return Array.from(byUserId.values());
73957
- }
73958
- const firstRejected = results.find(
73959
- (result) => result.status === "rejected"
73960
- );
73961
- if (firstRejected) {
73962
- throw firstRejected.reason;
73963
- }
73964
- return [];
73965
- })() : userManagementService.getCompanyUsers(companyId);
73966
- const [usersData, userStats] = await Promise.all([
73967
- usersPromise,
73968
- userManagementService.getUserStats(companyId)
73969
- ]);
73970
- setUsers(usersData);
73971
- setStats({
73972
- totalUsers: userStats.total,
73973
- owners: userStats.owners,
73974
- it: userStats.it,
73975
- plantHeads: userStats.plant_heads,
73976
- industrialEngineers: userStats.industrial_engineers,
73977
- supervisors: userStats.supervisors,
73978
- optifye: 0
73979
- });
73859
+ );
73860
+ if (!bootstrapResponse.ok) {
73861
+ const errorData = await bootstrapResponse.json().catch(() => ({}));
73862
+ throw new Error(errorData.detail || `Failed to fetch team bootstrap: ${bootstrapResponse.statusText}`);
73863
+ }
73864
+ const bootstrapData = await bootstrapResponse.json();
73865
+ setAvailableFactories((bootstrapData.factories || []).map((factory) => ({
73866
+ id: factory.id,
73867
+ factory_name: factory.factory_name
73868
+ })));
73869
+ setAvailableLines(bootstrapData.lines || []);
73870
+ setUsers(bootstrapData.users || []);
73871
+ setUsageSummaryByUser(bootstrapData.usage_summary_by_user || {});
73980
73872
  } catch (err) {
73981
73873
  console.error("Error loading team management data:", err);
73982
73874
  setError(err instanceof Error ? err.message : "Failed to load data");
@@ -73984,7 +73876,7 @@ var TeamManagementView = ({
73984
73876
  } finally {
73985
73877
  setIsLoading(false);
73986
73878
  }
73987
- }, [supabase, user, pageCompanyId, entityConfig?.factoryId, factoryScopedRoleFactoryIds]);
73879
+ }, [supabase, user, pageCompanyId, usageDateRange.endDate, usageDateRange.startDate]);
73988
73880
  useEffect(() => {
73989
73881
  const companyId = pageCompanyId;
73990
73882
  const canLoad = hasAccess && user && !!companyId;
@@ -74165,7 +74057,7 @@ var TeamManagementView = ({
74165
74057
  availableLines,
74166
74058
  availableFactories,
74167
74059
  avgDailyUsage: avgDailyUsageMap,
74168
- isUsageLoading,
74060
+ isUsageLoading: false,
74169
74061
  showUsageStats: canViewUsageStats
74170
74062
  }
74171
74063
  ) }),
@@ -78988,6 +78880,12 @@ var buildDeltaBadge = (delta, options) => {
78988
78880
  text: `${options.formatter(delta)} vs ${options.comparisonLabel}`
78989
78881
  };
78990
78882
  };
78883
+ var parseSpecificShiftId = (shiftMode) => {
78884
+ const rawMode = String(shiftMode || "").trim().toLowerCase();
78885
+ if (!rawMode.startsWith("shift:")) return null;
78886
+ const parsed = Number(rawMode.split(":", 2)[1]);
78887
+ return Number.isFinite(parsed) ? parsed : null;
78888
+ };
78991
78889
  var normalizeShiftLabel = (shiftName, shiftMode) => {
78992
78890
  if (shiftMode === "all") {
78993
78891
  return "All Shifts";
@@ -78999,6 +78897,10 @@ var normalizeShiftLabel = (shiftName, shiftMode) => {
78999
78897
  if (normalizedName === "night") return "Night Shift";
79000
78898
  return /shift/i.test(trimmedName) ? trimmedName : `${trimmedName} Shift`;
79001
78899
  }
78900
+ const specificShiftId = parseSpecificShiftId(shiftMode);
78901
+ if (specificShiftId !== null) {
78902
+ return `Shift ${specificShiftId}`;
78903
+ }
79002
78904
  if (shiftMode === "night") return "Night Shift";
79003
78905
  return "Day Shift";
79004
78906
  };
@@ -79071,6 +78973,7 @@ var OperationsOverviewHeader = React143__default.memo(({
79071
78973
  dateRange,
79072
78974
  displayDateRange,
79073
78975
  trendMode,
78976
+ shiftFilterOptions,
79074
78977
  isLiveScope,
79075
78978
  liveShiftName,
79076
78979
  lineOptions,
@@ -79324,7 +79227,7 @@ var OperationsOverviewHeader = React143__default.memo(({
79324
79227
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
79325
79228
  /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
79326
79229
  /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-gray-500 uppercase tracking-wide ml-1", children: "Shift" }),
79327
- /* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsxs(
79230
+ /* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsx(
79328
79231
  "select",
79329
79232
  {
79330
79233
  value: trendMode,
@@ -79336,11 +79239,7 @@ var OperationsOverviewHeader = React143__default.memo(({
79336
79239
  backgroundRepeat: "no-repeat",
79337
79240
  backgroundSize: "1.2em 1.2em"
79338
79241
  },
79339
- children: [
79340
- /* @__PURE__ */ jsx("option", { value: "all", children: "All Shifts" }),
79341
- /* @__PURE__ */ jsx("option", { value: "day", children: "Day Shift" }),
79342
- /* @__PURE__ */ jsx("option", { value: "night", children: "Night Shift" })
79343
- ]
79242
+ children: shiftFilterOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
79344
79243
  }
79345
79244
  ) })
79346
79245
  ] }),
@@ -80410,6 +80309,24 @@ var normalizeShiftId = (value) => {
80410
80309
  }
80411
80310
  return null;
80412
80311
  };
80312
+ var buildSpecificShiftMode = (value) => {
80313
+ const normalizedShiftId = normalizeShiftId(value);
80314
+ return normalizedShiftId === null ? null : `shift:${normalizedShiftId}`;
80315
+ };
80316
+ var parseSpecificShiftMode = (value) => {
80317
+ const rawValue = String(value || "").trim().toLowerCase();
80318
+ if (!rawValue.startsWith("shift:")) return null;
80319
+ const parsed = Number(rawValue.split(":", 2)[1]);
80320
+ return Number.isFinite(parsed) ? parsed : null;
80321
+ };
80322
+ var formatShiftOptionLabel = (shiftName, shiftId) => {
80323
+ const trimmedName = shiftName?.trim();
80324
+ if (trimmedName) {
80325
+ return /shift/i.test(trimmedName) ? trimmedName : `${trimmedName} Shift`;
80326
+ }
80327
+ const normalizedShiftId = normalizeShiftId(shiftId);
80328
+ return normalizedShiftId === null ? "Unknown Shift" : `Shift ${normalizedShiftId}`;
80329
+ };
80413
80330
  var classifyShiftBucket = ({
80414
80331
  shiftName,
80415
80332
  shiftId,
@@ -80496,6 +80413,26 @@ var normalizeShiftWindowMinutes = (startTime, endTime) => {
80496
80413
  }
80497
80414
  return { startMinutes, endMinutes };
80498
80415
  };
80416
+ var getOperationalSortStartMinutes = (startTime, endTime) => {
80417
+ const normalizedWindow = normalizeShiftWindowMinutes(startTime, endTime);
80418
+ if (!normalizedWindow) return null;
80419
+ return normalizedWindow.startMinutes < 6 * 60 ? normalizedWindow.startMinutes + 24 * 60 : normalizedWindow.startMinutes;
80420
+ };
80421
+ var doesShiftMatchTrendMode = ({
80422
+ trendMode,
80423
+ shiftName,
80424
+ shiftId,
80425
+ startTime,
80426
+ endTime
80427
+ }) => {
80428
+ if (trendMode === "all") return true;
80429
+ const specificShiftId = parseSpecificShiftMode(trendMode);
80430
+ if (specificShiftId !== null) {
80431
+ return normalizeShiftId(shiftId) === specificShiftId;
80432
+ }
80433
+ const bucket = classifyShiftBucket({ shiftName, shiftId, startTime, endTime });
80434
+ return bucket === trendMode;
80435
+ };
80499
80436
  var PlantHeadView = () => {
80500
80437
  const supabase = useSupabase();
80501
80438
  const entityConfig = useEntityConfig();
@@ -80626,6 +80563,45 @@ var PlantHeadView = () => {
80626
80563
  shiftConfigMap,
80627
80564
  isLoading: isShiftConfigLoading
80628
80565
  } = useMultiLineShiftConfigs(scopedLineIds, staticShiftConfig);
80566
+ const shiftFilterOptions = React143__default.useMemo(() => {
80567
+ const optionsById = /* @__PURE__ */ new Map();
80568
+ scopedLineIds.forEach((lineId) => {
80569
+ const shiftConfig = shiftConfigMap.get(lineId) || staticShiftConfig;
80570
+ getShiftWindowsForConfig(shiftConfig).forEach((shift) => {
80571
+ const shiftId = normalizeShiftId(shift.shiftId);
80572
+ if (shiftId === null) return;
80573
+ const sortKey = getOperationalSortStartMinutes(shift.startTime, shift.endTime) ?? 24 * 60 + shiftId;
80574
+ const existing = optionsById.get(shiftId);
80575
+ const label = formatShiftOptionLabel(shift.shiftName, shiftId);
80576
+ if (!existing) {
80577
+ optionsById.set(shiftId, {
80578
+ value: buildSpecificShiftMode(shiftId),
80579
+ label,
80580
+ shiftId,
80581
+ shiftName: shift.shiftName || null,
80582
+ startTime: shift.startTime || null,
80583
+ endTime: shift.endTime || null,
80584
+ sortKey
80585
+ });
80586
+ return;
80587
+ }
80588
+ if ((!existing.shiftName || existing.shiftName === `Shift ${shiftId}`) && shift.shiftName) {
80589
+ existing.shiftName = shift.shiftName;
80590
+ existing.label = label;
80591
+ }
80592
+ if (sortKey < existing.sortKey) {
80593
+ existing.sortKey = sortKey;
80594
+ existing.startTime = shift.startTime || null;
80595
+ existing.endTime = shift.endTime || null;
80596
+ }
80597
+ });
80598
+ });
80599
+ const dynamicOptions = Array.from(optionsById.values()).sort((a, b) => a.sortKey - b.sortKey || (a.shiftId || 0) - (b.shiftId || 0)).map(({ sortKey, ...option }) => option);
80600
+ return [
80601
+ { value: "all", label: "All Shifts" },
80602
+ ...dynamicOptions
80603
+ ];
80604
+ }, [appTimezone, scopedLineIds, shiftConfigMap, staticShiftConfig]);
80629
80605
  React143__default.useEffect(() => {
80630
80606
  if (scopedLineIds.length === 0 || isShiftConfigLoading) {
80631
80607
  return;
@@ -80688,18 +80664,20 @@ var PlantHeadView = () => {
80688
80664
  if (!activeShift) {
80689
80665
  return [];
80690
80666
  }
80691
- const trendBucket = classifyShiftBucket({
80667
+ const bucketTrendMode = classifyShiftBucket({
80692
80668
  shiftName: activeShift.shiftName,
80693
80669
  shiftId: activeShift.shiftId,
80694
80670
  startTime: activeShift.startTime,
80695
80671
  endTime: activeShift.endTime
80696
80672
  });
80697
- if (!trendBucket || trendBucket === "all") {
80673
+ const exactTrendMode = buildSpecificShiftMode(activeShift.shiftId);
80674
+ if (!bucketTrendMode && !exactTrendMode) {
80698
80675
  return [];
80699
80676
  }
80700
80677
  return [{
80701
80678
  lineId,
80702
- trendMode: trendBucket,
80679
+ exactTrendMode,
80680
+ bucketTrendMode,
80703
80681
  shiftId: activeShift.shiftId,
80704
80682
  shiftName: activeShift.shiftName || null,
80705
80683
  startTime: activeShift.startTime || null,
@@ -80708,14 +80686,12 @@ var PlantHeadView = () => {
80708
80686
  }];
80709
80687
  });
80710
80688
  }, [appTimezone, scopedLineIds, shiftConfigMap, shiftResolutionNow, staticShiftConfig]);
80711
- const hasActiveDayShiftLine = React143__default.useMemo(
80712
- () => activeLineShiftStates.some((shift) => shift.trendMode === "day" && shift.date === resolvedOperationalToday),
80713
- [activeLineShiftStates, resolvedOperationalToday]
80714
- );
80715
- const hasActiveNightShiftLine = React143__default.useMemo(
80716
- () => activeLineShiftStates.some((shift) => shift.trendMode === "night" && shift.date === resolvedOperationalToday),
80717
- [activeLineShiftStates, resolvedOperationalToday]
80718
- );
80689
+ const uniformActiveTrendMode = React143__default.useMemo(() => {
80690
+ const activeModes = Array.from(new Set(
80691
+ activeLineShiftStates.filter((shift) => shift.date === resolvedOperationalToday).map((shift) => shift.exactTrendMode).filter((mode) => !!mode)
80692
+ ));
80693
+ return activeModes.length === 1 ? activeModes[0] : null;
80694
+ }, [activeLineShiftStates, resolvedOperationalToday]);
80719
80695
  const resolvedTrendMode = isInitialScopeReady ? trendMode : "all";
80720
80696
  const hourlyWindowStartTime = React143__default.useMemo(() => {
80721
80697
  if (scopedLineIds.length === 0) {
@@ -80726,13 +80702,19 @@ var PlantHeadView = () => {
80726
80702
  scopedLineIds.forEach((lineId) => {
80727
80703
  const shiftConfig = shiftConfigMap.get(lineId) || staticShiftConfig;
80728
80704
  getShiftWindowsForConfig(shiftConfig).forEach((shift) => {
80729
- const bucket = classifyShiftBucket({
80705
+ classifyShiftBucket({
80730
80706
  shiftName: shift.shiftName,
80731
80707
  shiftId: shift.shiftId,
80732
80708
  startTime: shift.startTime,
80733
80709
  endTime: shift.endTime
80734
80710
  });
80735
- if (resolvedTrendMode !== "all" && bucket !== resolvedTrendMode) {
80711
+ if (!doesShiftMatchTrendMode({
80712
+ trendMode: resolvedTrendMode,
80713
+ shiftName: shift.shiftName,
80714
+ shiftId: shift.shiftId,
80715
+ startTime: shift.startTime,
80716
+ endTime: shift.endTime
80717
+ })) {
80736
80718
  return;
80737
80719
  }
80738
80720
  const normalizedWindow = normalizeShiftWindowMinutes(shift.startTime, shift.endTime);
@@ -80806,11 +80788,11 @@ var PlantHeadView = () => {
80806
80788
  endKey: nextStartKey
80807
80789
  };
80808
80790
  });
80809
- setTrendMode("all");
80791
+ setTrendMode(uniformActiveTrendMode || "all");
80810
80792
  setUsesThisWeekComparison(false);
80811
80793
  hasAutoInitializedScopeRef.current = true;
80812
80794
  setIsInitialScopeReady(true);
80813
- }, [fallbackOperationalDate, isShiftScopeResolved, resolvedOperationalToday, scopedLineIds.length]);
80795
+ }, [fallbackOperationalDate, isShiftScopeResolved, resolvedOperationalToday, scopedLineIds.length, uniformActiveTrendMode]);
80814
80796
  const handleDateRangeChange = React143__default.useCallback((range, meta) => {
80815
80797
  hasUserAdjustedScopeRef.current = true;
80816
80798
  setIsInitialScopeReady(true);
@@ -80897,6 +80879,33 @@ var PlantHeadView = () => {
80897
80879
  () => resolvedTrendMode,
80898
80880
  [resolvedTrendMode]
80899
80881
  );
80882
+ const hasActiveSelectedShiftLine = React143__default.useMemo(
80883
+ () => activeLineShiftStates.some((shift) => {
80884
+ if (shift.date !== resolvedOperationalToday) return false;
80885
+ if (effectiveTrendMode === "all") return true;
80886
+ const specificShiftId = parseSpecificShiftMode(effectiveTrendMode);
80887
+ if (specificShiftId !== null) {
80888
+ return shift.exactTrendMode === effectiveTrendMode;
80889
+ }
80890
+ return shift.bucketTrendMode === effectiveTrendMode;
80891
+ }),
80892
+ [activeLineShiftStates, effectiveTrendMode, resolvedOperationalToday]
80893
+ );
80894
+ const activeLiveShiftName = React143__default.useMemo(
80895
+ () => {
80896
+ if (effectiveTrendMode === "all") return null;
80897
+ const matchingShift = activeLineShiftStates.find((shift) => {
80898
+ if (shift.date !== resolvedOperationalToday) return false;
80899
+ const specificShiftId = parseSpecificShiftMode(effectiveTrendMode);
80900
+ if (specificShiftId !== null) {
80901
+ return shift.exactTrendMode === effectiveTrendMode;
80902
+ }
80903
+ return shift.bucketTrendMode === effectiveTrendMode;
80904
+ });
80905
+ return matchingShift?.shiftName || null;
80906
+ },
80907
+ [activeLineShiftStates, effectiveTrendMode, resolvedOperationalToday]
80908
+ );
80900
80909
  const hourlyLabelStartTime = React143__default.useMemo(() => {
80901
80910
  if (scopedLineIds.length === 0) {
80902
80911
  return null;
@@ -80908,12 +80917,11 @@ var PlantHeadView = () => {
80908
80917
  [effectiveDateRange.endKey, effectiveDateRange.startKey]
80909
80918
  );
80910
80919
  const isLiveScope = React143__default.useMemo(
80911
- () => isSingleDayScope && effectiveDateRange.startKey === resolvedOperationalToday && (effectiveTrendMode === "all" || effectiveTrendMode === "day" && hasActiveDayShiftLine || effectiveTrendMode === "night" && hasActiveNightShiftLine),
80920
+ () => isSingleDayScope && effectiveDateRange.startKey === resolvedOperationalToday && hasActiveSelectedShiftLine,
80912
80921
  [
80913
80922
  effectiveDateRange.startKey,
80914
80923
  effectiveTrendMode,
80915
- hasActiveDayShiftLine,
80916
- hasActiveNightShiftLine,
80924
+ hasActiveSelectedShiftLine,
80917
80925
  isSingleDayScope,
80918
80926
  resolvedOperationalToday
80919
80927
  ]
@@ -80950,8 +80958,9 @@ var PlantHeadView = () => {
80950
80958
  dateRange,
80951
80959
  displayDateRange: headerDateRange,
80952
80960
  trendMode,
80961
+ shiftFilterOptions,
80953
80962
  isLiveScope,
80954
- liveShiftName: isLiveScope && trendMode !== "all" ? trendMode : null,
80963
+ liveShiftName: isLiveScope && trendMode !== "all" ? activeLiveShiftName : null,
80955
80964
  lineOptions,
80956
80965
  supervisorOptions,
80957
80966
  selectedSupervisorId,