@optifye/dashboard-core 6.12.49 → 6.12.51
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/{automation-B472r1h3.d.mts → automation-Bxp-JzQB.d.mts} +9 -1
- package/dist/{automation-B472r1h3.d.ts → automation-Bxp-JzQB.d.ts} +9 -1
- package/dist/automation.d.mts +1 -1
- package/dist/automation.d.ts +1 -1
- package/dist/index.css +26 -0
- package/dist/index.d.mts +120 -14
- package/dist/index.d.ts +120 -14
- package/dist/index.js +1519 -373
- package/dist/index.mjs +1520 -375
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6085,6 +6085,10 @@ var workspaceService = {
|
|
|
6085
6085
|
_workspaceCameraIpsInFlight: /* @__PURE__ */ new Map(),
|
|
6086
6086
|
_workspaceCameraIpsCacheExpiryMs: 5 * 60 * 1e3,
|
|
6087
6087
|
// 5 minutes cache
|
|
6088
|
+
// Cache for workspace bulb IP configs
|
|
6089
|
+
_workspaceLightConfigsCache: /* @__PURE__ */ new Map(),
|
|
6090
|
+
_workspaceLightConfigsInFlight: /* @__PURE__ */ new Map(),
|
|
6091
|
+
_workspaceLightConfigsCacheExpiryMs: 5 * 60 * 1e3,
|
|
6088
6092
|
async getWorkspaces(lineId, options) {
|
|
6089
6093
|
const enabledOnly = options?.enabledOnly ?? false;
|
|
6090
6094
|
const force = options?.force ?? false;
|
|
@@ -6262,6 +6266,61 @@ var workspaceService = {
|
|
|
6262
6266
|
this._workspaceCameraIpsInFlight.set(cacheKey, fetchPromise);
|
|
6263
6267
|
return fetchPromise;
|
|
6264
6268
|
},
|
|
6269
|
+
async getWorkspaceLightConfigs(params) {
|
|
6270
|
+
const workspaceIds = (params.workspaceIds || []).filter(Boolean);
|
|
6271
|
+
const lineIds = (params.lineIds || []).filter(Boolean);
|
|
6272
|
+
const force = params.force ?? false;
|
|
6273
|
+
if (!workspaceIds.length && !lineIds.length) {
|
|
6274
|
+
return {};
|
|
6275
|
+
}
|
|
6276
|
+
const workspaceKey = workspaceIds.slice().sort().join(",");
|
|
6277
|
+
const lineKey = lineIds.slice().sort().join(",");
|
|
6278
|
+
const cacheKey = workspaceKey ? `workspaces:${workspaceKey}` : `lines:${lineKey}`;
|
|
6279
|
+
const now4 = Date.now();
|
|
6280
|
+
const cached = this._workspaceLightConfigsCache.get(cacheKey);
|
|
6281
|
+
if (!force && cached && now4 - cached.timestamp < this._workspaceLightConfigsCacheExpiryMs) {
|
|
6282
|
+
return cached.lightConfigs;
|
|
6283
|
+
}
|
|
6284
|
+
const inFlight = this._workspaceLightConfigsInFlight.get(cacheKey);
|
|
6285
|
+
if (!force && inFlight) {
|
|
6286
|
+
return inFlight;
|
|
6287
|
+
}
|
|
6288
|
+
const fetchPromise = (async () => {
|
|
6289
|
+
try {
|
|
6290
|
+
const token = await getAuthToken2();
|
|
6291
|
+
const apiUrl = getBackendUrl2();
|
|
6292
|
+
const response = await fetch(`${apiUrl}/api/workspaces/light-configs`, {
|
|
6293
|
+
method: "POST",
|
|
6294
|
+
headers: {
|
|
6295
|
+
"Authorization": `Bearer ${token}`,
|
|
6296
|
+
"Content-Type": "application/json"
|
|
6297
|
+
},
|
|
6298
|
+
body: JSON.stringify({
|
|
6299
|
+
workspace_ids: workspaceIds.length ? workspaceIds : void 0,
|
|
6300
|
+
line_ids: lineIds.length ? lineIds : void 0
|
|
6301
|
+
})
|
|
6302
|
+
});
|
|
6303
|
+
if (!response.ok) {
|
|
6304
|
+
const errorText = await response.text();
|
|
6305
|
+
throw new Error(`Backend API error (${response.status}): ${errorText}`);
|
|
6306
|
+
}
|
|
6307
|
+
const data = await response.json();
|
|
6308
|
+
const lightConfigs = data.light_configs || {};
|
|
6309
|
+
this._workspaceLightConfigsCache.set(cacheKey, {
|
|
6310
|
+
lightConfigs,
|
|
6311
|
+
timestamp: Date.now()
|
|
6312
|
+
});
|
|
6313
|
+
return lightConfigs;
|
|
6314
|
+
} catch (error) {
|
|
6315
|
+
console.error("Error fetching workspace light configs:", error);
|
|
6316
|
+
throw error;
|
|
6317
|
+
} finally {
|
|
6318
|
+
this._workspaceLightConfigsInFlight.delete(cacheKey);
|
|
6319
|
+
}
|
|
6320
|
+
})();
|
|
6321
|
+
this._workspaceLightConfigsInFlight.set(cacheKey, fetchPromise);
|
|
6322
|
+
return fetchPromise;
|
|
6323
|
+
},
|
|
6265
6324
|
/**
|
|
6266
6325
|
* Fetches workspace display names from the database
|
|
6267
6326
|
* Returns a map of workspace_id -> display_name
|
|
@@ -6682,6 +6741,138 @@ var computeWorkspaceHealthSummary = (data, lastUpdated = (/* @__PURE__ */ new Da
|
|
|
6682
6741
|
lastUpdated
|
|
6683
6742
|
};
|
|
6684
6743
|
};
|
|
6744
|
+
var normalizeHourBucket = (bucket) => {
|
|
6745
|
+
if (Array.isArray(bucket)) return bucket;
|
|
6746
|
+
if (bucket && typeof bucket === "object" && "values" in bucket && Array.isArray(bucket.values)) {
|
|
6747
|
+
return bucket.values;
|
|
6748
|
+
}
|
|
6749
|
+
return void 0;
|
|
6750
|
+
};
|
|
6751
|
+
var normalizeOutputHourly = (outputHourlyRaw) => outputHourlyRaw && typeof outputHourlyRaw === "object" ? Object.fromEntries(
|
|
6752
|
+
Object.entries(outputHourlyRaw).map(([key, value]) => [key, normalizeHourBucket(value) || []])
|
|
6753
|
+
) : {};
|
|
6754
|
+
var interpretUptimeValue = (value) => {
|
|
6755
|
+
if (value === null || value === void 0) return "down";
|
|
6756
|
+
if (typeof value === "string") {
|
|
6757
|
+
return value.trim().toLowerCase() === "x" ? "down" : "up";
|
|
6758
|
+
}
|
|
6759
|
+
return "up";
|
|
6760
|
+
};
|
|
6761
|
+
var deriveStatusForMinute = (minuteOffset, minuteDate, outputHourly, outputArray, timezone) => {
|
|
6762
|
+
const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
|
|
6763
|
+
const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
|
|
6764
|
+
const hourBucket = outputHourly[hourKey];
|
|
6765
|
+
if (Array.isArray(hourBucket)) {
|
|
6766
|
+
const value = hourBucket[minuteKey];
|
|
6767
|
+
if (value !== void 0) {
|
|
6768
|
+
return interpretUptimeValue(value);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
if (minuteOffset < outputArray.length) {
|
|
6772
|
+
return interpretUptimeValue(outputArray[minuteOffset]);
|
|
6773
|
+
}
|
|
6774
|
+
return "down";
|
|
6775
|
+
};
|
|
6776
|
+
var hasCompletedTimelineData = (completedMinutes, shiftStartDate, outputHourly, outputArray, timezone) => {
|
|
6777
|
+
if (completedMinutes <= 0) {
|
|
6778
|
+
return false;
|
|
6779
|
+
}
|
|
6780
|
+
for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex += 1) {
|
|
6781
|
+
const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
|
|
6782
|
+
const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
|
|
6783
|
+
const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
|
|
6784
|
+
const hourBucket = outputHourly[hourKey];
|
|
6785
|
+
if (Array.isArray(hourBucket) && hourBucket[minuteKey] !== void 0) {
|
|
6786
|
+
return true;
|
|
6787
|
+
}
|
|
6788
|
+
if (minuteIndex < outputArray.length) {
|
|
6789
|
+
return true;
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
return false;
|
|
6793
|
+
};
|
|
6794
|
+
var computeWorkspaceUptime = ({
|
|
6795
|
+
totalMinutes,
|
|
6796
|
+
completedMinutes,
|
|
6797
|
+
shiftStartDate,
|
|
6798
|
+
outputHourly,
|
|
6799
|
+
outputArray,
|
|
6800
|
+
timezone
|
|
6801
|
+
}) => {
|
|
6802
|
+
const hasData = hasCompletedTimelineData(
|
|
6803
|
+
completedMinutes,
|
|
6804
|
+
shiftStartDate,
|
|
6805
|
+
outputHourly,
|
|
6806
|
+
outputArray,
|
|
6807
|
+
timezone
|
|
6808
|
+
);
|
|
6809
|
+
if (!hasData) {
|
|
6810
|
+
return {
|
|
6811
|
+
hasData: false,
|
|
6812
|
+
uptimeMinutes: 0,
|
|
6813
|
+
downtimeMinutes: 0,
|
|
6814
|
+
uptimePercentage: 0,
|
|
6815
|
+
points: [],
|
|
6816
|
+
downtimeSegments: []
|
|
6817
|
+
};
|
|
6818
|
+
}
|
|
6819
|
+
const points = [];
|
|
6820
|
+
let uptimeMinutes = 0;
|
|
6821
|
+
let downtimeMinutes = 0;
|
|
6822
|
+
for (let minuteIndex = 0; minuteIndex < totalMinutes; minuteIndex += 1) {
|
|
6823
|
+
const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
|
|
6824
|
+
const timestamp = dateFnsTz.formatInTimeZone(minuteDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
6825
|
+
const status = minuteIndex < completedMinutes ? deriveStatusForMinute(minuteIndex, minuteDate, outputHourly, outputArray, timezone) : "pending";
|
|
6826
|
+
if (status === "up") {
|
|
6827
|
+
uptimeMinutes += 1;
|
|
6828
|
+
} else if (status === "down") {
|
|
6829
|
+
downtimeMinutes += 1;
|
|
6830
|
+
}
|
|
6831
|
+
points.push({
|
|
6832
|
+
minuteIndex,
|
|
6833
|
+
timestamp,
|
|
6834
|
+
status
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
const downtimeSegments = [];
|
|
6838
|
+
let currentSegmentStart = null;
|
|
6839
|
+
const pushSegment = (startIndex, endIndex) => {
|
|
6840
|
+
if (endIndex <= startIndex) return;
|
|
6841
|
+
const segmentStartDate = dateFns.addMinutes(shiftStartDate, startIndex);
|
|
6842
|
+
const segmentEndDate = dateFns.addMinutes(shiftStartDate, endIndex);
|
|
6843
|
+
downtimeSegments.push({
|
|
6844
|
+
startMinuteIndex: startIndex,
|
|
6845
|
+
endMinuteIndex: endIndex,
|
|
6846
|
+
startTime: dateFnsTz.formatInTimeZone(segmentStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
6847
|
+
endTime: dateFnsTz.formatInTimeZone(segmentEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
6848
|
+
durationMinutes: endIndex - startIndex
|
|
6849
|
+
});
|
|
6850
|
+
};
|
|
6851
|
+
for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex += 1) {
|
|
6852
|
+
const point = points[minuteIndex];
|
|
6853
|
+
if (point.status === "down") {
|
|
6854
|
+
if (currentSegmentStart === null) {
|
|
6855
|
+
currentSegmentStart = minuteIndex;
|
|
6856
|
+
}
|
|
6857
|
+
} else if (currentSegmentStart !== null) {
|
|
6858
|
+
pushSegment(currentSegmentStart, minuteIndex);
|
|
6859
|
+
currentSegmentStart = null;
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
if (currentSegmentStart !== null) {
|
|
6863
|
+
pushSegment(currentSegmentStart, completedMinutes);
|
|
6864
|
+
}
|
|
6865
|
+
const completedWindow = Math.max(1, uptimeMinutes + downtimeMinutes);
|
|
6866
|
+
const uptimePercentage = completedMinutes > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
|
|
6867
|
+
return {
|
|
6868
|
+
hasData: true,
|
|
6869
|
+
uptimeMinutes,
|
|
6870
|
+
downtimeMinutes,
|
|
6871
|
+
uptimePercentage,
|
|
6872
|
+
points,
|
|
6873
|
+
downtimeSegments
|
|
6874
|
+
};
|
|
6875
|
+
};
|
|
6685
6876
|
|
|
6686
6877
|
// src/lib/services/workspaceHealthService.ts
|
|
6687
6878
|
var DATA_PROCESSING_DELAY_MINUTES = 5;
|
|
@@ -6708,6 +6899,92 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6708
6899
|
setCache(key, data) {
|
|
6709
6900
|
this.cache.set(key, { data, timestamp: Date.now() });
|
|
6710
6901
|
}
|
|
6902
|
+
hasBackendUrl() {
|
|
6903
|
+
return Boolean(process.env.NEXT_PUBLIC_BACKEND_URL);
|
|
6904
|
+
}
|
|
6905
|
+
async fetchBackendWorkspaceUptimeSummaries(companyId, lineIds, date, shiftId, timezone) {
|
|
6906
|
+
if (!this.hasBackendUrl()) {
|
|
6907
|
+
return null;
|
|
6908
|
+
}
|
|
6909
|
+
const supabase = _getSupabaseInstance();
|
|
6910
|
+
if (!supabase) throw new Error("Supabase client not initialized");
|
|
6911
|
+
const params = new URLSearchParams({
|
|
6912
|
+
company_id: companyId,
|
|
6913
|
+
date,
|
|
6914
|
+
shift_id: String(shiftId),
|
|
6915
|
+
timezone
|
|
6916
|
+
});
|
|
6917
|
+
const filteredLineIds = (lineIds || []).filter(Boolean);
|
|
6918
|
+
if (filteredLineIds.length > 0) {
|
|
6919
|
+
params.set("line_ids", filteredLineIds.join(","));
|
|
6920
|
+
}
|
|
6921
|
+
const cacheKey = `backend-uptime-summary:${companyId}:${filteredLineIds.join(",") || "all"}:${date}:${shiftId}:${timezone}`;
|
|
6922
|
+
const cached = this.getFromCache(cacheKey);
|
|
6923
|
+
if (cached) {
|
|
6924
|
+
return cached;
|
|
6925
|
+
}
|
|
6926
|
+
try {
|
|
6927
|
+
const response = await fetchBackendJson(
|
|
6928
|
+
supabase,
|
|
6929
|
+
`/api/dashboard/workspace-uptime?${params.toString()}`,
|
|
6930
|
+
{
|
|
6931
|
+
retries: 1,
|
|
6932
|
+
timeoutMs: 15e3,
|
|
6933
|
+
sentry: {
|
|
6934
|
+
capture: true,
|
|
6935
|
+
surface: "workspace_health",
|
|
6936
|
+
route: "GET /api/dashboard/workspace-uptime",
|
|
6937
|
+
severity: "warning",
|
|
6938
|
+
quotaKey: "workspace-health-uptime-summary"
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
);
|
|
6942
|
+
const uptimeMap = /* @__PURE__ */ new Map();
|
|
6943
|
+
for (const entry of response.uptimes || []) {
|
|
6944
|
+
if (!entry.workspaceId || !entry.uptimeDetails) continue;
|
|
6945
|
+
uptimeMap.set(this.getUptimeMapKey(entry.lineId, entry.workspaceId), entry.uptimeDetails);
|
|
6946
|
+
}
|
|
6947
|
+
this.setCache(cacheKey, uptimeMap);
|
|
6948
|
+
return uptimeMap;
|
|
6949
|
+
} catch (error) {
|
|
6950
|
+
console.warn("[workspaceHealthService] Backend uptime summary failed; using frontend fallback", error);
|
|
6951
|
+
return null;
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
async fetchBackendWorkspaceUptimeTimeline(workspaceId, companyId, lineId, date, shiftId, timezone) {
|
|
6955
|
+
if (!this.hasBackendUrl()) {
|
|
6956
|
+
return null;
|
|
6957
|
+
}
|
|
6958
|
+
const supabase = _getSupabaseInstance();
|
|
6959
|
+
if (!supabase) throw new Error("Supabase client not initialized");
|
|
6960
|
+
const params = new URLSearchParams({
|
|
6961
|
+
company_id: companyId,
|
|
6962
|
+
line_id: lineId,
|
|
6963
|
+
date,
|
|
6964
|
+
shift_id: String(shiftId),
|
|
6965
|
+
timezone
|
|
6966
|
+
});
|
|
6967
|
+
try {
|
|
6968
|
+
return await fetchBackendJson(
|
|
6969
|
+
supabase,
|
|
6970
|
+
`/api/dashboard/workspace/${workspaceId}/uptime-timeline?${params.toString()}`,
|
|
6971
|
+
{
|
|
6972
|
+
retries: 1,
|
|
6973
|
+
timeoutMs: 15e3,
|
|
6974
|
+
sentry: {
|
|
6975
|
+
capture: true,
|
|
6976
|
+
surface: "workspace_health",
|
|
6977
|
+
route: "GET /api/dashboard/workspace/{workspace_id}/uptime-timeline",
|
|
6978
|
+
severity: "warning",
|
|
6979
|
+
quotaKey: "workspace-health-uptime-timeline"
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
);
|
|
6983
|
+
} catch (error) {
|
|
6984
|
+
console.warn("[workspaceHealthService] Backend uptime timeline failed; using frontend fallback", error);
|
|
6985
|
+
return null;
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6711
6988
|
getShiftTiming(timezone, shiftConfig) {
|
|
6712
6989
|
const currentShift = getCurrentShift(timezone, shiftConfig);
|
|
6713
6990
|
const { shiftId, date, shiftName, startTime, endTime } = currentShift;
|
|
@@ -6791,15 +7068,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6791
7068
|
pendingMinutes
|
|
6792
7069
|
};
|
|
6793
7070
|
}
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
)
|
|
7071
|
+
mergeOutputArrayValues(values) {
|
|
7072
|
+
const numericValues = values.filter(
|
|
7073
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
7074
|
+
);
|
|
7075
|
+
if (numericValues.length > 0) {
|
|
7076
|
+
const total = numericValues.reduce((sum, value) => sum + value, 0);
|
|
7077
|
+
return Number.isInteger(total) ? total : total;
|
|
7078
|
+
}
|
|
7079
|
+
if (values.length > 0 && values.every((value) => typeof value === "string" && value.trim().toLowerCase() === "x")) {
|
|
7080
|
+
return "x";
|
|
7081
|
+
}
|
|
7082
|
+
if (values.some((value) => value === null || value === void 0)) {
|
|
7083
|
+
return null;
|
|
7084
|
+
}
|
|
7085
|
+
return 0;
|
|
6803
7086
|
}
|
|
6804
7087
|
/**
|
|
6805
7088
|
* Group multi-SKU `performance_metrics` rows by workspace and merge their
|
|
@@ -6833,21 +7116,16 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6833
7116
|
const primary = group[0];
|
|
6834
7117
|
const mergedOutputHourly = mergeOutputHourly(
|
|
6835
7118
|
group.map((record) => ({
|
|
6836
|
-
output_hourly:
|
|
7119
|
+
output_hourly: normalizeOutputHourly(record.output_hourly || {})
|
|
6837
7120
|
}))
|
|
6838
7121
|
);
|
|
6839
7122
|
const arrays = group.map((record) => Array.isArray(record.output_array) ? record.output_array : []).filter((arr) => arr.length > 0);
|
|
6840
7123
|
let mergedArray = [];
|
|
6841
7124
|
if (arrays.length > 0) {
|
|
6842
7125
|
const maxLen = arrays.reduce((acc, arr) => Math.max(acc, arr.length), 0);
|
|
6843
|
-
mergedArray = new Array(maxLen).fill(0);
|
|
6844
7126
|
for (let i = 0; i < maxLen; i += 1) {
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
const value = i < arr.length ? arr[i] : 0;
|
|
6848
|
-
if (typeof value === "number" && Number.isFinite(value)) sum += value;
|
|
6849
|
-
}
|
|
6850
|
-
mergedArray[i] = sum;
|
|
7127
|
+
const values = arrays.filter((arr) => i < arr.length).map((arr) => arr[i]);
|
|
7128
|
+
mergedArray[i] = this.mergeOutputArrayValues(values);
|
|
6851
7129
|
}
|
|
6852
7130
|
}
|
|
6853
7131
|
return {
|
|
@@ -6857,45 +7135,392 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6857
7135
|
};
|
|
6858
7136
|
});
|
|
6859
7137
|
}
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
7138
|
+
parseShiftTime(value) {
|
|
7139
|
+
const [hourPart = "0", minutePart = "0"] = String(value || "00:00").split(":");
|
|
7140
|
+
const hour = Number.parseInt(hourPart, 10);
|
|
7141
|
+
const minute = Number.parseInt(minutePart, 10);
|
|
7142
|
+
return {
|
|
7143
|
+
hour: Number.isFinite(hour) ? hour : 0,
|
|
7144
|
+
minute: Number.isFinite(minute) ? minute : 0
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
7147
|
+
getShiftDurationMinutes(startTime, endTime) {
|
|
7148
|
+
const start = this.parseShiftTime(startTime);
|
|
7149
|
+
const end = this.parseShiftTime(endTime);
|
|
7150
|
+
let duration = end.hour * 60 + end.minute - (start.hour * 60 + start.minute);
|
|
7151
|
+
if (duration <= 0) {
|
|
7152
|
+
duration += 24 * 60;
|
|
6864
7153
|
}
|
|
6865
|
-
return
|
|
7154
|
+
return duration;
|
|
6866
7155
|
}
|
|
6867
|
-
|
|
6868
|
-
const
|
|
6869
|
-
const
|
|
6870
|
-
const
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
7156
|
+
resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date()) {
|
|
7157
|
+
const currentShift = getCurrentShift(timezone, shiftConfig);
|
|
7158
|
+
const queryDate = overrideDate ?? currentShift.date;
|
|
7159
|
+
const queryShiftId = overrideShiftId ?? currentShift.shiftId;
|
|
7160
|
+
const targetShiftConfig = shiftConfig?.shifts?.find((shift) => shift.shiftId === queryShiftId);
|
|
7161
|
+
const startTime = targetShiftConfig?.startTime || currentShift.startTime || "06:00";
|
|
7162
|
+
const endTime = targetShiftConfig?.endTime || currentShift.endTime || "18:00";
|
|
7163
|
+
const shiftLabel = targetShiftConfig?.shiftName || targetShiftConfig?.name || currentShift.shiftName || `Shift ${queryShiftId}`;
|
|
7164
|
+
const shiftStartDate = dateFnsTz.fromZonedTime(`${queryDate}T${startTime}:00`, timezone);
|
|
7165
|
+
const totalMinutes = this.getShiftDurationMinutes(startTime, endTime);
|
|
7166
|
+
const shiftEndDate = dateFns.addMinutes(shiftStartDate, totalMinutes);
|
|
7167
|
+
const boundedWindowEnd = new Date(Math.min(Math.max(now4.getTime(), shiftStartDate.getTime()), shiftEndDate.getTime()));
|
|
7168
|
+
const completedMinutes = Math.max(
|
|
7169
|
+
0,
|
|
7170
|
+
Math.min(totalMinutes, Math.floor((boundedWindowEnd.getTime() - shiftStartDate.getTime()) / 6e4))
|
|
7171
|
+
);
|
|
7172
|
+
return {
|
|
7173
|
+
shiftId: queryShiftId,
|
|
7174
|
+
shiftLabel,
|
|
7175
|
+
shiftStartDate,
|
|
7176
|
+
shiftEndDate,
|
|
7177
|
+
windowEndDate: boundedWindowEnd,
|
|
7178
|
+
totalMinutes,
|
|
7179
|
+
completedMinutes,
|
|
7180
|
+
pendingMinutes: Math.max(0, totalMinutes - completedMinutes)
|
|
7181
|
+
};
|
|
7182
|
+
}
|
|
7183
|
+
isLightStatus(status) {
|
|
7184
|
+
return status === "up" || status === "down" || status === "unknown";
|
|
7185
|
+
}
|
|
7186
|
+
secondsBetween(start, end) {
|
|
7187
|
+
return Math.max(0, Math.round((end.getTime() - start.getTime()) / 1e3));
|
|
7188
|
+
}
|
|
7189
|
+
roundPercent(value) {
|
|
7190
|
+
return Number(value.toFixed(1));
|
|
7191
|
+
}
|
|
7192
|
+
emptyLightTimeline(shiftWindow, timezone, bulbIp = null) {
|
|
7193
|
+
return {
|
|
7194
|
+
shiftId: shiftWindow.shiftId,
|
|
7195
|
+
shiftLabel: shiftWindow.shiftLabel,
|
|
7196
|
+
shiftStart: dateFnsTz.formatInTimeZone(shiftWindow.shiftStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7197
|
+
shiftEnd: dateFnsTz.formatInTimeZone(shiftWindow.shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7198
|
+
totalMinutes: shiftWindow.totalMinutes,
|
|
7199
|
+
completedMinutes: shiftWindow.completedMinutes,
|
|
7200
|
+
upSeconds: 0,
|
|
7201
|
+
downSeconds: 0,
|
|
7202
|
+
unknownSeconds: 0,
|
|
7203
|
+
totalObservedSeconds: 0,
|
|
7204
|
+
downtimeSeconds: 0,
|
|
7205
|
+
uptimePercentage: null,
|
|
7206
|
+
hasData: false,
|
|
7207
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7208
|
+
bulbIp,
|
|
7209
|
+
points: [],
|
|
7210
|
+
statusSegments: []
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
async resolveWorkspaceLightConfigs(workspaces) {
|
|
7214
|
+
const supabase = _getSupabaseInstance();
|
|
7215
|
+
if (!supabase) throw new Error("Supabase client not initialized");
|
|
7216
|
+
const visibleWorkspaces = workspaces.filter((workspace) => workspace.workspace_id);
|
|
7217
|
+
if (!visibleWorkspaces.length) return /* @__PURE__ */ new Map();
|
|
7218
|
+
const workspaceIds = Array.from(new Set(visibleWorkspaces.map((workspace) => String(workspace.workspace_id))));
|
|
7219
|
+
const configs = /* @__PURE__ */ new Map();
|
|
7220
|
+
if (process.env.NEXT_PUBLIC_BACKEND_URL) {
|
|
7221
|
+
try {
|
|
7222
|
+
const backendConfigs = await workspaceService.getWorkspaceLightConfigs({ workspaceIds });
|
|
7223
|
+
for (const [workspaceId, config] of Object.entries(backendConfigs)) {
|
|
7224
|
+
if (!config?.bulb_ip) continue;
|
|
7225
|
+
configs.set(workspaceId, {
|
|
7226
|
+
healthWorkspaceId: workspaceId,
|
|
7227
|
+
lineId: config.line_id,
|
|
7228
|
+
workspaceDbId: config.workspace_id || workspaceId,
|
|
7229
|
+
bulbIp: String(config.bulb_ip)
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
if (configs.size === workspaceIds.length) {
|
|
7233
|
+
return configs;
|
|
7234
|
+
}
|
|
7235
|
+
} catch (error) {
|
|
7236
|
+
console.error("[workspaceHealthService] Backend light config lookup failed, falling back to direct Supabase:", error);
|
|
6875
7237
|
}
|
|
6876
7238
|
}
|
|
6877
|
-
|
|
6878
|
-
|
|
7239
|
+
const unresolvedVisibleWorkspaces = visibleWorkspaces.filter(
|
|
7240
|
+
(workspace) => !configs.has(String(workspace.workspace_id))
|
|
7241
|
+
);
|
|
7242
|
+
if (!unresolvedVisibleWorkspaces.length) return configs;
|
|
7243
|
+
const fallbackWorkspaceIds = Array.from(new Set(unresolvedVisibleWorkspaces.map((workspace) => String(workspace.workspace_id))));
|
|
7244
|
+
const fallbackLineIds = Array.from(new Set(unresolvedVisibleWorkspaces.map((workspace) => workspace.line_id).filter(Boolean).map(String)));
|
|
7245
|
+
const fallbackVisiblePairs = new Set(unresolvedVisibleWorkspaces.map((workspace) => `${workspace.line_id || ""}::${workspace.workspace_id}`));
|
|
7246
|
+
let workspaceRows = [];
|
|
7247
|
+
try {
|
|
7248
|
+
let query = supabase.from("workspaces").select("id, workspace_id, line_id, enable_bulb_light").in("workspace_id", fallbackWorkspaceIds);
|
|
7249
|
+
if (fallbackLineIds.length) {
|
|
7250
|
+
query = query.in("line_id", fallbackLineIds);
|
|
7251
|
+
}
|
|
7252
|
+
const { data, error } = await query;
|
|
7253
|
+
if (error) throw error;
|
|
7254
|
+
workspaceRows = Array.isArray(data) ? data : [];
|
|
7255
|
+
} catch (error) {
|
|
7256
|
+
console.error("[workspaceHealthService] Error resolving light workspace rows:", error);
|
|
7257
|
+
return configs;
|
|
7258
|
+
}
|
|
7259
|
+
const dbIdToVisible = /* @__PURE__ */ new Map();
|
|
7260
|
+
for (const row of workspaceRows) {
|
|
7261
|
+
const healthWorkspaceId = row?.workspace_id ? String(row.workspace_id) : "";
|
|
7262
|
+
const lineId = row?.line_id ? String(row.line_id) : "";
|
|
7263
|
+
const dbId = row?.id ? String(row.id) : "";
|
|
7264
|
+
if (!healthWorkspaceId || !dbId) continue;
|
|
7265
|
+
if (fallbackLineIds.length && !fallbackVisiblePairs.has(`${lineId}::${healthWorkspaceId}`)) continue;
|
|
7266
|
+
if (row?.enable_bulb_light === false) continue;
|
|
7267
|
+
dbIdToVisible.set(dbId, { healthWorkspaceId, lineId });
|
|
7268
|
+
}
|
|
7269
|
+
const addMapping = (mapping, visible, workspaceDbId) => {
|
|
7270
|
+
const bulbIp = mapping?.bulb_ip ? String(mapping.bulb_ip).trim() : "";
|
|
7271
|
+
if (!bulbIp) return;
|
|
7272
|
+
configs.set(visible.healthWorkspaceId, {
|
|
7273
|
+
healthWorkspaceId: visible.healthWorkspaceId,
|
|
7274
|
+
lineId: visible.lineId,
|
|
7275
|
+
workspaceDbId,
|
|
7276
|
+
bulbIp
|
|
7277
|
+
});
|
|
7278
|
+
};
|
|
7279
|
+
const dbIds = Array.from(dbIdToVisible.keys());
|
|
7280
|
+
if (dbIds.length) {
|
|
7281
|
+
const { data, error } = await supabase.from("workspace_ip_mapping").select("workspace_id, bulb_ip").in("workspace_id", dbIds);
|
|
7282
|
+
if (!error && Array.isArray(data)) {
|
|
7283
|
+
for (const mapping of data) {
|
|
7284
|
+
const dbId = mapping?.workspace_id ? String(mapping.workspace_id) : "";
|
|
7285
|
+
const visible = dbIdToVisible.get(dbId);
|
|
7286
|
+
if (visible) addMapping(mapping, visible, dbId);
|
|
7287
|
+
}
|
|
7288
|
+
} else if (error) {
|
|
7289
|
+
console.error("[workspaceHealthService] Error resolving light IP mappings:", error);
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
const unresolvedVisibleIds = fallbackWorkspaceIds.filter((workspaceId) => !configs.has(workspaceId));
|
|
7293
|
+
if (unresolvedVisibleIds.length) {
|
|
7294
|
+
const visibleById = new Map(unresolvedVisibleWorkspaces.map((workspace) => [
|
|
7295
|
+
String(workspace.workspace_id),
|
|
7296
|
+
{ healthWorkspaceId: String(workspace.workspace_id), lineId: workspace.line_id }
|
|
7297
|
+
]));
|
|
7298
|
+
const { data, error } = await supabase.from("workspace_ip_mapping").select("workspace_id, bulb_ip").in("workspace_id", unresolvedVisibleIds);
|
|
7299
|
+
if (!error && Array.isArray(data)) {
|
|
7300
|
+
for (const mapping of data) {
|
|
7301
|
+
const workspaceId = mapping?.workspace_id ? String(mapping.workspace_id) : "";
|
|
7302
|
+
const visible = visibleById.get(workspaceId);
|
|
7303
|
+
if (visible) addMapping(mapping, visible, null);
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
return configs;
|
|
7308
|
+
}
|
|
7309
|
+
async fetchLightIntervals(bulbIps, windowStart, windowEnd) {
|
|
7310
|
+
if (!bulbIps.length || windowEnd <= windowStart) return [];
|
|
7311
|
+
const supabase = _getSupabaseInstance();
|
|
7312
|
+
if (!supabase) throw new Error("Supabase client not initialized");
|
|
7313
|
+
const { data, error } = await supabase.from("factory_light_status_intervals").select("workspace_id, bulb_ip, status, started_at, ended_at, last_seen_at, last_error").in("bulb_ip", bulbIps).lt("started_at", windowEnd.toISOString()).or(`ended_at.is.null,ended_at.gt.${windowStart.toISOString()}`);
|
|
7314
|
+
if (error) {
|
|
7315
|
+
console.error("[workspaceHealthService] Error fetching light status intervals:", error);
|
|
7316
|
+
throw error;
|
|
6879
7317
|
}
|
|
6880
|
-
return
|
|
7318
|
+
return Array.isArray(data) ? data : [];
|
|
6881
7319
|
}
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
7320
|
+
getLightIntervalEffectiveRange(interval, windowStart, windowEnd) {
|
|
7321
|
+
const startedAt = new Date(interval.started_at);
|
|
7322
|
+
const endedAt = interval.ended_at ? new Date(interval.ended_at) : windowEnd;
|
|
7323
|
+
const effectiveStart = new Date(Math.max(startedAt.getTime(), windowStart.getTime()));
|
|
7324
|
+
const effectiveEnd = new Date(Math.min(endedAt.getTime(), windowEnd.getTime()));
|
|
7325
|
+
if (effectiveEnd <= effectiveStart) return null;
|
|
7326
|
+
return {
|
|
7327
|
+
start: effectiveStart,
|
|
7328
|
+
end: effectiveEnd,
|
|
7329
|
+
seconds: this.secondsBetween(effectiveStart, effectiveEnd)
|
|
7330
|
+
};
|
|
7331
|
+
}
|
|
7332
|
+
summarizeLightIntervals(intervals, windowStart, windowEnd, now4) {
|
|
7333
|
+
let upSeconds = 0;
|
|
7334
|
+
let downSeconds = 0;
|
|
7335
|
+
let unknownSeconds = 0;
|
|
7336
|
+
let currentInterval = null;
|
|
7337
|
+
for (const interval of intervals) {
|
|
7338
|
+
if (!this.isLightStatus(interval.status)) continue;
|
|
7339
|
+
const range = this.getLightIntervalEffectiveRange(interval, windowStart, windowEnd);
|
|
7340
|
+
if (!range) continue;
|
|
7341
|
+
if (interval.status === "up") upSeconds += range.seconds;
|
|
7342
|
+
if (interval.status === "down") downSeconds += range.seconds;
|
|
7343
|
+
if (interval.status === "unknown") unknownSeconds += range.seconds;
|
|
7344
|
+
if (interval.ended_at == null) {
|
|
7345
|
+
currentInterval = interval;
|
|
7346
|
+
}
|
|
7347
|
+
}
|
|
7348
|
+
const totalObservedSeconds = upSeconds + downSeconds + unknownSeconds;
|
|
7349
|
+
return {
|
|
7350
|
+
hasLightConfig: true,
|
|
7351
|
+
bulbIp: null,
|
|
7352
|
+
currentStatus: this.isLightStatus(currentInterval?.status) ? currentInterval.status : null,
|
|
7353
|
+
currentStartedAt: currentInterval?.started_at || null,
|
|
7354
|
+
lastSeenAt: currentInterval?.last_seen_at || null,
|
|
7355
|
+
currentDurationSeconds: currentInterval?.started_at ? this.secondsBetween(new Date(currentInterval.started_at), now4) : null,
|
|
7356
|
+
lastError: currentInterval?.last_error || null,
|
|
7357
|
+
uptimePercent: totalObservedSeconds > 0 ? this.roundPercent(upSeconds / totalObservedSeconds * 100) : null,
|
|
7358
|
+
upSeconds,
|
|
7359
|
+
downSeconds,
|
|
7360
|
+
unknownSeconds,
|
|
7361
|
+
totalObservedSeconds
|
|
7362
|
+
};
|
|
7363
|
+
}
|
|
7364
|
+
async summarizeResolvedLightConfigs(configs, shiftWindow, now4) {
|
|
7365
|
+
if (!configs.size) return /* @__PURE__ */ new Map();
|
|
7366
|
+
const bulbIps = Array.from(new Set(Array.from(configs.values()).map((config) => config.bulbIp)));
|
|
7367
|
+
const intervals = await this.fetchLightIntervals(bulbIps, shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
|
|
7368
|
+
const intervalsByBulb = /* @__PURE__ */ new Map();
|
|
7369
|
+
for (const interval of intervals) {
|
|
7370
|
+
const bulbIp = interval.bulb_ip ? String(interval.bulb_ip) : "";
|
|
7371
|
+
if (!bulbIp) continue;
|
|
7372
|
+
if (!intervalsByBulb.has(bulbIp)) intervalsByBulb.set(bulbIp, []);
|
|
7373
|
+
intervalsByBulb.get(bulbIp).push(interval);
|
|
7374
|
+
}
|
|
7375
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
7376
|
+
for (const config of configs.values()) {
|
|
7377
|
+
const summary = this.summarizeLightIntervals(
|
|
7378
|
+
intervalsByBulb.get(config.bulbIp) || [],
|
|
7379
|
+
shiftWindow.shiftStartDate,
|
|
7380
|
+
shiftWindow.windowEndDate,
|
|
7381
|
+
now4
|
|
7382
|
+
);
|
|
7383
|
+
summaries.set(config.healthWorkspaceId, {
|
|
7384
|
+
...summary,
|
|
7385
|
+
hasLightConfig: true,
|
|
7386
|
+
bulbIp: config.bulbIp
|
|
7387
|
+
});
|
|
6885
7388
|
}
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
7389
|
+
return summaries;
|
|
7390
|
+
}
|
|
7391
|
+
async getWorkspaceLightSummaries(workspaces, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date(), lineShiftConfigs) {
|
|
7392
|
+
const dashboardConfig = _getDashboardConfigInstance();
|
|
7393
|
+
const shiftConfig = passedShiftConfig || dashboardConfig?.shiftConfig;
|
|
7394
|
+
const timezone = passedTimezone || shiftConfig?.timezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
|
|
7395
|
+
if (lineShiftConfigs && lineShiftConfigs.size > 0) {
|
|
7396
|
+
const resolvedConfigs = await this.resolveWorkspaceLightConfigs(workspaces);
|
|
7397
|
+
if (!resolvedConfigs.size) return /* @__PURE__ */ new Map();
|
|
7398
|
+
const groupedByLine = /* @__PURE__ */ new Map();
|
|
7399
|
+
const fallbackConfigs = /* @__PURE__ */ new Map();
|
|
7400
|
+
for (const workspace of workspaces) {
|
|
7401
|
+
const workspaceId = workspace.workspace_id ? String(workspace.workspace_id) : "";
|
|
7402
|
+
const config = workspaceId ? resolvedConfigs.get(workspaceId) : null;
|
|
7403
|
+
if (!config) continue;
|
|
7404
|
+
const lineId = workspace.line_id ? String(workspace.line_id) : "";
|
|
7405
|
+
const lineShiftConfig = lineId ? lineShiftConfigs.get(lineId) : null;
|
|
7406
|
+
if (!lineId || !lineShiftConfig) {
|
|
7407
|
+
fallbackConfigs.set(config.healthWorkspaceId, config);
|
|
7408
|
+
continue;
|
|
7409
|
+
}
|
|
7410
|
+
if (!groupedByLine.has(lineId)) groupedByLine.set(lineId, /* @__PURE__ */ new Map());
|
|
7411
|
+
groupedByLine.get(lineId).set(config.healthWorkspaceId, config);
|
|
6893
7412
|
}
|
|
6894
|
-
|
|
6895
|
-
|
|
7413
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
7414
|
+
const summaryPromises = Array.from(groupedByLine.entries()).map(async ([lineId, lineConfigs]) => {
|
|
7415
|
+
const lineShiftWindow = this.resolveLightShiftWindow(
|
|
7416
|
+
lineShiftConfigs.get(lineId),
|
|
7417
|
+
timezone,
|
|
7418
|
+
overrideDate,
|
|
7419
|
+
overrideShiftId,
|
|
7420
|
+
now4
|
|
7421
|
+
);
|
|
7422
|
+
return this.summarizeResolvedLightConfigs(lineConfigs, lineShiftWindow, now4);
|
|
7423
|
+
});
|
|
7424
|
+
if (fallbackConfigs.size > 0) {
|
|
7425
|
+
const fallbackShiftWindow = this.resolveLightShiftWindow(
|
|
7426
|
+
shiftConfig,
|
|
7427
|
+
timezone,
|
|
7428
|
+
overrideDate,
|
|
7429
|
+
overrideShiftId,
|
|
7430
|
+
now4
|
|
7431
|
+
);
|
|
7432
|
+
summaryPromises.push(this.summarizeResolvedLightConfigs(fallbackConfigs, fallbackShiftWindow, now4));
|
|
6896
7433
|
}
|
|
7434
|
+
const resolvedSummaries = await Promise.all(summaryPromises);
|
|
7435
|
+
for (const groupSummaries of resolvedSummaries) {
|
|
7436
|
+
groupSummaries.forEach((summary, workspaceId) => summaries.set(workspaceId, summary));
|
|
7437
|
+
}
|
|
7438
|
+
return summaries;
|
|
6897
7439
|
}
|
|
6898
|
-
|
|
7440
|
+
const shiftWindow = this.resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4);
|
|
7441
|
+
const configs = await this.resolveWorkspaceLightConfigs(workspaces);
|
|
7442
|
+
return this.summarizeResolvedLightConfigs(configs, shiftWindow, now4);
|
|
7443
|
+
}
|
|
7444
|
+
async getWorkspaceLightTimeline(workspaceId, lineId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date()) {
|
|
7445
|
+
if (!workspaceId) {
|
|
7446
|
+
throw new Error("workspaceId is required to fetch light timeline");
|
|
7447
|
+
}
|
|
7448
|
+
const dashboardConfig = _getDashboardConfigInstance();
|
|
7449
|
+
const shiftConfig = passedShiftConfig || dashboardConfig?.shiftConfig;
|
|
7450
|
+
const timezone = passedTimezone || shiftConfig?.timezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
|
|
7451
|
+
const shiftWindow = this.resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4);
|
|
7452
|
+
const configs = await this.resolveWorkspaceLightConfigs([{ workspace_id: workspaceId, line_id: lineId }]);
|
|
7453
|
+
const config = configs.get(workspaceId);
|
|
7454
|
+
if (!config) {
|
|
7455
|
+
return this.emptyLightTimeline(shiftWindow, timezone);
|
|
7456
|
+
}
|
|
7457
|
+
const intervals = await this.fetchLightIntervals([config.bulbIp], shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
|
|
7458
|
+
const summary = this.summarizeLightIntervals(intervals, shiftWindow.shiftStartDate, shiftWindow.windowEndDate, now4);
|
|
7459
|
+
const hasData = summary.totalObservedSeconds > 0;
|
|
7460
|
+
const points = [];
|
|
7461
|
+
if (hasData) {
|
|
7462
|
+
for (let minuteIndex = 0; minuteIndex < shiftWindow.totalMinutes; minuteIndex++) {
|
|
7463
|
+
const minuteStart = dateFns.addMinutes(shiftWindow.shiftStartDate, minuteIndex);
|
|
7464
|
+
const minuteEnd = dateFns.addMinutes(minuteStart, 1);
|
|
7465
|
+
const timestamp = dateFnsTz.formatInTimeZone(minuteStart, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
7466
|
+
let status = "pending";
|
|
7467
|
+
if (minuteIndex < shiftWindow.completedMinutes) {
|
|
7468
|
+
const matchingInterval = intervals.find((interval) => {
|
|
7469
|
+
if (!this.isLightStatus(interval.status)) return false;
|
|
7470
|
+
const start = new Date(interval.started_at);
|
|
7471
|
+
const end = interval.ended_at ? new Date(interval.ended_at) : shiftWindow.windowEndDate;
|
|
7472
|
+
return start < minuteEnd && end > minuteStart;
|
|
7473
|
+
});
|
|
7474
|
+
status = this.isLightStatus(matchingInterval?.status) ? matchingInterval.status : "pending";
|
|
7475
|
+
}
|
|
7476
|
+
points.push({ minuteIndex, timestamp, status });
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
const statusSegments = intervals.filter((interval) => interval.status === "down" || interval.status === "unknown").reduce((segments, interval) => {
|
|
7480
|
+
const range = this.getLightIntervalEffectiveRange(interval, shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
|
|
7481
|
+
if (!range || !this.isLightStatus(interval.status)) return segments;
|
|
7482
|
+
const startMinuteIndex = Math.max(0, Math.floor((range.start.getTime() - shiftWindow.shiftStartDate.getTime()) / 6e4));
|
|
7483
|
+
const endMinuteIndex = Math.min(
|
|
7484
|
+
shiftWindow.totalMinutes,
|
|
7485
|
+
Math.ceil((range.end.getTime() - shiftWindow.shiftStartDate.getTime()) / 6e4)
|
|
7486
|
+
);
|
|
7487
|
+
segments.push({
|
|
7488
|
+
status: interval.status,
|
|
7489
|
+
startMinuteIndex,
|
|
7490
|
+
endMinuteIndex,
|
|
7491
|
+
startTime: dateFnsTz.formatInTimeZone(range.start, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7492
|
+
endTime: dateFnsTz.formatInTimeZone(range.end, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7493
|
+
durationSeconds: range.seconds,
|
|
7494
|
+
durationMinutes: Math.ceil(range.seconds / 60),
|
|
7495
|
+
isCurrent: interval.ended_at == null,
|
|
7496
|
+
lastError: interval.last_error || null
|
|
7497
|
+
});
|
|
7498
|
+
return segments;
|
|
7499
|
+
}, []).sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
7500
|
+
return {
|
|
7501
|
+
shiftId: shiftWindow.shiftId,
|
|
7502
|
+
shiftLabel: shiftWindow.shiftLabel,
|
|
7503
|
+
shiftStart: dateFnsTz.formatInTimeZone(shiftWindow.shiftStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7504
|
+
shiftEnd: dateFnsTz.formatInTimeZone(shiftWindow.shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7505
|
+
totalMinutes: shiftWindow.totalMinutes,
|
|
7506
|
+
completedMinutes: shiftWindow.completedMinutes,
|
|
7507
|
+
upSeconds: summary.upSeconds,
|
|
7508
|
+
downSeconds: summary.downSeconds,
|
|
7509
|
+
unknownSeconds: summary.unknownSeconds,
|
|
7510
|
+
totalObservedSeconds: summary.totalObservedSeconds,
|
|
7511
|
+
downtimeSeconds: summary.downSeconds,
|
|
7512
|
+
uptimePercentage: summary.uptimePercent,
|
|
7513
|
+
hasData,
|
|
7514
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7515
|
+
bulbIp: config.bulbIp,
|
|
7516
|
+
currentStatus: summary.currentStatus,
|
|
7517
|
+
currentStartedAt: summary.currentStartedAt,
|
|
7518
|
+
currentDurationSeconds: summary.currentDurationSeconds,
|
|
7519
|
+
lastSeenAt: summary.lastSeenAt,
|
|
7520
|
+
lastError: summary.lastError,
|
|
7521
|
+
points,
|
|
7522
|
+
statusSegments
|
|
7523
|
+
};
|
|
6899
7524
|
}
|
|
6900
7525
|
async getWorkspaceHealthStatus(options = {}) {
|
|
6901
7526
|
const supabase = _getSupabaseInstance();
|
|
@@ -6930,7 +7555,8 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6930
7555
|
timezone,
|
|
6931
7556
|
options.lineShiftConfigs,
|
|
6932
7557
|
options.date,
|
|
6933
|
-
options.shiftId
|
|
7558
|
+
options.shiftId,
|
|
7559
|
+
options.lineId
|
|
6934
7560
|
);
|
|
6935
7561
|
} catch (error2) {
|
|
6936
7562
|
console.error("Error calculating uptime:", error2);
|
|
@@ -6995,6 +7621,27 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
6995
7621
|
} catch (e) {
|
|
6996
7622
|
console.error("Error filtering workspaces:", e);
|
|
6997
7623
|
}
|
|
7624
|
+
if (filteredData.length > 0) {
|
|
7625
|
+
try {
|
|
7626
|
+
const lightSummaries = await this.getWorkspaceLightSummaries(
|
|
7627
|
+
filteredData,
|
|
7628
|
+
shiftConfig,
|
|
7629
|
+
timezone,
|
|
7630
|
+
options.date,
|
|
7631
|
+
options.shiftId,
|
|
7632
|
+
/* @__PURE__ */ new Date(),
|
|
7633
|
+
options.lineShiftConfigs
|
|
7634
|
+
);
|
|
7635
|
+
if (lightSummaries.size > 0) {
|
|
7636
|
+
filteredData = filteredData.map((workspace) => {
|
|
7637
|
+
const lightSummary = lightSummaries.get(workspace.workspace_id);
|
|
7638
|
+
return lightSummary ? { ...workspace, lightSummary } : workspace;
|
|
7639
|
+
});
|
|
7640
|
+
}
|
|
7641
|
+
} catch (error2) {
|
|
7642
|
+
console.error("Error calculating light uptime summaries:", error2);
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
6998
7645
|
if (options.status) {
|
|
6999
7646
|
filteredData = filteredData.filter((item) => item.status === options.status);
|
|
7000
7647
|
}
|
|
@@ -7023,7 +7670,7 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7023
7670
|
}
|
|
7024
7671
|
return filteredData;
|
|
7025
7672
|
}
|
|
7026
|
-
async getWorkspaceUptimeTimeline(workspaceId, companyId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId) {
|
|
7673
|
+
async getWorkspaceUptimeTimeline(workspaceId, companyId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, lineId) {
|
|
7027
7674
|
if (!workspaceId) {
|
|
7028
7675
|
throw new Error("workspaceId is required to fetch uptime timeline");
|
|
7029
7676
|
}
|
|
@@ -7042,6 +7689,19 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7042
7689
|
const currentTiming = this.getShiftTiming(timezone, shiftConfig);
|
|
7043
7690
|
const queryDate = overrideDate ?? currentTiming.date;
|
|
7044
7691
|
const queryShiftId = overrideShiftId ?? currentTiming.shiftId;
|
|
7692
|
+
if (lineId) {
|
|
7693
|
+
const backendTimeline = await this.fetchBackendWorkspaceUptimeTimeline(
|
|
7694
|
+
workspaceId,
|
|
7695
|
+
companyId,
|
|
7696
|
+
lineId,
|
|
7697
|
+
queryDate,
|
|
7698
|
+
queryShiftId,
|
|
7699
|
+
timezone
|
|
7700
|
+
);
|
|
7701
|
+
if (backendTimeline) {
|
|
7702
|
+
return backendTimeline;
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7045
7705
|
let shiftStartDate;
|
|
7046
7706
|
let shiftEndDate;
|
|
7047
7707
|
let totalMinutes;
|
|
@@ -7090,18 +7750,17 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7090
7750
|
}
|
|
7091
7751
|
const mergedRecords = this.mergeRecordsByWorkspace(Array.isArray(data) ? data : []);
|
|
7092
7752
|
const record = mergedRecords.length > 0 ? mergedRecords[0] : null;
|
|
7093
|
-
const outputHourly =
|
|
7753
|
+
const outputHourly = normalizeOutputHourly(record?.output_hourly || {});
|
|
7094
7754
|
const outputArray = Array.isArray(record?.output_array) ? record.output_array : [];
|
|
7095
|
-
const
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
)
|
|
7104
|
-
if (!hasData) {
|
|
7755
|
+
const uptime = record ? computeWorkspaceUptime({
|
|
7756
|
+
totalMinutes,
|
|
7757
|
+
completedMinutes,
|
|
7758
|
+
shiftStartDate,
|
|
7759
|
+
outputHourly,
|
|
7760
|
+
outputArray,
|
|
7761
|
+
timezone
|
|
7762
|
+
}) : null;
|
|
7763
|
+
if (!uptime?.hasData) {
|
|
7105
7764
|
return {
|
|
7106
7765
|
shiftId: queryShiftId,
|
|
7107
7766
|
shiftLabel,
|
|
@@ -7119,80 +7778,6 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7119
7778
|
downtimeSegments: []
|
|
7120
7779
|
};
|
|
7121
7780
|
}
|
|
7122
|
-
const points = [];
|
|
7123
|
-
let uptimeMinutes = 0;
|
|
7124
|
-
let downtimeMinutes = 0;
|
|
7125
|
-
const MIN_DOWNTIME_MINUTES = 2;
|
|
7126
|
-
for (let minuteIndex = 0; minuteIndex < totalMinutes; minuteIndex++) {
|
|
7127
|
-
const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
|
|
7128
|
-
const timestamp = dateFnsTz.formatInTimeZone(minuteDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
|
7129
|
-
let status;
|
|
7130
|
-
if (minuteIndex < completedMinutes) {
|
|
7131
|
-
status = this.deriveStatusForMinute(
|
|
7132
|
-
minuteIndex,
|
|
7133
|
-
minuteDate,
|
|
7134
|
-
outputHourly,
|
|
7135
|
-
outputArray,
|
|
7136
|
-
timezone
|
|
7137
|
-
);
|
|
7138
|
-
if (status === "up") {
|
|
7139
|
-
uptimeMinutes += 1;
|
|
7140
|
-
} else {
|
|
7141
|
-
downtimeMinutes += 1;
|
|
7142
|
-
}
|
|
7143
|
-
} else {
|
|
7144
|
-
status = "pending";
|
|
7145
|
-
}
|
|
7146
|
-
points.push({
|
|
7147
|
-
minuteIndex,
|
|
7148
|
-
timestamp,
|
|
7149
|
-
status
|
|
7150
|
-
});
|
|
7151
|
-
}
|
|
7152
|
-
const downtimeSegments = [];
|
|
7153
|
-
let currentSegmentStart = null;
|
|
7154
|
-
const pushSegment = (startIndex, endIndex) => {
|
|
7155
|
-
if (endIndex <= startIndex) return;
|
|
7156
|
-
const segmentStartDate = dateFns.addMinutes(shiftStartDate, startIndex);
|
|
7157
|
-
const segmentEndDate = dateFns.addMinutes(shiftStartDate, endIndex);
|
|
7158
|
-
downtimeSegments.push({
|
|
7159
|
-
startMinuteIndex: startIndex,
|
|
7160
|
-
endMinuteIndex: endIndex,
|
|
7161
|
-
startTime: dateFnsTz.formatInTimeZone(segmentStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7162
|
-
endTime: dateFnsTz.formatInTimeZone(segmentEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7163
|
-
durationMinutes: endIndex - startIndex
|
|
7164
|
-
});
|
|
7165
|
-
};
|
|
7166
|
-
for (let i = 0; i < completedMinutes; i++) {
|
|
7167
|
-
const point = points[i];
|
|
7168
|
-
if (point.status === "down") {
|
|
7169
|
-
if (currentSegmentStart === null) {
|
|
7170
|
-
currentSegmentStart = i;
|
|
7171
|
-
}
|
|
7172
|
-
} else if (currentSegmentStart !== null) {
|
|
7173
|
-
pushSegment(currentSegmentStart, i);
|
|
7174
|
-
currentSegmentStart = null;
|
|
7175
|
-
}
|
|
7176
|
-
}
|
|
7177
|
-
if (currentSegmentStart !== null) {
|
|
7178
|
-
pushSegment(currentSegmentStart, completedMinutes);
|
|
7179
|
-
}
|
|
7180
|
-
const filteredSegments = [];
|
|
7181
|
-
downtimeSegments.forEach((segment) => {
|
|
7182
|
-
if (segment.durationMinutes >= MIN_DOWNTIME_MINUTES) {
|
|
7183
|
-
filteredSegments.push(segment);
|
|
7184
|
-
} else {
|
|
7185
|
-
for (let i = segment.startMinuteIndex; i < segment.endMinuteIndex; i++) {
|
|
7186
|
-
if (points[i] && points[i].status === "down") {
|
|
7187
|
-
points[i].status = "up";
|
|
7188
|
-
downtimeMinutes = Math.max(0, downtimeMinutes - 1);
|
|
7189
|
-
uptimeMinutes += 1;
|
|
7190
|
-
}
|
|
7191
|
-
}
|
|
7192
|
-
}
|
|
7193
|
-
});
|
|
7194
|
-
const completedWindow = Math.max(1, uptimeMinutes + downtimeMinutes);
|
|
7195
|
-
const uptimePercentage = completedMinutes > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
|
|
7196
7781
|
return {
|
|
7197
7782
|
shiftId: queryShiftId,
|
|
7198
7783
|
shiftLabel,
|
|
@@ -7200,14 +7785,14 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7200
7785
|
shiftEnd: dateFnsTz.formatInTimeZone(shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
7201
7786
|
totalMinutes,
|
|
7202
7787
|
completedMinutes,
|
|
7203
|
-
uptimeMinutes,
|
|
7204
|
-
downtimeMinutes,
|
|
7788
|
+
uptimeMinutes: uptime.uptimeMinutes,
|
|
7789
|
+
downtimeMinutes: uptime.downtimeMinutes,
|
|
7205
7790
|
pendingMinutes,
|
|
7206
|
-
uptimePercentage,
|
|
7791
|
+
uptimePercentage: uptime.uptimePercentage,
|
|
7207
7792
|
hasData: true,
|
|
7208
7793
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7209
|
-
points,
|
|
7210
|
-
downtimeSegments:
|
|
7794
|
+
points: uptime.points,
|
|
7795
|
+
downtimeSegments: uptime.downtimeSegments
|
|
7211
7796
|
};
|
|
7212
7797
|
}
|
|
7213
7798
|
async getWorkspaceHealthById(workspaceId) {
|
|
@@ -7319,7 +7904,7 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7319
7904
|
getUptimeMapKey(lineId, workspaceId) {
|
|
7320
7905
|
return lineId ? `${lineId}::${workspaceId}` : workspaceId;
|
|
7321
7906
|
}
|
|
7322
|
-
async calculateWorkspaceUptime(companyId, passedShiftConfig, timezone, lineShiftConfigs, overrideDate, overrideShiftId) {
|
|
7907
|
+
async calculateWorkspaceUptime(companyId, passedShiftConfig, timezone, lineShiftConfigs, overrideDate, overrideShiftId, lineId) {
|
|
7323
7908
|
const supabase = _getSupabaseInstance();
|
|
7324
7909
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
7325
7910
|
const dashboardConfig = _getDashboardConfigInstance();
|
|
@@ -7341,6 +7926,16 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7341
7926
|
const currentTiming = this.getShiftTiming(effectiveTimezone, shiftConfig);
|
|
7342
7927
|
const queryDate = overrideDate ?? currentTiming.date;
|
|
7343
7928
|
const queryShiftId = overrideShiftId ?? currentTiming.shiftId;
|
|
7929
|
+
const backendUptime = await this.fetchBackendWorkspaceUptimeSummaries(
|
|
7930
|
+
companyId,
|
|
7931
|
+
lineId ? [lineId] : void 0,
|
|
7932
|
+
queryDate,
|
|
7933
|
+
queryShiftId,
|
|
7934
|
+
effectiveTimezone
|
|
7935
|
+
);
|
|
7936
|
+
if (backendUptime) {
|
|
7937
|
+
return backendUptime;
|
|
7938
|
+
}
|
|
7344
7939
|
let shiftStartDate = currentTiming.shiftStartDate;
|
|
7345
7940
|
let completedMinutes = currentTiming.completedMinutes;
|
|
7346
7941
|
const isHistoricalDate = overrideDate && overrideDate !== currentTiming.date;
|
|
@@ -7364,50 +7959,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7364
7959
|
const uptimeMap = /* @__PURE__ */ new Map();
|
|
7365
7960
|
const mergedRecords = this.mergeRecordsByWorkspace(queryData || []);
|
|
7366
7961
|
for (const record of mergedRecords) {
|
|
7367
|
-
const outputHourly =
|
|
7962
|
+
const outputHourly = normalizeOutputHourly(record.output_hourly || {});
|
|
7368
7963
|
const outputArray = Array.isArray(record.output_array) ? record.output_array : [];
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
minuteDate,
|
|
7378
|
-
outputHourly,
|
|
7379
|
-
outputArray,
|
|
7380
|
-
effectiveTimezone
|
|
7381
|
-
);
|
|
7382
|
-
if (status === "down") {
|
|
7383
|
-
currentDownRun += 1;
|
|
7384
|
-
} else {
|
|
7385
|
-
if (currentDownRun > 0) {
|
|
7386
|
-
if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
|
|
7387
|
-
downtimeMinutes += currentDownRun;
|
|
7388
|
-
} else {
|
|
7389
|
-
uptimeMinutes += currentDownRun;
|
|
7390
|
-
}
|
|
7391
|
-
currentDownRun = 0;
|
|
7392
|
-
}
|
|
7393
|
-
uptimeMinutes += 1;
|
|
7394
|
-
}
|
|
7395
|
-
}
|
|
7396
|
-
if (currentDownRun > 0) {
|
|
7397
|
-
if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
|
|
7398
|
-
downtimeMinutes += currentDownRun;
|
|
7399
|
-
} else {
|
|
7400
|
-
uptimeMinutes += currentDownRun;
|
|
7401
|
-
}
|
|
7402
|
-
currentDownRun = 0;
|
|
7403
|
-
}
|
|
7404
|
-
const completedWindow = uptimeMinutes + downtimeMinutes;
|
|
7405
|
-
const percentage = completedWindow > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
|
|
7964
|
+
const uptime = computeWorkspaceUptime({
|
|
7965
|
+
totalMinutes: completedMinutes,
|
|
7966
|
+
completedMinutes,
|
|
7967
|
+
shiftStartDate,
|
|
7968
|
+
outputHourly,
|
|
7969
|
+
outputArray,
|
|
7970
|
+
timezone: effectiveTimezone
|
|
7971
|
+
});
|
|
7406
7972
|
const mapKey = this.getUptimeMapKey(record.line_id, record.workspace_id);
|
|
7407
7973
|
uptimeMap.set(mapKey, {
|
|
7408
7974
|
expectedMinutes: completedMinutes,
|
|
7409
|
-
actualMinutes: uptimeMinutes,
|
|
7410
|
-
percentage,
|
|
7975
|
+
actualMinutes: uptime.uptimeMinutes,
|
|
7976
|
+
percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100,
|
|
7411
7977
|
lastCalculated: (/* @__PURE__ */ new Date()).toISOString()
|
|
7412
7978
|
});
|
|
7413
7979
|
}
|
|
@@ -7460,6 +8026,26 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7460
8026
|
}
|
|
7461
8027
|
uniqueQueries.get(key).lineConfigs.push({ lineId, shiftStartDate, completedMinutes });
|
|
7462
8028
|
});
|
|
8029
|
+
const backendUptimeMap = /* @__PURE__ */ new Map();
|
|
8030
|
+
let backendAvailable = true;
|
|
8031
|
+
for (const { date, shiftId, lineConfigs } of uniqueQueries.values()) {
|
|
8032
|
+
const lineIds = Array.from(new Set(lineConfigs.map((lc) => lc.lineId).filter(Boolean)));
|
|
8033
|
+
const result = await this.fetchBackendWorkspaceUptimeSummaries(
|
|
8034
|
+
companyId,
|
|
8035
|
+
lineIds,
|
|
8036
|
+
date,
|
|
8037
|
+
shiftId,
|
|
8038
|
+
timezone
|
|
8039
|
+
);
|
|
8040
|
+
if (result === null) {
|
|
8041
|
+
backendAvailable = false;
|
|
8042
|
+
break;
|
|
8043
|
+
}
|
|
8044
|
+
result.forEach((value, key) => backendUptimeMap.set(key, value));
|
|
8045
|
+
}
|
|
8046
|
+
if (backendAvailable) {
|
|
8047
|
+
return backendUptimeMap;
|
|
8048
|
+
}
|
|
7463
8049
|
console.log(`[calculateWorkspaceUptimeMultiLine] Querying ${uniqueQueries.size} unique date/shift combinations for ${lineShiftConfigs.size} lines`);
|
|
7464
8050
|
uniqueQueries.forEach((queryConfig, key) => {
|
|
7465
8051
|
console.log(`[calculateWorkspaceUptimeMultiLine] Query batch ${key}:`, {
|
|
@@ -7511,49 +8097,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7511
8097
|
continue;
|
|
7512
8098
|
}
|
|
7513
8099
|
const { shiftStartDate, completedMinutes } = lineConfig;
|
|
7514
|
-
const outputHourly =
|
|
8100
|
+
const outputHourly = normalizeOutputHourly(record.output_hourly || {});
|
|
7515
8101
|
const outputArray = Array.isArray(record.output_array) ? record.output_array : [];
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
minuteDate,
|
|
7525
|
-
outputHourly,
|
|
7526
|
-
outputArray,
|
|
7527
|
-
timezone
|
|
7528
|
-
);
|
|
7529
|
-
if (status === "down") {
|
|
7530
|
-
currentDownRun += 1;
|
|
7531
|
-
} else {
|
|
7532
|
-
if (currentDownRun > 0) {
|
|
7533
|
-
if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
|
|
7534
|
-
downtimeMinutes += currentDownRun;
|
|
7535
|
-
} else {
|
|
7536
|
-
uptimeMinutes += currentDownRun;
|
|
7537
|
-
}
|
|
7538
|
-
currentDownRun = 0;
|
|
7539
|
-
}
|
|
7540
|
-
uptimeMinutes += 1;
|
|
7541
|
-
}
|
|
7542
|
-
}
|
|
7543
|
-
if (currentDownRun > 0) {
|
|
7544
|
-
if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
|
|
7545
|
-
downtimeMinutes += currentDownRun;
|
|
7546
|
-
} else {
|
|
7547
|
-
uptimeMinutes += currentDownRun;
|
|
7548
|
-
}
|
|
7549
|
-
}
|
|
7550
|
-
const completedWindow = uptimeMinutes + downtimeMinutes;
|
|
7551
|
-
const percentage = completedWindow > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
|
|
8102
|
+
const uptime = computeWorkspaceUptime({
|
|
8103
|
+
totalMinutes: completedMinutes,
|
|
8104
|
+
completedMinutes,
|
|
8105
|
+
shiftStartDate,
|
|
8106
|
+
outputHourly,
|
|
8107
|
+
outputArray,
|
|
8108
|
+
timezone
|
|
8109
|
+
});
|
|
7552
8110
|
const mapKey = this.getUptimeMapKey(record.line_id, record.workspace_id);
|
|
7553
8111
|
uptimeMap.set(mapKey, {
|
|
7554
8112
|
expectedMinutes: completedMinutes,
|
|
7555
|
-
actualMinutes: uptimeMinutes,
|
|
7556
|
-
percentage,
|
|
8113
|
+
actualMinutes: uptime.uptimeMinutes,
|
|
8114
|
+
percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100,
|
|
7557
8115
|
lastCalculated: (/* @__PURE__ */ new Date()).toISOString()
|
|
7558
8116
|
});
|
|
7559
8117
|
console.log(`[calculateWorkspaceUptimeMultiLine] Storing uptime:`, {
|
|
@@ -7562,8 +8120,8 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
|
|
|
7562
8120
|
workspaceId: record.workspace_id,
|
|
7563
8121
|
workspaceDisplayName: record.workspace_display_name,
|
|
7564
8122
|
expectedMinutes: completedMinutes,
|
|
7565
|
-
actualMinutes: uptimeMinutes,
|
|
7566
|
-
percentage
|
|
8123
|
+
actualMinutes: uptime.uptimeMinutes,
|
|
8124
|
+
percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100
|
|
7567
8125
|
});
|
|
7568
8126
|
}
|
|
7569
8127
|
}
|
|
@@ -13820,12 +14378,22 @@ var stripSeconds = (timeStr) => {
|
|
|
13820
14378
|
if (!timeStr) return timeStr;
|
|
13821
14379
|
return timeStr.substring(0, 5);
|
|
13822
14380
|
};
|
|
14381
|
+
var normalizeOffDays = (value) => {
|
|
14382
|
+
if (!value) return [];
|
|
14383
|
+
if (Array.isArray(value)) {
|
|
14384
|
+
return value.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean);
|
|
14385
|
+
}
|
|
14386
|
+
if (Array.isArray(value.off_days)) return normalizeOffDays(value.off_days);
|
|
14387
|
+
if (Array.isArray(value.offDays)) return normalizeOffDays(value.offDays);
|
|
14388
|
+
return [];
|
|
14389
|
+
};
|
|
13823
14390
|
var buildShiftConfigFromOperatingHoursRows = (rows, fallback) => {
|
|
13824
14391
|
const mapped = (rows || []).map((row) => ({
|
|
13825
14392
|
shiftId: row.shift_id,
|
|
13826
14393
|
shiftName: row.shift_name || `Shift ${row.shift_id}`,
|
|
13827
14394
|
startTime: stripSeconds(row.start_time),
|
|
13828
14395
|
endTime: stripSeconds(row.end_time),
|
|
14396
|
+
offDays: normalizeOffDays(row.off_days),
|
|
13829
14397
|
breaks: (() => {
|
|
13830
14398
|
const raw = Array.isArray(row.breaks) ? row.breaks : Array.isArray(row.breaks?.breaks) ? row.breaks.breaks : [];
|
|
13831
14399
|
return raw.map((b) => ({
|
|
@@ -13885,7 +14453,7 @@ var fetchAndStoreShiftConfig = async (supabase, lineId, fallback) => {
|
|
|
13885
14453
|
if (existing) return existing;
|
|
13886
14454
|
const promise = (async () => {
|
|
13887
14455
|
try {
|
|
13888
|
-
const { data, error } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone").eq("line_id", lineId);
|
|
14456
|
+
const { data, error } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone, off_days").eq("line_id", lineId);
|
|
13889
14457
|
if (error) {
|
|
13890
14458
|
throw new Error(`Failed to fetch shift config: ${error.message}`);
|
|
13891
14459
|
}
|
|
@@ -14578,7 +15146,7 @@ var useMultiLineShiftConfigs = (lineIds, fallbackConfig) => {
|
|
|
14578
15146
|
setError(null);
|
|
14579
15147
|
}
|
|
14580
15148
|
console.log(`[useMultiLineShiftConfigs] Fetching shift configs for ${missingLineIds.length} lines`);
|
|
14581
|
-
const { data, error: fetchError } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone").in("line_id", missingLineIds);
|
|
15149
|
+
const { data, error: fetchError } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone, off_days").in("line_id", missingLineIds);
|
|
14582
15150
|
if (fetchError) {
|
|
14583
15151
|
console.error("[useMultiLineShiftConfigs] Error fetching shift configs:", fetchError);
|
|
14584
15152
|
throw new Error(`Failed to fetch shift configs: ${fetchError.message}`);
|
|
@@ -20794,7 +21362,8 @@ var useWorkspaceUptimeTimeline = (options) => {
|
|
|
20794
21362
|
effectiveShiftConfig,
|
|
20795
21363
|
effectiveTimezone,
|
|
20796
21364
|
overrideDate,
|
|
20797
|
-
overrideShiftId
|
|
21365
|
+
overrideShiftId,
|
|
21366
|
+
lineId
|
|
20798
21367
|
);
|
|
20799
21368
|
setTimeline(data);
|
|
20800
21369
|
} catch (err) {
|
|
@@ -20804,7 +21373,89 @@ var useWorkspaceUptimeTimeline = (options) => {
|
|
|
20804
21373
|
setLoading(false);
|
|
20805
21374
|
isFetchingRef.current = false;
|
|
20806
21375
|
}
|
|
20807
|
-
}, [enabled, workspaceId, companyId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId]);
|
|
21376
|
+
}, [enabled, workspaceId, companyId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId, lineId]);
|
|
21377
|
+
React125.useEffect(() => {
|
|
21378
|
+
fetchTimeline();
|
|
21379
|
+
}, [fetchTimeline]);
|
|
21380
|
+
React125.useEffect(() => {
|
|
21381
|
+
if (!refreshInterval || refreshInterval <= 0 || !enabled) {
|
|
21382
|
+
return;
|
|
21383
|
+
}
|
|
21384
|
+
intervalRef.current = setInterval(() => {
|
|
21385
|
+
fetchTimeline();
|
|
21386
|
+
}, refreshInterval);
|
|
21387
|
+
return () => {
|
|
21388
|
+
if (intervalRef.current) {
|
|
21389
|
+
clearInterval(intervalRef.current);
|
|
21390
|
+
}
|
|
21391
|
+
};
|
|
21392
|
+
}, [refreshInterval, enabled, fetchTimeline]);
|
|
21393
|
+
return {
|
|
21394
|
+
timeline,
|
|
21395
|
+
loading: loading || shiftConfigPending,
|
|
21396
|
+
error,
|
|
21397
|
+
refetch: fetchTimeline
|
|
21398
|
+
};
|
|
21399
|
+
};
|
|
21400
|
+
var useWorkspaceLightTimeline = (options) => {
|
|
21401
|
+
const {
|
|
21402
|
+
workspaceId,
|
|
21403
|
+
lineId,
|
|
21404
|
+
enabled = true,
|
|
21405
|
+
refreshInterval,
|
|
21406
|
+
shiftConfig: passedShiftConfig,
|
|
21407
|
+
timezone: passedTimezone,
|
|
21408
|
+
date: overrideDate,
|
|
21409
|
+
shiftId: overrideShiftId
|
|
21410
|
+
} = options;
|
|
21411
|
+
const { shiftConfig: dynamicShiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(lineId);
|
|
21412
|
+
const appTimezone = useAppTimezone();
|
|
21413
|
+
const shiftConfigPending = !passedShiftConfig && (!lineId || isShiftConfigLoading || !dynamicShiftConfig);
|
|
21414
|
+
const effectiveShiftConfig = passedShiftConfig ?? (shiftConfigPending ? void 0 : dynamicShiftConfig);
|
|
21415
|
+
const effectiveTimezone = passedTimezone || appTimezone || effectiveShiftConfig?.timezone || "UTC";
|
|
21416
|
+
const [timeline, setTimeline] = React125.useState(null);
|
|
21417
|
+
const [loading, setLoading] = React125.useState(false);
|
|
21418
|
+
const [error, setError] = React125.useState(null);
|
|
21419
|
+
const isFetchingRef = React125.useRef(false);
|
|
21420
|
+
const intervalRef = React125.useRef(null);
|
|
21421
|
+
React125.useEffect(() => {
|
|
21422
|
+
setTimeline(null);
|
|
21423
|
+
setError(null);
|
|
21424
|
+
setLoading(enabled && Boolean(workspaceId));
|
|
21425
|
+
}, [workspaceId, lineId, enabled]);
|
|
21426
|
+
const fetchTimeline = React125.useCallback(async () => {
|
|
21427
|
+
if (!enabled) return;
|
|
21428
|
+
if (shiftConfigPending) {
|
|
21429
|
+
setLoading(true);
|
|
21430
|
+
return;
|
|
21431
|
+
}
|
|
21432
|
+
if (!effectiveShiftConfig || !workspaceId) {
|
|
21433
|
+
setLoading(false);
|
|
21434
|
+
setTimeline(null);
|
|
21435
|
+
return;
|
|
21436
|
+
}
|
|
21437
|
+
if (isFetchingRef.current) return;
|
|
21438
|
+
try {
|
|
21439
|
+
isFetchingRef.current = true;
|
|
21440
|
+
setLoading(true);
|
|
21441
|
+
setError(null);
|
|
21442
|
+
const data = await workspaceHealthService.getWorkspaceLightTimeline(
|
|
21443
|
+
workspaceId,
|
|
21444
|
+
lineId,
|
|
21445
|
+
effectiveShiftConfig,
|
|
21446
|
+
effectiveTimezone,
|
|
21447
|
+
overrideDate,
|
|
21448
|
+
overrideShiftId
|
|
21449
|
+
);
|
|
21450
|
+
setTimeline(data);
|
|
21451
|
+
} catch (err) {
|
|
21452
|
+
console.error("[useWorkspaceLightTimeline] Failed to fetch light timeline:", err);
|
|
21453
|
+
setError({ message: err?.message || "Failed to load light timeline", code: err?.code || "FETCH_ERROR" });
|
|
21454
|
+
} finally {
|
|
21455
|
+
setLoading(false);
|
|
21456
|
+
isFetchingRef.current = false;
|
|
21457
|
+
}
|
|
21458
|
+
}, [enabled, workspaceId, lineId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId]);
|
|
20808
21459
|
React125.useEffect(() => {
|
|
20809
21460
|
fetchTimeline();
|
|
20810
21461
|
}, [fetchTimeline]);
|
|
@@ -37808,7 +38459,12 @@ var HourlyOutputChartComponent = ({
|
|
|
37808
38459
|
}, [idleBarState.visible, idleBarState.key, idleBarState.shouldAnimate]);
|
|
37809
38460
|
const maxDataValue = Math.max(...data, 0);
|
|
37810
38461
|
const numericChartTargets = chartData.map((d) => d.target).filter((target) => target !== null && Number.isFinite(target));
|
|
37811
|
-
const
|
|
38462
|
+
const hasAuthoritativeNumericTargets = hasHourlyTargetOutputProp && numericChartTargets.length > 0;
|
|
38463
|
+
const maxTargetValue = Math.max(
|
|
38464
|
+
...numericChartTargets,
|
|
38465
|
+
hasAuthoritativeNumericTargets ? 0 : pphThreshold,
|
|
38466
|
+
0
|
|
38467
|
+
);
|
|
37812
38468
|
const maxYValue = Math.max(
|
|
37813
38469
|
Math.ceil(maxTargetValue * 1.5),
|
|
37814
38470
|
Math.ceil(maxDataValue * 1.15)
|
|
@@ -37963,14 +38619,9 @@ var HourlyOutputChartComponent = ({
|
|
|
37963
38619
|
return /* @__PURE__ */ jsxRuntime.jsx("g", { children: lines });
|
|
37964
38620
|
}, [hourlyTargetSegments, targetTimelineSegments, SHIFT_DURATION, pphThreshold, targetLineEndOffset, hasHourlyTargetOutputProp]);
|
|
37965
38621
|
const renderLegend = () => {
|
|
37966
|
-
const uniqueTargets = [...new Set(
|
|
37967
|
-
chartData.map((d) => d.target).filter((target) => target !== null && Number.isFinite(target)).map((target) => Math.round(target))
|
|
37968
|
-
)].sort((a, b) => a - b);
|
|
37969
|
-
const unitLabel = hasHourlyTargetOutputProp ? "units" : "units/hr";
|
|
37970
|
-
const targetText = uniqueTargets.length === 0 ? `Target` : uniqueTargets.length === 1 ? `Target: ${uniqueTargets[0]} ${unitLabel}` : `Target: ${uniqueTargets[0]} - ${uniqueTargets[uniqueTargets.length - 1]} ${unitLabel}`;
|
|
37971
38622
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-sm text-gray-600 bg-white py-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-gray-100 rounded-full px-3 py-1", children: [
|
|
37972
38623
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full border-t-2 border-[#E34329] border-dashed" }) }),
|
|
37973
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children:
|
|
38624
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Target" })
|
|
37974
38625
|
] }) });
|
|
37975
38626
|
};
|
|
37976
38627
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -52488,6 +53139,106 @@ var getOrdinal = (n) => {
|
|
|
52488
53139
|
const v = n % 100;
|
|
52489
53140
|
return n + (suffix[(v - 20) % 10] || suffix[v] || suffix[0]);
|
|
52490
53141
|
};
|
|
53142
|
+
var resolveDailyTargetOutput = (shiftData) => {
|
|
53143
|
+
if (!shiftData || shiftData.hasData === false) return 0;
|
|
53144
|
+
const target = Number(shiftData?.targetOutput || shiftData?.idealOutput || 0);
|
|
53145
|
+
return Number.isFinite(target) && target > 0 ? target : 0;
|
|
53146
|
+
};
|
|
53147
|
+
var getUniqueRoundedTargets = (data) => Array.from(new Set(
|
|
53148
|
+
data.map((entry) => Number(entry.targetOutput || 0)).filter((target) => Number.isFinite(target) && target > 0).map((target) => Math.round(target))
|
|
53149
|
+
)).sort((a, b) => a - b);
|
|
53150
|
+
var formatDailyTargetLegend = (targets) => {
|
|
53151
|
+
if (targets.length === 0) return "";
|
|
53152
|
+
if (targets.length === 1) return `Target: ${targets[0].toLocaleString()} units/day`;
|
|
53153
|
+
return `Target: ${targets[0].toLocaleString()} - ${targets[targets.length - 1].toLocaleString()} units/day`;
|
|
53154
|
+
};
|
|
53155
|
+
var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
53156
|
+
var getShiftOffDays = (shiftConfig, selectedShiftId) => {
|
|
53157
|
+
const shift = shiftConfig?.shifts?.find((candidate) => candidate.shiftId === selectedShiftId);
|
|
53158
|
+
const raw = shift?.offDays || shift?.off_days || [];
|
|
53159
|
+
return Array.isArray(raw) ? raw.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean) : [];
|
|
53160
|
+
};
|
|
53161
|
+
var isScheduledOffDay = (dateKey, shiftConfig, selectedShiftId) => {
|
|
53162
|
+
const offDays = getShiftOffDays(shiftConfig, selectedShiftId);
|
|
53163
|
+
if (offDays.length === 0) return false;
|
|
53164
|
+
const date = parseDateKeyToDate(dateKey);
|
|
53165
|
+
return offDays.includes(WEEKDAY_NAMES[date.getDay()]);
|
|
53166
|
+
};
|
|
53167
|
+
var renderDailyOutputCapsules = (props, data) => {
|
|
53168
|
+
const { offset, yAxisMap } = props;
|
|
53169
|
+
if (!offset || !yAxisMap || data.length === 0) return null;
|
|
53170
|
+
const { left, width } = offset;
|
|
53171
|
+
const yAxis = yAxisMap.default || yAxisMap[0];
|
|
53172
|
+
if (!Number.isFinite(left) || !Number.isFinite(width) || width <= 0 || !yAxis?.scale) {
|
|
53173
|
+
return null;
|
|
53174
|
+
}
|
|
53175
|
+
const slotWidth = width / data.length;
|
|
53176
|
+
const capsuleWidth = Math.max(10, Math.min(28, slotWidth * 0.44));
|
|
53177
|
+
const radius = capsuleWidth / 2;
|
|
53178
|
+
const baseY = yAxis.scale(0);
|
|
53179
|
+
const capsules = [];
|
|
53180
|
+
data.forEach((entry, index) => {
|
|
53181
|
+
const target = Number(entry.targetOutput || 0);
|
|
53182
|
+
const output = Number(entry.output || 0);
|
|
53183
|
+
if ((!Number.isFinite(target) || target <= 0) && (!Number.isFinite(output) || output <= 0)) return;
|
|
53184
|
+
const x = left + index * slotWidth + (slotWidth - capsuleWidth) / 2;
|
|
53185
|
+
const targetTop = target > 0 ? yAxis.scale(target) : yAxis.scale(output);
|
|
53186
|
+
const capsuleTop = Math.min(targetTop, baseY - 4);
|
|
53187
|
+
const capsuleHeight = Math.max(baseY - capsuleTop, 4);
|
|
53188
|
+
const fillValue = target > 0 ? Math.min(Math.max(output, 0), target) : Math.max(output, 0);
|
|
53189
|
+
const fillTop = fillValue > 0 ? Math.max(yAxis.scale(fillValue), capsuleTop) : baseY;
|
|
53190
|
+
const fillHeight = Math.max(baseY - fillTop, 0);
|
|
53191
|
+
const fillColor = target > 0 ? output >= target ? "#00AB45" : "#E34329" : entry.color || "#6b7280";
|
|
53192
|
+
const trackFill = output >= target ? "#f0fdf4" : "#fff5f3";
|
|
53193
|
+
const trackStroke = output >= target ? "#00AB45" : "#E34329";
|
|
53194
|
+
const clipId = `line-daily-output-capsule-${index}`;
|
|
53195
|
+
capsules.push(
|
|
53196
|
+
/* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
53197
|
+
/* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
53198
|
+
"rect",
|
|
53199
|
+
{
|
|
53200
|
+
x,
|
|
53201
|
+
y: capsuleTop,
|
|
53202
|
+
width: capsuleWidth,
|
|
53203
|
+
height: capsuleHeight,
|
|
53204
|
+
rx: radius,
|
|
53205
|
+
ry: radius
|
|
53206
|
+
}
|
|
53207
|
+
) }) }),
|
|
53208
|
+
target > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
53209
|
+
"rect",
|
|
53210
|
+
{
|
|
53211
|
+
"data-testid": "daily-target-capsule-track",
|
|
53212
|
+
x,
|
|
53213
|
+
y: capsuleTop,
|
|
53214
|
+
width: capsuleWidth,
|
|
53215
|
+
height: capsuleHeight,
|
|
53216
|
+
rx: radius,
|
|
53217
|
+
ry: radius,
|
|
53218
|
+
fill: trackFill,
|
|
53219
|
+
stroke: trackStroke,
|
|
53220
|
+
strokeWidth: 1.5
|
|
53221
|
+
}
|
|
53222
|
+
),
|
|
53223
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
53224
|
+
"rect",
|
|
53225
|
+
{
|
|
53226
|
+
"data-testid": "daily-output-capsule-fill",
|
|
53227
|
+
x,
|
|
53228
|
+
y: fillTop,
|
|
53229
|
+
width: capsuleWidth,
|
|
53230
|
+
height: fillHeight,
|
|
53231
|
+
rx: fillHeight >= capsuleHeight ? radius : 0,
|
|
53232
|
+
ry: fillHeight >= capsuleHeight ? radius : 0,
|
|
53233
|
+
fill: fillColor,
|
|
53234
|
+
clipPath: target > 0 ? `url(#${clipId})` : void 0
|
|
53235
|
+
}
|
|
53236
|
+
)
|
|
53237
|
+
] }, `daily-output-capsule-${index}`)
|
|
53238
|
+
);
|
|
53239
|
+
});
|
|
53240
|
+
return capsules.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("g", { children: capsules }) : null;
|
|
53241
|
+
};
|
|
52491
53242
|
var CustomTooltip2 = ({ active, payload, label, isUptimeMode }) => {
|
|
52492
53243
|
if (!active || !payload || payload.length === 0) return null;
|
|
52493
53244
|
if (isUptimeMode) {
|
|
@@ -52571,6 +53322,7 @@ var LineMonthlyHistory = ({
|
|
|
52571
53322
|
legend,
|
|
52572
53323
|
monitoringMode,
|
|
52573
53324
|
lineAssembly = false,
|
|
53325
|
+
shiftConfig,
|
|
52574
53326
|
underperformingWorkspaces = {},
|
|
52575
53327
|
lineId,
|
|
52576
53328
|
selectedShiftId = 0,
|
|
@@ -52588,6 +53340,13 @@ var LineMonthlyHistory = ({
|
|
|
52588
53340
|
const { isIdleTimeVlmEnabled } = useIdleTimeVlmConfig();
|
|
52589
53341
|
const idleTimeVlmEnabled = isIdleTimeVlmEnabled(lineId);
|
|
52590
53342
|
const isUptimeMode = monitoringMode === "uptime";
|
|
53343
|
+
const [isMobile, setIsMobile] = React125.useState(false);
|
|
53344
|
+
React125.useEffect(() => {
|
|
53345
|
+
const checkMobile = () => setIsMobile(window.innerWidth < 640);
|
|
53346
|
+
checkMobile();
|
|
53347
|
+
window.addEventListener("resize", checkMobile);
|
|
53348
|
+
return () => window.removeEventListener("resize", checkMobile);
|
|
53349
|
+
}, []);
|
|
52591
53350
|
const chartKey = React125.useMemo(() => `${lineId}-${month}-${year}-${selectedShiftId}-${rangeStart}-${rangeEnd}`, [lineId, month, year, selectedShiftId, rangeStart, rangeEnd]);
|
|
52592
53351
|
const monthBounds = React125.useMemo(() => getMonthKeyBounds(year, month), [year, month]);
|
|
52593
53352
|
const normalizedRange = React125.useMemo(() => {
|
|
@@ -52749,29 +53508,21 @@ var LineMonthlyHistory = ({
|
|
|
52749
53508
|
});
|
|
52750
53509
|
}
|
|
52751
53510
|
const yAxisMax2 = maxHours > 0 ? 100 : 1;
|
|
52752
|
-
return { data: dailyData2, maxOutput: 0,
|
|
53511
|
+
return { data: dailyData2, maxOutput: 0, targetValues: [], yAxisMax: yAxisMax2, targetLegend: "" };
|
|
52753
53512
|
}
|
|
52754
53513
|
const dailyData = [];
|
|
52755
53514
|
let maxOutput = 0;
|
|
52756
|
-
let
|
|
52757
|
-
for (let i = rangeDateKeys.length - 1; i >= 0; i--) {
|
|
52758
|
-
const dayKey = rangeDateKeys[i];
|
|
52759
|
-
const dayData = analysisMonthlyDataByKey.get(dayKey);
|
|
52760
|
-
const shiftData = dayData ? getShiftData2(dayData, selectedShiftId) : null;
|
|
52761
|
-
const idealOutput = shiftData ? shiftData.idealOutput || 0 : 0;
|
|
52762
|
-
if (idealOutput > 0) {
|
|
52763
|
-
lastSetTarget = idealOutput;
|
|
52764
|
-
break;
|
|
52765
|
-
}
|
|
52766
|
-
}
|
|
53515
|
+
let maxTarget = 0;
|
|
52767
53516
|
for (const dayKey of rangeDateKeys) {
|
|
52768
53517
|
const day = Number(dayKey.slice(-2));
|
|
52769
53518
|
const dayData = analysisMonthlyDataByKey.get(dayKey);
|
|
53519
|
+
const isOffDay = isScheduledOffDay(dayKey, shiftConfig, selectedShiftId);
|
|
52770
53520
|
const shiftData = dayData ? getShiftData2(dayData, selectedShiftId) : null;
|
|
52771
|
-
const output = shiftData && hasRealData(shiftData) ? shiftData.output || 0 : 0;
|
|
52772
|
-
const
|
|
53521
|
+
const output = !isOffDay && shiftData && hasRealData(shiftData) ? shiftData.output || 0 : 0;
|
|
53522
|
+
const targetOutput = isOffDay ? 0 : resolveDailyTargetOutput(shiftData);
|
|
52773
53523
|
if (output > maxOutput) maxOutput = output;
|
|
52774
|
-
|
|
53524
|
+
if (targetOutput > maxTarget) maxTarget = targetOutput;
|
|
53525
|
+
const color2 = targetOutput > 0 && output >= targetOutput ? "#00AB45" : "#E34329";
|
|
52775
53526
|
dailyData.push({
|
|
52776
53527
|
hour: getOrdinal(day),
|
|
52777
53528
|
// Using ordinal format (1st, 2nd, 3rd, etc.)
|
|
@@ -52779,17 +53530,25 @@ var LineMonthlyHistory = ({
|
|
|
52779
53530
|
output,
|
|
52780
53531
|
originalOutput: output,
|
|
52781
53532
|
// For label display
|
|
52782
|
-
idealOutput,
|
|
53533
|
+
idealOutput: targetOutput,
|
|
53534
|
+
targetOutput,
|
|
52783
53535
|
color: color2
|
|
52784
53536
|
});
|
|
52785
53537
|
}
|
|
52786
|
-
const calculatedMax = Math.max(maxOutput,
|
|
53538
|
+
const calculatedMax = Math.max(maxOutput, maxTarget);
|
|
52787
53539
|
const yAxisMax = calculatedMax > 0 ? calculatedMax * 1.1 : 100;
|
|
52788
|
-
|
|
52789
|
-
|
|
53540
|
+
const targetValues = getUniqueRoundedTargets(dailyData);
|
|
53541
|
+
return {
|
|
53542
|
+
data: dailyData,
|
|
53543
|
+
maxOutput,
|
|
53544
|
+
targetValues,
|
|
53545
|
+
yAxisMax,
|
|
53546
|
+
targetLegend: formatDailyTargetLegend(targetValues)
|
|
53547
|
+
};
|
|
53548
|
+
}, [analysisMonthlyDataByKey, normalizedRange.endKey, normalizedRange.startKey, selectedShiftId, isUptimeMode, timezone, shiftConfig]);
|
|
52790
53549
|
const yAxisTicks = React125.useMemo(() => {
|
|
52791
53550
|
const max = chartData.yAxisMax;
|
|
52792
|
-
const
|
|
53551
|
+
const targets = chartData.targetValues || [];
|
|
52793
53552
|
if (!max || max <= 0) return void 0;
|
|
52794
53553
|
const desiredIntervals = 4;
|
|
52795
53554
|
const roughStep = max / desiredIntervals;
|
|
@@ -52803,11 +53562,18 @@ var LineMonthlyHistory = ({
|
|
|
52803
53562
|
for (let v = 0; v <= max; v += step) {
|
|
52804
53563
|
ticks.push(Math.round(v));
|
|
52805
53564
|
}
|
|
52806
|
-
|
|
52807
|
-
ticks.push(Math.round(target));
|
|
52808
|
-
}
|
|
53565
|
+
targets.forEach((target) => ticks.push(Math.round(target)));
|
|
52809
53566
|
return Array.from(new Set(ticks)).filter((v) => v >= 0 && v <= max).sort((a, b) => a - b);
|
|
52810
|
-
}, [chartData.yAxisMax, chartData.
|
|
53567
|
+
}, [chartData.yAxisMax, chartData.targetValues]);
|
|
53568
|
+
const visibleYAxisTicks = React125.useMemo(() => {
|
|
53569
|
+
if (!isMobile || isUptimeMode || !yAxisTicks || yAxisTicks.length <= 3) return yAxisTicks;
|
|
53570
|
+
const importantTicks = /* @__PURE__ */ new Set([
|
|
53571
|
+
0,
|
|
53572
|
+
Math.round(chartData.yAxisMax),
|
|
53573
|
+
...chartData.targetValues || []
|
|
53574
|
+
]);
|
|
53575
|
+
return yAxisTicks.filter((tick) => importantTicks.has(Math.round(tick))).sort((a, b) => a - b).slice(-3);
|
|
53576
|
+
}, [chartData.targetValues, chartData.yAxisMax, isMobile, isUptimeMode, yAxisTicks]);
|
|
52811
53577
|
const pieChartData = React125.useMemo(() => {
|
|
52812
53578
|
if (!isUptimeMode) return [];
|
|
52813
53579
|
const validShifts = (analysisMonthlyData || []).map((day) => getShiftData2(day, selectedShiftId)).filter(
|
|
@@ -53126,36 +53892,35 @@ var LineMonthlyHistory = ({
|
|
|
53126
53892
|
] }),
|
|
53127
53893
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-lg sm:rounded-xl shadow-sm border border-gray-100 p-2 sm:p-3 lg:p-4", children: [
|
|
53128
53894
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs sm:text-sm font-bold text-gray-700 mb-1 sm:mb-2 text-left", children: isUptimeMode ? "Daily Utilization" : "Daily Output" }),
|
|
53129
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[
|
|
53895
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[140px] sm:h-[180px] lg:h-[220px]", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
53130
53896
|
recharts.BarChart,
|
|
53131
53897
|
{
|
|
53132
53898
|
data: chartData.data,
|
|
53133
|
-
margin: { top: 20, right: 10, bottom: 40, left: 10 },
|
|
53899
|
+
margin: isMobile ? { top: 8, right: 4, bottom: 28, left: 0 } : { top: 20, right: 10, bottom: 40, left: 10 },
|
|
53134
53900
|
children: [
|
|
53135
53901
|
/* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#f3f4f6" }),
|
|
53136
53902
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
53137
53903
|
recharts.XAxis,
|
|
53138
53904
|
{
|
|
53139
53905
|
dataKey: "hour",
|
|
53140
|
-
tick: { fontSize: 10, fill: "#6b7280" },
|
|
53141
|
-
interval: 0,
|
|
53906
|
+
tick: { fontSize: isMobile ? 9 : 10, fill: "#6b7280" },
|
|
53907
|
+
interval: isMobile ? 3 : 0,
|
|
53142
53908
|
angle: -45,
|
|
53143
53909
|
textAnchor: "end",
|
|
53144
|
-
height: 60
|
|
53910
|
+
height: isMobile ? 42 : 60
|
|
53145
53911
|
}
|
|
53146
53912
|
),
|
|
53147
53913
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
53148
53914
|
recharts.YAxis,
|
|
53149
53915
|
{
|
|
53150
53916
|
domain: [0, chartData.yAxisMax],
|
|
53151
|
-
width: 40,
|
|
53152
|
-
ticks: isUptimeMode ? [0, 25, 50, 75, 100] :
|
|
53917
|
+
width: isMobile ? 34 : 40,
|
|
53918
|
+
ticks: isUptimeMode ? [0, 25, 50, 75, 100] : visibleYAxisTicks,
|
|
53153
53919
|
tickFormatter: isUptimeMode ? (value) => `${value}%` : void 0,
|
|
53154
53920
|
tick: isUptimeMode ? void 0 : (props) => {
|
|
53155
53921
|
const { x, y, payload } = props;
|
|
53156
53922
|
const value = Math.round(payload.value);
|
|
53157
|
-
const
|
|
53158
|
-
const isTarget = value === targetValue && targetValue > 0;
|
|
53923
|
+
const isTarget = (chartData.targetValues || []).includes(value);
|
|
53159
53924
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
53160
53925
|
"text",
|
|
53161
53926
|
{
|
|
@@ -53178,15 +53943,7 @@ var LineMonthlyHistory = ({
|
|
|
53178
53943
|
content: (props) => /* @__PURE__ */ jsxRuntime.jsx(CustomTooltip2, { ...props, isUptimeMode })
|
|
53179
53944
|
}
|
|
53180
53945
|
),
|
|
53181
|
-
!isUptimeMode &&
|
|
53182
|
-
recharts.ReferenceLine,
|
|
53183
|
-
{
|
|
53184
|
-
y: chartData.lastSetTarget,
|
|
53185
|
-
stroke: "#E34329",
|
|
53186
|
-
strokeDasharray: "5 5",
|
|
53187
|
-
strokeWidth: 2
|
|
53188
|
-
}
|
|
53189
|
-
),
|
|
53946
|
+
!isUptimeMode && /* @__PURE__ */ jsxRuntime.jsx(recharts.Customized, { component: (props) => renderDailyOutputCapsules(props, chartData.data) }),
|
|
53190
53947
|
isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
53191
53948
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
53192
53949
|
recharts.Bar,
|
|
@@ -53219,6 +53976,8 @@ var LineMonthlyHistory = ({
|
|
|
53219
53976
|
{
|
|
53220
53977
|
dataKey: "output",
|
|
53221
53978
|
radius: [4, 4, 0, 0],
|
|
53979
|
+
fill: "transparent",
|
|
53980
|
+
opacity: 0,
|
|
53222
53981
|
isAnimationActive: true,
|
|
53223
53982
|
animationBegin: 0,
|
|
53224
53983
|
animationDuration: 1e3,
|
|
@@ -53237,13 +53996,9 @@ var LineMonthlyHistory = ({
|
|
|
53237
53996
|
},
|
|
53238
53997
|
chartKey
|
|
53239
53998
|
) }) }),
|
|
53240
|
-
!isUptimeMode && chartData.
|
|
53241
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-
|
|
53242
|
-
/* @__PURE__ */ jsxRuntime.
|
|
53243
|
-
"Target: ",
|
|
53244
|
-
Math.round(chartData.lastSetTarget),
|
|
53245
|
-
" units/day"
|
|
53246
|
-
] })
|
|
53999
|
+
!isUptimeMode && chartData.targetValues.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-sm text-gray-600 bg-white py-1 pt-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-gray-100 rounded-full px-3 py-1", children: [
|
|
54000
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-5 w-3 rounded-full border border-[#E34329] bg-[#fff5f3] overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-x-0 bottom-0 h-1/2 bg-[#E34329]" }) }),
|
|
54001
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: chartData.targetLegend })
|
|
53247
54002
|
] }) })
|
|
53248
54003
|
] })
|
|
53249
54004
|
] })
|
|
@@ -55166,6 +55921,106 @@ var formatCycleSeconds = (value) => {
|
|
|
55166
55921
|
if (!Number.isFinite(value)) return "0.0s";
|
|
55167
55922
|
return `${value.toFixed(1)}s`;
|
|
55168
55923
|
};
|
|
55924
|
+
var resolveDailyTargetOutput2 = (shiftData) => {
|
|
55925
|
+
if (!shiftData || shiftData.hasData === false) return 0;
|
|
55926
|
+
const target = Number(shiftData?.targetOutput || shiftData?.idealOutput || 0);
|
|
55927
|
+
return Number.isFinite(target) && target > 0 ? target : 0;
|
|
55928
|
+
};
|
|
55929
|
+
var getUniqueRoundedTargets2 = (data) => Array.from(new Set(
|
|
55930
|
+
data.map((entry) => Number(entry.targetOutput || 0)).filter((target) => Number.isFinite(target) && target > 0).map((target) => Math.round(target))
|
|
55931
|
+
)).sort((a, b) => a - b);
|
|
55932
|
+
var formatDailyTargetLegend2 = (targets) => {
|
|
55933
|
+
if (targets.length === 0) return "";
|
|
55934
|
+
if (targets.length === 1) return `Target: ${targets[0].toLocaleString()} units/day`;
|
|
55935
|
+
return `Target: ${targets[0].toLocaleString()} - ${targets[targets.length - 1].toLocaleString()} units/day`;
|
|
55936
|
+
};
|
|
55937
|
+
var WEEKDAY_NAMES2 = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
55938
|
+
var getShiftOffDays2 = (shiftConfig, selectedShiftId) => {
|
|
55939
|
+
const shift = shiftConfig?.shifts?.find((candidate) => candidate.shiftId === selectedShiftId);
|
|
55940
|
+
const raw = shift?.offDays || shift?.off_days || [];
|
|
55941
|
+
return Array.isArray(raw) ? raw.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean) : [];
|
|
55942
|
+
};
|
|
55943
|
+
var isScheduledOffDay2 = (dateKey, shiftConfig, selectedShiftId) => {
|
|
55944
|
+
const offDays = getShiftOffDays2(shiftConfig, selectedShiftId);
|
|
55945
|
+
if (offDays.length === 0) return false;
|
|
55946
|
+
const date = parseDateKeyToDate(dateKey);
|
|
55947
|
+
return offDays.includes(WEEKDAY_NAMES2[date.getDay()]);
|
|
55948
|
+
};
|
|
55949
|
+
var renderDailyOutputCapsules2 = (props, data) => {
|
|
55950
|
+
const { offset, yAxisMap } = props;
|
|
55951
|
+
if (!offset || !yAxisMap || data.length === 0) return null;
|
|
55952
|
+
const { left, width } = offset;
|
|
55953
|
+
const yAxis = yAxisMap.default || yAxisMap[0];
|
|
55954
|
+
if (!Number.isFinite(left) || !Number.isFinite(width) || width <= 0 || !yAxis?.scale) {
|
|
55955
|
+
return null;
|
|
55956
|
+
}
|
|
55957
|
+
const slotWidth = width / data.length;
|
|
55958
|
+
const capsuleWidth = Math.max(10, Math.min(28, slotWidth * 0.44));
|
|
55959
|
+
const radius = capsuleWidth / 2;
|
|
55960
|
+
const baseY = yAxis.scale(0);
|
|
55961
|
+
const capsules = [];
|
|
55962
|
+
data.forEach((entry, index) => {
|
|
55963
|
+
const target = Number(entry.targetOutput || 0);
|
|
55964
|
+
const output = Number(entry.output || 0);
|
|
55965
|
+
if ((!Number.isFinite(target) || target <= 0) && (!Number.isFinite(output) || output <= 0)) return;
|
|
55966
|
+
const x = left + index * slotWidth + (slotWidth - capsuleWidth) / 2;
|
|
55967
|
+
const targetTop = target > 0 ? yAxis.scale(target) : yAxis.scale(output);
|
|
55968
|
+
const capsuleTop = Math.min(targetTop, baseY - 4);
|
|
55969
|
+
const capsuleHeight = Math.max(baseY - capsuleTop, 4);
|
|
55970
|
+
const fillValue = target > 0 ? Math.min(Math.max(output, 0), target) : Math.max(output, 0);
|
|
55971
|
+
const fillTop = fillValue > 0 ? Math.max(yAxis.scale(fillValue), capsuleTop) : baseY;
|
|
55972
|
+
const fillHeight = Math.max(baseY - fillTop, 0);
|
|
55973
|
+
const fillColor = target > 0 ? output >= target ? "#00AB45" : "#E34329" : entry.color || "#6b7280";
|
|
55974
|
+
const trackFill = output >= target ? "#f0fdf4" : "#fff5f3";
|
|
55975
|
+
const trackStroke = output >= target ? "#00AB45" : "#E34329";
|
|
55976
|
+
const clipId = `workspace-daily-output-capsule-${index}`;
|
|
55977
|
+
capsules.push(
|
|
55978
|
+
/* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
55979
|
+
/* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
55980
|
+
"rect",
|
|
55981
|
+
{
|
|
55982
|
+
x,
|
|
55983
|
+
y: capsuleTop,
|
|
55984
|
+
width: capsuleWidth,
|
|
55985
|
+
height: capsuleHeight,
|
|
55986
|
+
rx: radius,
|
|
55987
|
+
ry: radius
|
|
55988
|
+
}
|
|
55989
|
+
) }) }),
|
|
55990
|
+
target > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
55991
|
+
"rect",
|
|
55992
|
+
{
|
|
55993
|
+
"data-testid": "daily-target-capsule-track",
|
|
55994
|
+
x,
|
|
55995
|
+
y: capsuleTop,
|
|
55996
|
+
width: capsuleWidth,
|
|
55997
|
+
height: capsuleHeight,
|
|
55998
|
+
rx: radius,
|
|
55999
|
+
ry: radius,
|
|
56000
|
+
fill: trackFill,
|
|
56001
|
+
stroke: trackStroke,
|
|
56002
|
+
strokeWidth: 1.5
|
|
56003
|
+
}
|
|
56004
|
+
),
|
|
56005
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
56006
|
+
"rect",
|
|
56007
|
+
{
|
|
56008
|
+
"data-testid": "daily-output-capsule-fill",
|
|
56009
|
+
x,
|
|
56010
|
+
y: fillTop,
|
|
56011
|
+
width: capsuleWidth,
|
|
56012
|
+
height: fillHeight,
|
|
56013
|
+
rx: fillHeight >= capsuleHeight ? radius : 0,
|
|
56014
|
+
ry: fillHeight >= capsuleHeight ? radius : 0,
|
|
56015
|
+
fill: fillColor,
|
|
56016
|
+
clipPath: target > 0 ? `url(#${clipId})` : void 0
|
|
56017
|
+
}
|
|
56018
|
+
)
|
|
56019
|
+
] }, `daily-output-capsule-${index}`)
|
|
56020
|
+
);
|
|
56021
|
+
});
|
|
56022
|
+
return capsules.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("g", { children: capsules }) : null;
|
|
56023
|
+
};
|
|
55169
56024
|
var CustomTooltip3 = ({ active, payload, label, isUptimeMode }) => {
|
|
55170
56025
|
if (!active || !payload || payload.length === 0) return null;
|
|
55171
56026
|
if (isUptimeMode) {
|
|
@@ -55345,28 +56200,20 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55345
56200
|
});
|
|
55346
56201
|
}
|
|
55347
56202
|
const yAxisMax2 = maxHours > 0 ? maxHours * 1.1 : 1;
|
|
55348
|
-
return { data: dailyData, yAxisMax: yAxisMax2,
|
|
56203
|
+
return { data: dailyData, yAxisMax: yAxisMax2, targetValues: [], targetLegend: "" };
|
|
55349
56204
|
}
|
|
55350
56205
|
let maxOutput = 0;
|
|
55351
|
-
let
|
|
55352
|
-
for (let i = rangeDateKeys.length - 1; i >= 0; i--) {
|
|
55353
|
-
const dateKey = rangeDateKeys[i];
|
|
55354
|
-
const dayData = analysisMonthlyDataByKey.get(dateKey);
|
|
55355
|
-
const shiftData = dayData ? getShiftData(dayData, selectedShiftId) : null;
|
|
55356
|
-
const idealOutput = shiftData ? shiftData.idealOutput : 0;
|
|
55357
|
-
if (idealOutput > 0) {
|
|
55358
|
-
lastSetTarget = idealOutput;
|
|
55359
|
-
break;
|
|
55360
|
-
}
|
|
55361
|
-
}
|
|
56206
|
+
let maxTarget = 0;
|
|
55362
56207
|
for (const dateKey of rangeDateKeys) {
|
|
55363
56208
|
const dayData = analysisMonthlyDataByKey.get(dateKey);
|
|
55364
56209
|
const dayNumber = Number(dateKey.slice(-2));
|
|
56210
|
+
const isOffDay = isScheduledOffDay2(dateKey, shiftConfig, selectedShiftId);
|
|
55365
56211
|
const shiftData = dayData ? getShiftData(dayData, selectedShiftId) : null;
|
|
55366
|
-
const output = shiftData && hasRealData(shiftData) ? shiftData.output : 0;
|
|
55367
|
-
const
|
|
56212
|
+
const output = !isOffDay && shiftData && hasRealData(shiftData) ? shiftData.output : 0;
|
|
56213
|
+
const targetOutput = isOffDay ? 0 : resolveDailyTargetOutput2(shiftData);
|
|
55368
56214
|
if (output > maxOutput) maxOutput = output;
|
|
55369
|
-
|
|
56215
|
+
if (targetOutput > maxTarget) maxTarget = targetOutput;
|
|
56216
|
+
const color2 = targetOutput > 0 && output >= targetOutput ? "#00AB45" : "#E34329";
|
|
55370
56217
|
dailyData.push({
|
|
55371
56218
|
hour: getOrdinal2(dayNumber),
|
|
55372
56219
|
// Using ordinal format (1st, 2nd, 3rd, etc.)
|
|
@@ -55374,21 +56221,29 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55374
56221
|
output,
|
|
55375
56222
|
originalOutput: output,
|
|
55376
56223
|
// For label display
|
|
55377
|
-
idealOutput,
|
|
56224
|
+
idealOutput: targetOutput,
|
|
56225
|
+
targetOutput,
|
|
55378
56226
|
efficiency: shiftData && hasRealData(shiftData) ? shiftData.efficiency : 0,
|
|
55379
56227
|
color: color2,
|
|
55380
56228
|
idleMinutes: 0
|
|
55381
56229
|
// Not used but keeps structure consistent
|
|
55382
56230
|
});
|
|
55383
56231
|
}
|
|
55384
|
-
const calculatedMax = Math.max(maxOutput,
|
|
56232
|
+
const calculatedMax = Math.max(maxOutput, maxTarget);
|
|
55385
56233
|
const yAxisMax = calculatedMax > 0 ? calculatedMax * 1.1 : 100;
|
|
55386
|
-
|
|
55387
|
-
|
|
56234
|
+
const targetValues = getUniqueRoundedTargets2(dailyData);
|
|
56235
|
+
return {
|
|
56236
|
+
data: dailyData,
|
|
56237
|
+
maxOutput,
|
|
56238
|
+
targetValues,
|
|
56239
|
+
yAxisMax,
|
|
56240
|
+
targetLegend: formatDailyTargetLegend2(targetValues)
|
|
56241
|
+
};
|
|
56242
|
+
}, [analysisMonthlyDataByKey, rangeDateKeys, selectedShiftId, isUptimeMode, shiftConfig, shiftWorkSeconds]);
|
|
55388
56243
|
const yAxisTicks = React125.useMemo(() => {
|
|
55389
56244
|
if (isUptimeMode) return void 0;
|
|
55390
56245
|
const max = chartData.yAxisMax;
|
|
55391
|
-
const
|
|
56246
|
+
const targets = chartData.targetValues || [];
|
|
55392
56247
|
if (!max || max <= 0) return void 0;
|
|
55393
56248
|
const desiredIntervals = 4;
|
|
55394
56249
|
const roughStep = max / desiredIntervals;
|
|
@@ -55402,7 +56257,7 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55402
56257
|
for (let v = 0; v <= max; v += step) {
|
|
55403
56258
|
ticks.push(Math.round(v));
|
|
55404
56259
|
}
|
|
55405
|
-
|
|
56260
|
+
targets.forEach((target) => {
|
|
55406
56261
|
const roundedTarget = Math.round(target);
|
|
55407
56262
|
if (!ticks.includes(roundedTarget)) {
|
|
55408
56263
|
let nearestIndex = -1;
|
|
@@ -55421,9 +56276,18 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55421
56276
|
ticks.push(roundedTarget);
|
|
55422
56277
|
}
|
|
55423
56278
|
}
|
|
55424
|
-
}
|
|
56279
|
+
});
|
|
55425
56280
|
return ticks.filter((v) => v >= 0 && v <= max * 1.05).sort((a, b) => a - b);
|
|
55426
|
-
}, [chartData.yAxisMax, chartData.
|
|
56281
|
+
}, [chartData.yAxisMax, chartData.targetValues, isUptimeMode]);
|
|
56282
|
+
const visibleYAxisTicks = React125.useMemo(() => {
|
|
56283
|
+
if (!isMobile || isUptimeMode || !yAxisTicks || yAxisTicks.length <= 3) return yAxisTicks;
|
|
56284
|
+
const importantTicks = /* @__PURE__ */ new Set([
|
|
56285
|
+
0,
|
|
56286
|
+
Math.round(chartData.yAxisMax),
|
|
56287
|
+
...chartData.targetValues || []
|
|
56288
|
+
]);
|
|
56289
|
+
return yAxisTicks.filter((tick) => importantTicks.has(Math.round(tick))).sort((a, b) => a - b).slice(-3);
|
|
56290
|
+
}, [chartData.targetValues, chartData.yAxisMax, isMobile, isUptimeMode, yAxisTicks]);
|
|
55427
56291
|
const pieChartData = React125.useMemo(() => {
|
|
55428
56292
|
const aggregateMode = isUptimeMode ? "uptime" : "output";
|
|
55429
56293
|
const validShifts = analysisMonthlyData.map((d) => getShiftData(d, selectedShiftId)).filter(
|
|
@@ -55813,22 +56677,22 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55813
56677
|
] }),
|
|
55814
56678
|
(!isAssemblyWorkspace || isUptimeMode) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-100 p-4 flex-1", children: [
|
|
55815
56679
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-bold text-gray-700 mb-3 text-left", children: isUptimeMode ? "Daily Utilization" : "Daily Output" }),
|
|
55816
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: "220px" }, children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
56680
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: isMobile ? "150px" : "220px" }, children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
55817
56681
|
recharts.BarChart,
|
|
55818
56682
|
{
|
|
55819
56683
|
data: chartData.data,
|
|
55820
|
-
margin: { top: 20, right: 10, bottom: 40, left: 10 },
|
|
56684
|
+
margin: isMobile ? { top: 8, right: 4, bottom: 28, left: 0 } : { top: 20, right: 10, bottom: 40, left: 10 },
|
|
55821
56685
|
children: [
|
|
55822
56686
|
/* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#f3f4f6" }),
|
|
55823
56687
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
55824
56688
|
recharts.XAxis,
|
|
55825
56689
|
{
|
|
55826
56690
|
dataKey: "hour",
|
|
55827
|
-
tick: { fontSize: 10, fill: "#6b7280" },
|
|
55828
|
-
interval: isMobile ?
|
|
56691
|
+
tick: { fontSize: isMobile ? 9 : 10, fill: "#6b7280" },
|
|
56692
|
+
interval: isMobile ? 3 : 0,
|
|
55829
56693
|
angle: -45,
|
|
55830
56694
|
textAnchor: "end",
|
|
55831
|
-
height: 60
|
|
56695
|
+
height: isMobile ? 42 : 60
|
|
55832
56696
|
}
|
|
55833
56697
|
),
|
|
55834
56698
|
isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -55843,13 +56707,12 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55843
56707
|
recharts.YAxis,
|
|
55844
56708
|
{
|
|
55845
56709
|
domain: [0, chartData.yAxisMax],
|
|
55846
|
-
width: 40,
|
|
55847
|
-
ticks:
|
|
56710
|
+
width: isMobile ? 34 : 40,
|
|
56711
|
+
ticks: visibleYAxisTicks,
|
|
55848
56712
|
tick: (props) => {
|
|
55849
56713
|
const { x, y, payload } = props;
|
|
55850
56714
|
const value = Math.round(payload.value);
|
|
55851
|
-
const
|
|
55852
|
-
const isTarget = value === targetValue && targetValue > 0;
|
|
56715
|
+
const isTarget = (chartData.targetValues || []).includes(value);
|
|
55853
56716
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
55854
56717
|
"text",
|
|
55855
56718
|
{
|
|
@@ -55872,15 +56735,7 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55872
56735
|
content: (props) => /* @__PURE__ */ jsxRuntime.jsx(CustomTooltip3, { ...props, isUptimeMode })
|
|
55873
56736
|
}
|
|
55874
56737
|
),
|
|
55875
|
-
!isUptimeMode &&
|
|
55876
|
-
recharts.ReferenceLine,
|
|
55877
|
-
{
|
|
55878
|
-
y: chartData.lastSetTarget,
|
|
55879
|
-
stroke: "#E34329",
|
|
55880
|
-
strokeDasharray: "5 5",
|
|
55881
|
-
strokeWidth: 2
|
|
55882
|
-
}
|
|
55883
|
-
),
|
|
56738
|
+
!isUptimeMode && /* @__PURE__ */ jsxRuntime.jsx(recharts.Customized, { component: (props) => renderDailyOutputCapsules2(props, chartData.data) }),
|
|
55884
56739
|
isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
55885
56740
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
55886
56741
|
recharts.Bar,
|
|
@@ -55913,6 +56768,8 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55913
56768
|
{
|
|
55914
56769
|
dataKey: "output",
|
|
55915
56770
|
radius: [4, 4, 0, 0],
|
|
56771
|
+
fill: "transparent",
|
|
56772
|
+
opacity: 0,
|
|
55916
56773
|
isAnimationActive: true,
|
|
55917
56774
|
animationBegin: 0,
|
|
55918
56775
|
animationDuration: 1e3,
|
|
@@ -55956,13 +56813,9 @@ var WorkspaceMonthlyHistory = ({
|
|
|
55956
56813
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-2.5 h-2.5 rounded-full", style: { backgroundColor: "#e5e7eb" } }),
|
|
55957
56814
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600", children: "Idle" })
|
|
55958
56815
|
] })
|
|
55959
|
-
] }) : chartData.
|
|
55960
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
55961
|
-
/* @__PURE__ */ jsxRuntime.
|
|
55962
|
-
"Target: ",
|
|
55963
|
-
Math.round(chartData.lastSetTarget),
|
|
55964
|
-
" units/day"
|
|
55965
|
-
] })
|
|
56816
|
+
] }) : chartData.targetValues.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
56817
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-5 w-3 rounded-full border border-[#E34329] bg-[#fff5f3] overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-x-0 bottom-0 h-1/2 bg-[#E34329]" }) }),
|
|
56818
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600", children: chartData.targetLegend })
|
|
55966
56819
|
] }) })
|
|
55967
56820
|
] })
|
|
55968
56821
|
] })
|
|
@@ -58029,7 +58882,14 @@ var WorkspaceHealthCard = ({
|
|
|
58029
58882
|
event.stopPropagation();
|
|
58030
58883
|
event.preventDefault();
|
|
58031
58884
|
if (onViewDetails) {
|
|
58032
|
-
onViewDetails(workspace);
|
|
58885
|
+
onViewDetails(workspace, "camera", "card_button");
|
|
58886
|
+
}
|
|
58887
|
+
};
|
|
58888
|
+
const handleLightChipClick = (event) => {
|
|
58889
|
+
event.stopPropagation();
|
|
58890
|
+
event.preventDefault();
|
|
58891
|
+
if (onViewDetails) {
|
|
58892
|
+
onViewDetails(workspace, "light", "light_chip");
|
|
58033
58893
|
}
|
|
58034
58894
|
};
|
|
58035
58895
|
const handleKeyDown = (event) => {
|
|
@@ -58107,6 +58967,22 @@ var WorkspaceHealthCard = ({
|
|
|
58107
58967
|
};
|
|
58108
58968
|
};
|
|
58109
58969
|
const downtimeConfig = getDowntimeConfig(workspace.uptimeDetails);
|
|
58970
|
+
const hasLightConfig = Boolean(workspace.lightSummary?.hasLightConfig && workspace.lightSummary.bulbIp);
|
|
58971
|
+
const lightStatus = workspace.lightSummary?.currentStatus || null;
|
|
58972
|
+
const lightChipLabel = (() => {
|
|
58973
|
+
if (!workspace.lightSummary) return "Light --";
|
|
58974
|
+
if (lightStatus === "down") return "Light offline";
|
|
58975
|
+
if (lightStatus === "unknown") return "Light unknown";
|
|
58976
|
+
if (typeof workspace.lightSummary.uptimePercent === "number") {
|
|
58977
|
+
return `Light ${workspace.lightSummary.uptimePercent.toFixed(1)}%`;
|
|
58978
|
+
}
|
|
58979
|
+
if (lightStatus === "up") return "Light operational";
|
|
58980
|
+
return "Light --";
|
|
58981
|
+
})();
|
|
58982
|
+
const lightChipClassName = clsx(
|
|
58983
|
+
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold transition-all",
|
|
58984
|
+
lightStatus === "down" ? "border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100" : lightStatus === "unknown" ? "border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100" : "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
|
|
58985
|
+
);
|
|
58110
58986
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
58111
58987
|
Card2,
|
|
58112
58988
|
{
|
|
@@ -58176,6 +59052,20 @@ var WorkspaceHealthCard = ({
|
|
|
58176
59052
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: "Camera IP:" }),
|
|
58177
59053
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: "N/A" })
|
|
58178
59054
|
] }),
|
|
59055
|
+
hasLightConfig && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
59056
|
+
"button",
|
|
59057
|
+
{
|
|
59058
|
+
type: "button",
|
|
59059
|
+
onClick: handleLightChipClick,
|
|
59060
|
+
className: lightChipClassName,
|
|
59061
|
+
"aria-label": `Open light timeline for ${workspace.workspace_display_name || workspace.workspace_id}`,
|
|
59062
|
+
title: workspace.lightSummary?.bulbIp ? `Bulb IP ${workspace.lightSummary.bulbIp}` : "Light timeline",
|
|
59063
|
+
children: [
|
|
59064
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lightbulb, { className: "h-3.5 w-3.5" }),
|
|
59065
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: lightChipLabel })
|
|
59066
|
+
]
|
|
59067
|
+
}
|
|
59068
|
+
),
|
|
58179
59069
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 pt-3 border-t border-slate-100 dark:border-slate-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
58180
59070
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-500 uppercase tracking-wide", children: downtimeConfig.label }),
|
|
58181
59071
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx("text-sm font-semibold", downtimeConfig.className), children: downtimeConfig.text })
|
|
@@ -58245,7 +59135,7 @@ var CompactWorkspaceHealthCard = ({
|
|
|
58245
59135
|
event.stopPropagation();
|
|
58246
59136
|
event.preventDefault();
|
|
58247
59137
|
if (onViewDetails) {
|
|
58248
|
-
onViewDetails(workspace);
|
|
59138
|
+
onViewDetails(workspace, "camera", "card_button");
|
|
58249
59139
|
}
|
|
58250
59140
|
};
|
|
58251
59141
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -67592,6 +68482,17 @@ var setSessionSeenValue = (key) => {
|
|
|
67592
68482
|
};
|
|
67593
68483
|
var buildAllGreenCelebrationSeenKey = (identity) => `${ALL_GREEN_CELEBRATION_SEEN_PREFIX}${identity}`;
|
|
67594
68484
|
var buildAllGreenMilestoneSeenKey = (identity, milestoneSeconds) => `${ALL_GREEN_MILESTONE_SEEN_PREFIX}${identity}:${milestoneSeconds}`;
|
|
68485
|
+
var LINE_SELECTOR_INDICATOR_VERSION = "incident_exclamation_v1";
|
|
68486
|
+
var LineSelectorIncidentIcon = ({ lineId }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
68487
|
+
"span",
|
|
68488
|
+
{
|
|
68489
|
+
"data-testid": `line-selector-incident-icon-${lineId}`,
|
|
68490
|
+
"aria-label": "Line needs attention",
|
|
68491
|
+
role: "img",
|
|
68492
|
+
className: "inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full border border-rose-200 bg-white text-[13px] font-semibold leading-none text-rose-600 shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
|
|
68493
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "-mt-px", children: "!" })
|
|
68494
|
+
}
|
|
68495
|
+
);
|
|
67595
68496
|
var LoadingPageCmp = LoadingPage_default;
|
|
67596
68497
|
var LoadingOverlayCmp = LoadingOverlay_default;
|
|
67597
68498
|
function HomeView({
|
|
@@ -67931,25 +68832,49 @@ function HomeView({
|
|
|
67931
68832
|
const currentIsCurrentScopeResolved = isBootstrapMonitorMode ? bootstrapMonitor.isCurrentScopeResolved : legacyIsCurrentScopeResolved;
|
|
67932
68833
|
const currentMetricsError = isBootstrapMonitorMode ? bootstrapMonitor.error : legacyMetricsError;
|
|
67933
68834
|
const currentRefetchMetrics = isBootstrapMonitorMode ? bootstrapMonitor.refetch : refetchLegacyMetrics;
|
|
67934
|
-
const
|
|
67935
|
-
const
|
|
68835
|
+
const lineSelectorIndicatorByLine = React125.useMemo(() => {
|
|
68836
|
+
const indicatorByLine = /* @__PURE__ */ new Map();
|
|
67936
68837
|
const legend = currentEfficiencyLegend || DEFAULT_EFFICIENCY_LEGEND;
|
|
67937
68838
|
const addRows = (rows) => {
|
|
67938
68839
|
(rows || []).forEach((row) => {
|
|
67939
68840
|
const lineId = typeof row?.line_id === "string" ? row.line_id : "";
|
|
67940
|
-
if (!lineId
|
|
68841
|
+
if (!lineId) {
|
|
68842
|
+
return;
|
|
68843
|
+
}
|
|
68844
|
+
if (row?.red_flow_incident?.active === true) {
|
|
68845
|
+
indicatorByLine.set(lineId, "incident");
|
|
68846
|
+
return;
|
|
68847
|
+
}
|
|
68848
|
+
if (indicatorByLine.get(lineId) === "incident") {
|
|
67941
68849
|
return;
|
|
67942
68850
|
}
|
|
67943
68851
|
const status = getKpiSignalStatus(row?.line_signal, legend);
|
|
67944
|
-
if (status) {
|
|
67945
|
-
|
|
68852
|
+
if (status === "attention") {
|
|
68853
|
+
indicatorByLine.set(lineId, "red");
|
|
67946
68854
|
}
|
|
67947
68855
|
});
|
|
67948
68856
|
};
|
|
67949
68857
|
addRows(currentSelectorLineMetrics);
|
|
67950
68858
|
addRows(currentLineMetrics);
|
|
67951
|
-
return
|
|
68859
|
+
return indicatorByLine;
|
|
67952
68860
|
}, [currentEfficiencyLegend, currentLineMetrics, currentSelectorLineMetrics]);
|
|
68861
|
+
const lineSelectorIndicatorStats = React125.useMemo(() => {
|
|
68862
|
+
let incidentLineCount = 0;
|
|
68863
|
+
let redFlowLineCount = 0;
|
|
68864
|
+
visibleLineIds.forEach((lineId) => {
|
|
68865
|
+
const indicator = lineSelectorIndicatorByLine.get(lineId);
|
|
68866
|
+
if (indicator === "incident") {
|
|
68867
|
+
incidentLineCount += 1;
|
|
68868
|
+
} else if (indicator === "red") {
|
|
68869
|
+
redFlowLineCount += 1;
|
|
68870
|
+
}
|
|
68871
|
+
});
|
|
68872
|
+
return {
|
|
68873
|
+
incidentLineCount,
|
|
68874
|
+
redFlowLineCount,
|
|
68875
|
+
hasAnyIncident: incidentLineCount > 0
|
|
68876
|
+
};
|
|
68877
|
+
}, [lineSelectorIndicatorByLine, visibleLineIds]);
|
|
67953
68878
|
const metricsDisplayNames = React125.useMemo(() => {
|
|
67954
68879
|
const nextDisplayNames = {};
|
|
67955
68880
|
currentWorkspaceMetrics.forEach((workspace) => {
|
|
@@ -68806,9 +69731,12 @@ function HomeView({
|
|
|
68806
69731
|
new_line_ids: normalizedLineIds,
|
|
68807
69732
|
selected_line_count: normalizedLineIds.length,
|
|
68808
69733
|
selection_mode: isAllLinesSelection(normalizedLineIds) ? "all" : normalizedLineIds.length === 1 ? "single" : "custom",
|
|
69734
|
+
incident_line_count: lineSelectorIndicatorStats.incidentLineCount,
|
|
69735
|
+
red_flow_line_count: lineSelectorIndicatorStats.redFlowLineCount,
|
|
69736
|
+
selector_indicator_version: LINE_SELECTOR_INDICATOR_VERSION,
|
|
68809
69737
|
line_name: getLineSelectionLabel(normalizedLineIds)
|
|
68810
69738
|
});
|
|
68811
|
-
}, [factoryViewId, getLineSelectionLabel, getTrackedLineScope, selectedLineIds, selectedLineIdsKey, visibleLineIds]);
|
|
69739
|
+
}, [factoryViewId, getLineSelectionLabel, getTrackedLineScope, lineSelectorIndicatorStats, selectedLineIds, selectedLineIdsKey, visibleLineIds]);
|
|
68812
69740
|
React125.useCallback(() => {
|
|
68813
69741
|
updateSelectedLineIds(visibleLineIds);
|
|
68814
69742
|
}, [updateSelectedLineIds, visibleLineIds]);
|
|
@@ -68866,7 +69794,10 @@ function HomeView({
|
|
|
68866
69794
|
current_display_mode_label: getHomeDisplayModeLabel(displayMode),
|
|
68867
69795
|
selected_line_ids: selectedLineIds,
|
|
68868
69796
|
selected_line_count: selectedLineIds.length,
|
|
68869
|
-
is_all_lines: isAllLinesSelection(selectedLineIds)
|
|
69797
|
+
is_all_lines: isAllLinesSelection(selectedLineIds),
|
|
69798
|
+
incident_line_count: lineSelectorIndicatorStats.incidentLineCount,
|
|
69799
|
+
red_flow_line_count: lineSelectorIndicatorStats.redFlowLineCount,
|
|
69800
|
+
selector_indicator_version: LINE_SELECTOR_INDICATOR_VERSION
|
|
68870
69801
|
});
|
|
68871
69802
|
}
|
|
68872
69803
|
},
|
|
@@ -68907,8 +69838,8 @@ function HomeView({
|
|
|
68907
69838
|
] }),
|
|
68908
69839
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-56 space-y-0.5 overflow-y-auto pr-1", children: visibleLineIds.map((lineId) => {
|
|
68909
69840
|
const isChecked = pendingSelectedLineIds.includes(lineId);
|
|
68910
|
-
const
|
|
68911
|
-
const
|
|
69841
|
+
const selectorIndicator = lineSelectorIndicatorByLine.get(lineId);
|
|
69842
|
+
const lineLabel = mergedLineNames[lineId] || `Line ${lineId.substring(0, 4)}`;
|
|
68912
69843
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
68913
69844
|
"label",
|
|
68914
69845
|
{
|
|
@@ -68918,6 +69849,7 @@ function HomeView({
|
|
|
68918
69849
|
"input",
|
|
68919
69850
|
{
|
|
68920
69851
|
type: "checkbox",
|
|
69852
|
+
"aria-label": lineLabel,
|
|
68921
69853
|
className: "h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500",
|
|
68922
69854
|
checked: isChecked,
|
|
68923
69855
|
onChange: () => {
|
|
@@ -68933,12 +69865,12 @@ function HomeView({
|
|
|
68933
69865
|
}
|
|
68934
69866
|
}
|
|
68935
69867
|
),
|
|
68936
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children:
|
|
68937
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-
|
|
69868
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: lineLabel }),
|
|
69869
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center", children: selectorIndicator === "incident" ? /* @__PURE__ */ jsxRuntime.jsx(LineSelectorIncidentIcon, { lineId }) : selectorIndicator === "red" && !lineSelectorIndicatorStats.hasAnyIncident ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
68938
69870
|
"span",
|
|
68939
69871
|
{
|
|
68940
69872
|
"data-testid": `line-selector-signal-dot-${lineId}`,
|
|
68941
|
-
className:
|
|
69873
|
+
className: "h-2 w-2 rounded-full bg-red-500",
|
|
68942
69874
|
"aria-hidden": "true"
|
|
68943
69875
|
}
|
|
68944
69876
|
) : null })
|
|
@@ -68970,7 +69902,8 @@ function HomeView({
|
|
|
68970
69902
|
mergedLineNames,
|
|
68971
69903
|
selectedLineIds,
|
|
68972
69904
|
pendingSelectedLineIds,
|
|
68973
|
-
|
|
69905
|
+
lineSelectorIndicatorByLine,
|
|
69906
|
+
lineSelectorIndicatorStats,
|
|
68974
69907
|
displayMode,
|
|
68975
69908
|
slideshowActiveLineId,
|
|
68976
69909
|
visibleLineIds,
|
|
@@ -83931,11 +84864,13 @@ var useWorkspaceHealth = (options) => {
|
|
|
83931
84864
|
var STATUS_COLORS = {
|
|
83932
84865
|
up: "bg-emerald-500",
|
|
83933
84866
|
down: "bg-rose-500",
|
|
84867
|
+
unknown: "bg-amber-400",
|
|
83934
84868
|
pending: "bg-gray-200"
|
|
83935
84869
|
};
|
|
83936
84870
|
var STATUS_TITLES = {
|
|
83937
84871
|
up: "Uptime",
|
|
83938
84872
|
down: "Downtime",
|
|
84873
|
+
unknown: "Unknown",
|
|
83939
84874
|
pending: "Pending"
|
|
83940
84875
|
};
|
|
83941
84876
|
var formatTime4 = (date, timezone) => new Intl.DateTimeFormat("en-IN", {
|
|
@@ -83978,7 +84913,9 @@ var UptimeTimelineStrip = ({
|
|
|
83978
84913
|
timezone,
|
|
83979
84914
|
className = "",
|
|
83980
84915
|
uptimePercentage = null,
|
|
83981
|
-
downtimeMinutes = 0
|
|
84916
|
+
downtimeMinutes = 0,
|
|
84917
|
+
metricLabel = "uptime",
|
|
84918
|
+
summaryText
|
|
83982
84919
|
}) => {
|
|
83983
84920
|
const segments = React125.useMemo(() => {
|
|
83984
84921
|
if (!points.length || totalMinutes <= 0) return [];
|
|
@@ -84042,9 +84979,11 @@ var UptimeTimelineStrip = ({
|
|
|
84042
84979
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full rounded-xl border border-dashed border-gray-200 bg-gray-50/50 p-6 text-center text-sm text-gray-600", children: "No uptime data available for this shift yet." });
|
|
84043
84980
|
}
|
|
84044
84981
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative w-full ${className}`, children: [
|
|
84045
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center mb-3 text-sm", children: typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-900 font-semibold", children: [
|
|
84982
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center mb-3 text-sm", children: summaryText ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900 font-semibold", children: summaryText }) : typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-900 font-semibold", children: [
|
|
84046
84983
|
uptimePercentage.toFixed(1),
|
|
84047
|
-
" %
|
|
84984
|
+
" % ",
|
|
84985
|
+
metricLabel,
|
|
84986
|
+
" ",
|
|
84048
84987
|
downtimeMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
|
|
84049
84988
|
"(",
|
|
84050
84989
|
formatDowntimeLabel(downtimeMinutes),
|
|
@@ -84112,6 +85051,27 @@ var formatDowntimeLabel2 = (minutes, includeSuffix = true) => {
|
|
|
84112
85051
|
const label = formatDuration4(minutes);
|
|
84113
85052
|
return includeSuffix ? `${label} down` : label;
|
|
84114
85053
|
};
|
|
85054
|
+
var formatSecondsDuration = (seconds) => {
|
|
85055
|
+
if (!seconds || seconds <= 0) return "0 min";
|
|
85056
|
+
return formatDuration4(Math.max(1, Math.ceil(seconds / 60)));
|
|
85057
|
+
};
|
|
85058
|
+
var getLightStatusLabel = (status) => {
|
|
85059
|
+
if (status === "up") return "operational";
|
|
85060
|
+
if (status === "down") return "offline";
|
|
85061
|
+
if (status === "unknown") return "unknown";
|
|
85062
|
+
return "status unavailable";
|
|
85063
|
+
};
|
|
85064
|
+
var getLightDurationPrefix = (status) => {
|
|
85065
|
+
if (status === "up") return "Operational";
|
|
85066
|
+
if (status === "down") return "Offline";
|
|
85067
|
+
if (status === "unknown") return "Unknown";
|
|
85068
|
+
return "Current";
|
|
85069
|
+
};
|
|
85070
|
+
var formatLightError = (error) => {
|
|
85071
|
+
if (!error) return null;
|
|
85072
|
+
const cleaned = error.replace(/\s*\(after retry\)\s*$/i, "").trim();
|
|
85073
|
+
return cleaned || null;
|
|
85074
|
+
};
|
|
84115
85075
|
var formatTimeRange = (start, end, timezone) => {
|
|
84116
85076
|
const formatter = new Intl.DateTimeFormat("en-IN", {
|
|
84117
85077
|
hour: "numeric",
|
|
@@ -84127,12 +85087,15 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84127
85087
|
onClose,
|
|
84128
85088
|
shiftConfig: passedShiftConfig,
|
|
84129
85089
|
date,
|
|
84130
|
-
shiftId
|
|
85090
|
+
shiftId,
|
|
85091
|
+
initialMode = "camera"
|
|
84131
85092
|
}) => {
|
|
84132
85093
|
const timezone = useAppTimezone() || "UTC";
|
|
84133
85094
|
const logsContainerRef = React125.useRef(null);
|
|
84134
85095
|
const [showScrollIndicator, setShowScrollIndicator] = React125.useState(false);
|
|
85096
|
+
const [activeMode, setActiveMode] = React125.useState(initialMode);
|
|
84135
85097
|
const isHistorical = Boolean(date);
|
|
85098
|
+
const hasLightTimeline = Boolean(workspace?.lightSummary?.hasLightConfig && workspace?.lightSummary?.bulbIp);
|
|
84136
85099
|
const {
|
|
84137
85100
|
timeline,
|
|
84138
85101
|
loading,
|
|
@@ -84141,7 +85104,7 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84141
85104
|
} = useWorkspaceUptimeTimeline({
|
|
84142
85105
|
workspaceId: workspace?.workspace_id,
|
|
84143
85106
|
companyId: workspace?.company_id,
|
|
84144
|
-
enabled: isOpen && Boolean(workspace?.workspace_id && workspace?.company_id),
|
|
85107
|
+
enabled: isOpen && activeMode === "camera" && Boolean(workspace?.workspace_id && workspace?.company_id),
|
|
84145
85108
|
refreshInterval: isHistorical ? void 0 : 6e4,
|
|
84146
85109
|
// Disable auto-refresh for historical
|
|
84147
85110
|
lineId: workspace?.line_id,
|
|
@@ -84154,6 +85117,25 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84154
85117
|
shiftId
|
|
84155
85118
|
// Pass override shift ID for historical queries
|
|
84156
85119
|
});
|
|
85120
|
+
const {
|
|
85121
|
+
timeline: lightTimeline,
|
|
85122
|
+
loading: lightLoading,
|
|
85123
|
+
error: lightError,
|
|
85124
|
+
refetch: refetchLight
|
|
85125
|
+
} = useWorkspaceLightTimeline({
|
|
85126
|
+
workspaceId: workspace?.workspace_id,
|
|
85127
|
+
enabled: isOpen && activeMode === "light" && hasLightTimeline && Boolean(workspace?.workspace_id),
|
|
85128
|
+
refreshInterval: isHistorical ? void 0 : 6e4,
|
|
85129
|
+
lineId: workspace?.line_id,
|
|
85130
|
+
shiftConfig: passedShiftConfig || void 0,
|
|
85131
|
+
timezone,
|
|
85132
|
+
date,
|
|
85133
|
+
shiftId
|
|
85134
|
+
});
|
|
85135
|
+
React125.useEffect(() => {
|
|
85136
|
+
if (!isOpen) return;
|
|
85137
|
+
setActiveMode(initialMode === "light" && hasLightTimeline ? "light" : "camera");
|
|
85138
|
+
}, [hasLightTimeline, initialMode, isOpen]);
|
|
84157
85139
|
React125.useEffect(() => {
|
|
84158
85140
|
if (!isOpen || !workspace) return;
|
|
84159
85141
|
const handleKeyDown = (event) => {
|
|
@@ -84166,13 +85148,21 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84166
85148
|
window.removeEventListener("keydown", handleKeyDown);
|
|
84167
85149
|
};
|
|
84168
85150
|
}, [isOpen, onClose, workspace]);
|
|
84169
|
-
const
|
|
84170
|
-
const
|
|
85151
|
+
const activeTimeline = activeMode === "light" ? lightTimeline : timeline;
|
|
85152
|
+
const activeLoading = activeMode === "light" ? lightLoading : loading;
|
|
85153
|
+
const activeError = activeMode === "light" ? lightError : error;
|
|
85154
|
+
const activeRefetch = activeMode === "light" ? refetchLight : refetch;
|
|
85155
|
+
const shiftStart = activeTimeline ? new Date(activeTimeline.shiftStart) : null;
|
|
85156
|
+
const shiftEnd = activeTimeline ? new Date(activeTimeline.shiftEnd) : null;
|
|
84171
85157
|
const downtimeSegments = timeline?.downtimeSegments || [];
|
|
84172
85158
|
downtimeSegments.length;
|
|
84173
85159
|
const downtimeMinutes = timeline?.downtimeMinutes ?? 0;
|
|
84174
|
-
const hasTimelineData = Boolean(timeline?.hasData);
|
|
84175
|
-
const uptimePercentage = hasTimelineData ? timeline?.uptimePercentage ?? workspace?.uptimePercentage ?? null : null;
|
|
85160
|
+
const hasTimelineData = activeMode === "light" ? Boolean(lightTimeline?.hasData) : Boolean(timeline?.hasData);
|
|
85161
|
+
const uptimePercentage = activeMode === "light" ? lightTimeline?.uptimePercentage ?? null : hasTimelineData ? timeline?.uptimePercentage ?? workspace?.uptimePercentage ?? null : null;
|
|
85162
|
+
const lightStatus = lightTimeline?.currentStatus || workspace?.lightSummary?.currentStatus || null;
|
|
85163
|
+
const lightStatusText = `Light ${getLightStatusLabel(lightStatus)}`;
|
|
85164
|
+
const lightDurationText = lightTimeline?.currentDurationSeconds ? `${getLightDurationPrefix(lightStatus)} for ${formatSecondsDuration(lightTimeline.currentDurationSeconds)}` : null;
|
|
85165
|
+
const lightLastError = formatLightError(lightTimeline?.lastError);
|
|
84176
85166
|
const allInterruptionsSorted = React125.useMemo(
|
|
84177
85167
|
() => [...downtimeSegments].sort(
|
|
84178
85168
|
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
|
|
@@ -84194,7 +85184,7 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84194
85184
|
container.addEventListener("scroll", checkScroll);
|
|
84195
85185
|
return () => container.removeEventListener("scroll", checkScroll);
|
|
84196
85186
|
}
|
|
84197
|
-
}, [downtimeSegments]);
|
|
85187
|
+
}, [downtimeSegments, lightTimeline?.statusSegments, activeMode]);
|
|
84198
85188
|
const renderSegment = (segment) => {
|
|
84199
85189
|
const start = new Date(segment.startTime);
|
|
84200
85190
|
const end = new Date(segment.endTime);
|
|
@@ -84215,6 +85205,63 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84215
85205
|
`${segment.startMinuteIndex}-${segment.endMinuteIndex}`
|
|
84216
85206
|
);
|
|
84217
85207
|
};
|
|
85208
|
+
const renderLightSegment = (segment) => {
|
|
85209
|
+
const start = new Date(segment.startTime);
|
|
85210
|
+
const end = new Date(segment.endTime);
|
|
85211
|
+
const startLabel = new Intl.DateTimeFormat("en-IN", {
|
|
85212
|
+
hour: "numeric",
|
|
85213
|
+
minute: "2-digit",
|
|
85214
|
+
hour12: true,
|
|
85215
|
+
timeZone: timezone
|
|
85216
|
+
}).format(start);
|
|
85217
|
+
const endLabel = segment.isCurrent ? "Now" : new Intl.DateTimeFormat("en-IN", {
|
|
85218
|
+
hour: "numeric",
|
|
85219
|
+
minute: "2-digit",
|
|
85220
|
+
hour12: true,
|
|
85221
|
+
timeZone: timezone
|
|
85222
|
+
}).format(end);
|
|
85223
|
+
const containerClasses = segment.status === "down" ? "border-rose-200 bg-rose-50" : "border-amber-200 bg-amber-50";
|
|
85224
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
85225
|
+
"div",
|
|
85226
|
+
{
|
|
85227
|
+
className: `rounded-lg border px-5 py-3 ${containerClasses}`,
|
|
85228
|
+
children: [
|
|
85229
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm font-semibold text-gray-900", children: [
|
|
85230
|
+
startLabel,
|
|
85231
|
+
" - ",
|
|
85232
|
+
endLabel
|
|
85233
|
+
] }),
|
|
85234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-600 mt-1", children: [
|
|
85235
|
+
getLightDurationPrefix(segment.status),
|
|
85236
|
+
" for ",
|
|
85237
|
+
formatSecondsDuration(segment.durationSeconds)
|
|
85238
|
+
] }),
|
|
85239
|
+
formatLightError(segment.lastError) && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs font-medium text-rose-700", children: formatLightError(segment.lastError) })
|
|
85240
|
+
]
|
|
85241
|
+
},
|
|
85242
|
+
`${segment.status}-${segment.startMinuteIndex}-${segment.endMinuteIndex}`
|
|
85243
|
+
);
|
|
85244
|
+
};
|
|
85245
|
+
const handleModeChange = (mode) => {
|
|
85246
|
+
if (mode === activeMode) return;
|
|
85247
|
+
const previousMode = activeMode;
|
|
85248
|
+
setActiveMode(mode);
|
|
85249
|
+
trackCoreEvent("Health Timeline Mode Changed", {
|
|
85250
|
+
previous_mode: previousMode,
|
|
85251
|
+
selected_mode: mode,
|
|
85252
|
+
source: "segmented_control",
|
|
85253
|
+
workspace_id: workspace?.workspace_id,
|
|
85254
|
+
line_id: workspace?.line_id
|
|
85255
|
+
});
|
|
85256
|
+
};
|
|
85257
|
+
const handleRefresh = () => {
|
|
85258
|
+
trackCoreEvent("Health Timeline Refreshed", {
|
|
85259
|
+
mode: activeMode,
|
|
85260
|
+
workspace_id: workspace?.workspace_id,
|
|
85261
|
+
line_id: workspace?.line_id
|
|
85262
|
+
});
|
|
85263
|
+
activeRefetch();
|
|
85264
|
+
};
|
|
84218
85265
|
if (!isOpen || !workspace) {
|
|
84219
85266
|
return null;
|
|
84220
85267
|
}
|
|
@@ -84238,13 +85285,35 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84238
85285
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 mr-4", children: [
|
|
84239
85286
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: "uptime-detail-title", className: "text-2xl font-semibold text-gray-900 truncate mb-3", children: workspace.workspace_display_name || `Workspace ${workspace.workspace_id.slice(0, 6)}` }),
|
|
84240
85287
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 text-sm text-gray-600", children: [
|
|
84241
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children:
|
|
85288
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: activeTimeline?.shiftLabel || "Current Shift" }),
|
|
84242
85289
|
shiftStart && shiftEnd && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
84243
85290
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
|
|
84244
85291
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-600", children: formatTimeRange(shiftStart, shiftEnd, timezone) })
|
|
84245
85292
|
] })
|
|
84246
85293
|
] }),
|
|
84247
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-
|
|
85294
|
+
hasLightTimeline && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5", children: [
|
|
85295
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
85296
|
+
"button",
|
|
85297
|
+
{
|
|
85298
|
+
type: "button",
|
|
85299
|
+
onClick: () => handleModeChange("camera"),
|
|
85300
|
+
"aria-pressed": activeMode === "camera",
|
|
85301
|
+
className: `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeMode === "camera" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
|
|
85302
|
+
children: "Camera"
|
|
85303
|
+
}
|
|
85304
|
+
),
|
|
85305
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
85306
|
+
"button",
|
|
85307
|
+
{
|
|
85308
|
+
type: "button",
|
|
85309
|
+
onClick: () => handleModeChange("light"),
|
|
85310
|
+
"aria-pressed": activeMode === "light",
|
|
85311
|
+
className: `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeMode === "light" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
|
|
85312
|
+
children: "Light"
|
|
85313
|
+
}
|
|
85314
|
+
)
|
|
85315
|
+
] }),
|
|
85316
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: activeMode === "camera" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
84248
85317
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
84249
85318
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-2 h-2 rounded-full ${workspace.status === "healthy" ? "bg-emerald-500 animate-pulse" : workspace.status === "warning" ? "bg-amber-500" : "bg-rose-500"}` }),
|
|
84250
85319
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs font-medium ${workspace.status === "healthy" ? "text-emerald-700" : workspace.status === "warning" ? "text-amber-700" : "text-rose-700"}`, children: workspace.status === "healthy" ? "Operational" : workspace.status === "warning" ? "Intermittent" : "Down" })
|
|
@@ -84261,17 +85330,37 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84261
85330
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-600 dark:text-slate-300 select-all", children: workspace.cameraIp })
|
|
84262
85331
|
] })
|
|
84263
85332
|
] })
|
|
84264
|
-
] })
|
|
85333
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
85334
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
85335
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-2 h-2 rounded-full ${lightStatus === "up" ? "bg-emerald-500 animate-pulse" : lightStatus === "unknown" ? "bg-amber-500" : "bg-rose-500"}` }),
|
|
85336
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs font-medium ${lightStatus === "up" ? "text-emerald-700" : lightStatus === "unknown" ? "text-amber-700" : "text-rose-700"}`, children: lightStatusText })
|
|
85337
|
+
] }),
|
|
85338
|
+
lightTimeline?.bulbIp && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
85339
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
|
|
85340
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 bg-slate-50 px-2 py-0.5 rounded border border-slate-100", title: "Bulb IP", children: [
|
|
85341
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lightbulb, { className: "h-3 w-3 text-slate-400" }),
|
|
85342
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-600 select-all", children: lightTimeline.bulbIp })
|
|
85343
|
+
] })
|
|
85344
|
+
] }),
|
|
85345
|
+
lightDurationText && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
85346
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
|
|
85347
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: lightDurationText })
|
|
85348
|
+
] }),
|
|
85349
|
+
lightLastError && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
85350
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
|
|
85351
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-rose-700", children: lightLastError })
|
|
85352
|
+
] })
|
|
85353
|
+
] }) })
|
|
84265
85354
|
] }),
|
|
84266
85355
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
|
|
84267
85356
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
84268
85357
|
"button",
|
|
84269
85358
|
{
|
|
84270
|
-
onClick:
|
|
84271
|
-
disabled:
|
|
85359
|
+
onClick: handleRefresh,
|
|
85360
|
+
disabled: activeLoading,
|
|
84272
85361
|
className: "inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200 shadow-sm",
|
|
84273
85362
|
children: [
|
|
84274
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-4 w-4 ${
|
|
85363
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-4 w-4 ${activeLoading ? "animate-spin" : ""}` }),
|
|
84275
85364
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Refresh" })
|
|
84276
85365
|
]
|
|
84277
85366
|
}
|
|
@@ -84287,31 +85376,63 @@ var WorkspaceUptimeDetailModal = ({
|
|
|
84287
85376
|
)
|
|
84288
85377
|
] })
|
|
84289
85378
|
] }),
|
|
84290
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-8 py-6 space-y-6", children:
|
|
85379
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-8 py-6 space-y-6", children: activeError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-rose-100 bg-rose-50 p-5 text-sm text-rose-700", children: [
|
|
84291
85380
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold mb-1", children: "Unable to load uptime details" }),
|
|
84292
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-rose-600/90", children:
|
|
85381
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-rose-600/90", children: activeError.message })
|
|
84293
85382
|
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
84294
85383
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative pb-4 border-b border-gray-200", children: [
|
|
84295
85384
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
84296
85385
|
UptimeTimelineStrip_default,
|
|
84297
85386
|
{
|
|
84298
|
-
points:
|
|
84299
|
-
totalMinutes:
|
|
84300
|
-
shiftStart:
|
|
84301
|
-
shiftEnd:
|
|
85387
|
+
points: activeTimeline?.points || [],
|
|
85388
|
+
totalMinutes: activeTimeline?.totalMinutes || 0,
|
|
85389
|
+
shiftStart: activeTimeline?.shiftStart || (/* @__PURE__ */ new Date()).toISOString(),
|
|
85390
|
+
shiftEnd: activeTimeline?.shiftEnd || (/* @__PURE__ */ new Date()).toISOString(),
|
|
84302
85391
|
timezone,
|
|
84303
85392
|
uptimePercentage,
|
|
84304
|
-
downtimeMinutes: hasTimelineData ? downtimeMinutes : 0
|
|
85393
|
+
downtimeMinutes: activeMode === "light" ? 0 : hasTimelineData ? downtimeMinutes : 0,
|
|
85394
|
+
metricLabel: activeMode === "light" ? "light uptime" : "uptime",
|
|
85395
|
+
summaryText: activeMode === "light" && typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
85396
|
+
uptimePercentage.toFixed(1),
|
|
85397
|
+
" % light uptime",
|
|
85398
|
+
(lightTimeline?.downSeconds || 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
|
|
85399
|
+
" (",
|
|
85400
|
+
formatSecondsDuration(lightTimeline?.downSeconds || 0),
|
|
85401
|
+
" offline)"
|
|
85402
|
+
] }),
|
|
85403
|
+
(lightTimeline?.unknownSeconds || 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
|
|
85404
|
+
" (",
|
|
85405
|
+
formatSecondsDuration(lightTimeline?.unknownSeconds || 0),
|
|
85406
|
+
" unknown)"
|
|
85407
|
+
] })
|
|
85408
|
+
] }) : void 0
|
|
84305
85409
|
}
|
|
84306
85410
|
),
|
|
84307
|
-
|
|
85411
|
+
activeLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 mt-4", children: [
|
|
84308
85412
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4 animate-spin text-gray-500" }),
|
|
84309
85413
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Updating timeline\u2026" })
|
|
84310
85414
|
] })
|
|
84311
85415
|
] }),
|
|
84312
85416
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4", children: [
|
|
84313
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: "Downtime Logs" }),
|
|
84314
|
-
!hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No
|
|
85417
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: activeMode === "light" ? "Light Status Logs" : "Downtime Logs" }),
|
|
85418
|
+
activeMode === "light" ? !hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No light status data available for this shift." }) }) : !lightTimeline?.statusSegments.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No light down or unknown events recorded for this shift." }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
85419
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
85420
|
+
"div",
|
|
85421
|
+
{
|
|
85422
|
+
ref: logsContainerRef,
|
|
85423
|
+
className: "max-h-[400px] overflow-y-auto space-y-2 pr-2",
|
|
85424
|
+
style: {
|
|
85425
|
+
scrollbarWidth: "thin",
|
|
85426
|
+
scrollbarColor: "#CBD5E0 #F7FAFC"
|
|
85427
|
+
},
|
|
85428
|
+
children: lightTimeline.statusSegments.map((segment) => renderLightSegment(segment))
|
|
85429
|
+
}
|
|
85430
|
+
),
|
|
85431
|
+
showScrollIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white via-white/80 to-transparent pointer-events-none flex items-end justify-center pb-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-gray-500 animate-bounce", children: [
|
|
85432
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Scroll for more" }),
|
|
85433
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" })
|
|
85434
|
+
] }) })
|
|
85435
|
+
] }) : !hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No uptime data available for this shift." }) }) : downtimeSegments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No downtime events recorded for this shift." }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
84315
85436
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
84316
85437
|
"div",
|
|
84317
85438
|
{
|
|
@@ -84358,6 +85479,7 @@ var WorkspaceHealthView = ({
|
|
|
84358
85479
|
const timezone = useAppTimezone();
|
|
84359
85480
|
const { shiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(effectiveLineIdForShiftConfig);
|
|
84360
85481
|
const [selectedWorkspace, setSelectedWorkspace] = React125.useState(null);
|
|
85482
|
+
const [selectedTimelineMode, setSelectedTimelineMode] = React125.useState("camera");
|
|
84361
85483
|
const [selectedDate, setSelectedDate] = React125.useState(void 0);
|
|
84362
85484
|
const [selectedShiftId, setSelectedShiftId] = React125.useState(void 0);
|
|
84363
85485
|
const [showDatePicker, setShowDatePicker] = React125.useState(false);
|
|
@@ -84417,11 +85539,33 @@ var WorkspaceHealthView = ({
|
|
|
84417
85539
|
},
|
|
84418
85540
|
[router$1, onNavigate]
|
|
84419
85541
|
);
|
|
84420
|
-
const handleViewDetails = React125.useCallback((workspace) => {
|
|
85542
|
+
const handleViewDetails = React125.useCallback((workspace, mode = "camera", source = "card_button") => {
|
|
85543
|
+
const hasLightConfig = Boolean(workspace.lightSummary?.hasLightConfig && workspace.lightSummary?.bulbIp);
|
|
85544
|
+
if (source === "light_chip") {
|
|
85545
|
+
trackCoreEvent("Health Light Chip Clicked", {
|
|
85546
|
+
workspace_id: workspace.workspace_id,
|
|
85547
|
+
line_id: workspace.line_id,
|
|
85548
|
+
light_status: workspace.lightSummary?.currentStatus || null,
|
|
85549
|
+
uptime_percent: workspace.lightSummary?.uptimePercent ?? null,
|
|
85550
|
+
bulb_ip_present: Boolean(workspace.lightSummary?.bulbIp)
|
|
85551
|
+
});
|
|
85552
|
+
}
|
|
85553
|
+
trackCoreEvent("Health Timeline Opened", {
|
|
85554
|
+
source,
|
|
85555
|
+
initial_mode: mode,
|
|
85556
|
+
workspace_id: workspace.workspace_id,
|
|
85557
|
+
line_id: workspace.line_id,
|
|
85558
|
+
has_light_config: hasLightConfig,
|
|
85559
|
+
light_status: workspace.lightSummary?.currentStatus || null,
|
|
85560
|
+
selected_date: selectedDate || operationalDate,
|
|
85561
|
+
selected_shift_id: selectedShiftId ?? currentShiftDetails.shiftId
|
|
85562
|
+
});
|
|
85563
|
+
setSelectedTimelineMode(mode);
|
|
84421
85564
|
setSelectedWorkspace(workspace);
|
|
84422
|
-
}, []);
|
|
85565
|
+
}, [currentShiftDetails.shiftId, operationalDate, selectedDate, selectedShiftId]);
|
|
84423
85566
|
const handleCloseDetails = React125.useCallback(() => {
|
|
84424
85567
|
setSelectedWorkspace(null);
|
|
85568
|
+
setSelectedTimelineMode("camera");
|
|
84425
85569
|
}, []);
|
|
84426
85570
|
const getStatusIcon = (status) => {
|
|
84427
85571
|
switch (status) {
|
|
@@ -84679,7 +85823,8 @@ var WorkspaceHealthView = ({
|
|
|
84679
85823
|
onClose: handleCloseDetails,
|
|
84680
85824
|
shiftConfig: modalShiftConfig,
|
|
84681
85825
|
date: selectedDate,
|
|
84682
|
-
shiftId: selectedShiftId
|
|
85826
|
+
shiftId: selectedShiftId,
|
|
85827
|
+
initialMode: selectedTimelineMode
|
|
84683
85828
|
}
|
|
84684
85829
|
)
|
|
84685
85830
|
] });
|
|
@@ -93495,6 +94640,7 @@ exports.useWorkspaceHealthById = useWorkspaceHealthById;
|
|
|
93495
94640
|
exports.useWorkspaceHealthLastSeen = useWorkspaceHealthLastSeen;
|
|
93496
94641
|
exports.useWorkspaceHealthStatus = useWorkspaceHealthStatus;
|
|
93497
94642
|
exports.useWorkspaceHourSummary = useWorkspaceHourSummary;
|
|
94643
|
+
exports.useWorkspaceLightTimeline = useWorkspaceLightTimeline;
|
|
93498
94644
|
exports.useWorkspaceMetrics = useWorkspaceMetrics;
|
|
93499
94645
|
exports.useWorkspaceNavigation = useWorkspaceNavigation;
|
|
93500
94646
|
exports.useWorkspaceOperators = useWorkspaceOperators;
|