Files
XRLib/Assets/Resources/SHI/UIToolkit/ISOP/ShipblockGanttController.cs
2025-11-27 13:41:37 +09:00

297 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
using UVC.Util;
public class ShipblockGanttController : MonoBehaviour
{
[SerializeField] private VisualTreeAsset taskRowTemplate; // row with 8 cells
[SerializeField] private StyleSheet styleSheet;
[SerializeField] private Color planColor = new Color(0.2f, 0.4f, 0.8f, 0.8f);
[SerializeField] private Color actualColor = new Color(0.2f, 0.9f, 0.3f, 0.9f);
private float pixelsPerDay = 18f; // day cell width
[SerializeField] private Font PretendardRegular;
private VisualElement root;
private ListView taskListView;
private ScrollView timelineScrollView; // main scroll view (vertical + horizontal)
private ScrollView axisScrollView; // header axis scroll view (hidden scrollers, synced)
private VisualElement timelineContent;
private List<ShipblockTask> tasks;
private DateTime projectStartDate;
private DateTime projectEndDate;
private int totalDays;
public void Load(string jsonFileName)
{
root = GetComponent<UIDocument>().rootVisualElement;
if (styleSheet != null && !root.styleSheets.Contains(styleSheet)) root.styleSheets.Add(styleSheet);
LoadData(jsonFileName);
CalculateProjectRange();
InitializeUI();
SetupTimeAxis();
RenderTasks();
}
void LoadData(string jsonFileName)
{
var json = File.ReadAllText(jsonFileName);
var wrapper = JsonUtility.FromJson<ShipblockDataWrapper>("{\"items\":" + json + "}");
tasks = wrapper.items.Where(t => !string.IsNullOrEmpty(t.STDT21) && t.STDT21 != "null").ToList();
foreach (var task in tasks)
task.CalculatedProgress = CalculateProgress(task);
}
void CalculateProjectRange()
{
var dates = new List<DateTime>();
foreach (var task in tasks)
{
if (task.GetPlanStart().HasValue) dates.Add(task.GetPlanStart().Value);
if (task.GetPlanEnd().HasValue) dates.Add(task.GetPlanEnd().Value);
if (task.GetActualStart().HasValue) dates.Add(task.GetActualStart().Value);
if (task.GetActualEnd().HasValue) dates.Add(task.GetActualEnd().Value);
}
projectStartDate = dates.Any() ? dates.Min().AddDays(-3) : new DateTime(2025, 1, 6);
projectEndDate = dates.Any() ? dates.Max().AddDays(3) : new DateTime(2025, 2, 28);
totalDays = (int)(projectEndDate - projectStartDate).TotalDays + 1;
}
void InitializeUI()
{
taskListView = root.Q<ListView>("task-list-view");
taskListView.itemsSource = tasks;
taskListView.makeItem = () => taskRowTemplate.CloneTree();
taskListView.bindItem = (ve, i) => BindTaskRow(ve, tasks[i]);
taskListView.fixedItemHeight = 49;
taskListView.selectionType = SelectionType.None;
taskListView.showAlternatingRowBackgrounds = AlternatingRowBackground.None;
taskListView.reorderable = false;
taskListView.showBoundCollectionSize = false;
taskListView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
var internalScroll = taskListView.Q<ScrollView>();
if (internalScroll != null)
{
internalScroll.verticalScrollerVisibility = ScrollerVisibility.Hidden;
internalScroll.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
}
axisScrollView = root.Q<ScrollView>("time-axis-scroll");
if (axisScrollView != null)
{
axisScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
axisScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
}
timelineScrollView = root.Q<ScrollView>("timeline-scroll-view");
if (timelineScrollView != null)
{
timelineScrollView.mode = ScrollViewMode.VerticalAndHorizontal;
timelineContent = timelineScrollView.Q<VisualElement>("unity-content-container");
if (timelineContent != null)
timelineContent.style.width = totalDays * pixelsPerDay;
}
if (internalScroll != null && timelineScrollView != null)
{
internalScroll.verticalScroller.valueChanged += v => timelineScrollView.verticalScroller.value = v;
timelineScrollView.verticalScroller.valueChanged += v => internalScroll.verticalScroller.value = v;
}
if (axisScrollView != null && timelineScrollView != null)
{
axisScrollView.horizontalScroller.valueChanged += v => timelineScrollView.horizontalScroller.value = v;
timelineScrollView.horizontalScroller.valueChanged += v => axisScrollView.horizontalScroller.value = v;
}
}
void BindTaskRow(VisualElement row, ShipblockTask task)
{
Set(row, "c1", task?.L1); Set(row, "c2", task?.L2); Set(row, "c3", task?.L3); Set(row, "c4", task?.L4);
Set(row, "c5", task?.L5); Set(row, "c6", task?.L6); Set(row, "c7", task?.L7); Set(row, "c8", task?.L8);
row.tooltip = task != null ? $"{task.BLK_NO} | {task.SHIP_TYPE}" : string.Empty;
}
static void Set(VisualElement row, string name, string text)
{ var lab = row.Q<UnityEngine.UIElements.Label>(name); if (lab != null) lab.text = string.IsNullOrEmpty(text) ? string.Empty : text; }
void SetupTimeAxis() => SetupThreeTierHeader();
void SetupThreeTierHeader()
{
var container = root.Q<VisualElement>("time-axis-container");
container.Clear();
container.style.height = pixelsPerDay * 3; // total header height
// Background base grid style color
Color lineColor = ColorUtil.FromHex("#7BB7E2"); // light blue lines
var monthsLayer = new VisualElement { name = "months-layer" };
var weeksLayer = new VisualElement { name = "weeks-layer" };
var daysLayer = new VisualElement { name = "days-layer" };
foreach (var layer in new[] { monthsLayer, weeksLayer, daysLayer })
{
layer.style.position = Position.Absolute;
layer.style.left = 0;
layer.style.top = 0;
layer.style.height = pixelsPerDay * 3;
layer.style.width = totalDays * pixelsPerDay;
}
container.Add(monthsLayer); container.Add(weeksLayer); container.Add(daysLayer);
DateTime cursor = projectStartDate;
while (cursor <= projectEndDate)
{
DateTime monthStart = new DateTime(cursor.Year, cursor.Month, 1);
DateTime nextMonthStart = monthStart.AddMonths(1);
DateTime segmentStart = cursor; // first day we actually display within month
DateTime segmentEnd = nextMonthStart.AddDays(-1) < projectEndDate ? nextMonthStart.AddDays(-1) : projectEndDate;
int startIndex = (int)(segmentStart - projectStartDate).TotalDays;
int spanDays = (int)(segmentEnd - segmentStart).TotalDays + 1;
AddMonthLabel(monthsLayer, segmentStart.Year, segmentStart.Month, startIndex, spanDays, lineColor);
// Weeks inside month
DateTime weekCursor = segmentStart;
int weekOfMonth = 1; // label weeks sequentially per month
while (weekCursor <= segmentEnd)
{
DateTime weekStart = weekCursor;
// Keep the existing segmentation logic (end at Sunday or month end)
DateTime tentativeEnd = weekStart.AddDays(7 - (int)weekStart.DayOfWeek); // until Sunday
DateTime weekEnd = tentativeEnd > segmentEnd ? segmentEnd : tentativeEnd;
int weekStartIndex = (int)(weekStart - projectStartDate).TotalDays;
int weekSpanDays = (int)(weekEnd - weekStart).TotalDays + 1;
AddWeekLabel(weeksLayer, weekOfMonth, weekStartIndex, weekSpanDays, lineColor);
weekCursor = weekEnd.AddDays(1);
weekOfMonth++;
}
cursor = segmentEnd.AddDays(1);
}
// Days line
for (int i = 0; i < totalDays; i++)
{
DateTime date = projectStartDate.AddDays(i);
AddDayCell(daysLayer, date.Day, i, lineColor);
}
}
void AddMonthLabel(VisualElement parent, int year, int month, int startIndex, int spanDays, Color lineColor)
{
var ve = new VisualElement();
ve.style.position = Position.Absolute; ve.style.left = startIndex * pixelsPerDay; ve.style.top = 0;
ve.style.width = spanDays * pixelsPerDay; ve.style.height = pixelsPerDay;
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = startIndex == 0 ? 0 : 1;
ve.style.borderBottomColor = lineColor; ve.style.borderBottomWidth = 1;
var lab = new Label($"{year}년 {month}월"); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.unityFontStyleAndWeight = FontStyle.Normal; lab.style.color = Color.black;
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = spanDays * pixelsPerDay; lab.style.height = pixelsPerDay;
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void AddWeekLabel(VisualElement parent, int weekOfMonth, int startIndex, int spanDays, Color lineColor)
{
var ve = new VisualElement();
ve.style.position = Position.Absolute; ve.style.left = startIndex * pixelsPerDay; ve.style.top = pixelsPerDay;
ve.style.width = spanDays * pixelsPerDay; ve.style.height = pixelsPerDay;
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = startIndex == 0 ? 0 : 1;
ve.style.borderBottomColor = lineColor; ve.style.borderBottomWidth = 1;
var lab = new Label($"{weekOfMonth}주"); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.color = Color.black;
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = spanDays * pixelsPerDay; lab.style.height = pixelsPerDay;
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void AddDayCell(VisualElement parent, int day, int dayIndex, Color lineColor)
{
var ve = new VisualElement();
ve.style.position = Position.Absolute; ve.style.left = dayIndex * pixelsPerDay; ve.style.top = pixelsPerDay * 2;
ve.style.width = pixelsPerDay; ve.style.height = pixelsPerDay;
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = dayIndex == 0 ? 0 : 1;
var lab = new Label(day.ToString("00")); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.fontSize = 10; lab.style.color = Color.black;
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = pixelsPerDay; lab.style.height = pixelsPerDay;
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void RenderTasks()
{
if (timelineContent == null) return;
timelineContent.Clear();
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
var row = new VisualElement();
row.style.height = 40; row.style.flexDirection = FlexDirection.Row; row.style.position = Position.Relative;
row.style.borderBottomWidth = 1; row.style.borderBottomColor = new Color(0.9f, 0.9f, 0.9f);
if (task.GetPlanStart().HasValue && task.GetPlanEnd().HasValue)
row.Add(CreateBar(task, task.GetPlanStart().Value, task.GetPlanEnd().Value, planColor, 10));
if (task.GetActualStart().HasValue && task.GetActualEnd().HasValue)
row.Add(CreateBar(task, task.GetActualStart().Value, task.GetActualEnd().Value, actualColor, 22));
var progressLabel = new Label($"{Mathf.RoundToInt(task.CalculatedProgress)}%");
progressLabel.style.position = Position.Absolute; progressLabel.style.right = 8; progressLabel.style.top = 10; progressLabel.style.fontSize = 10; progressLabel.style.color = Color.black; progressLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
row.Add(progressLabel);
timelineContent.Add(row);
}
}
VisualElement CreateBar(ShipblockTask task, DateTime start, DateTime end, Color color, int top)
{
var bar = new VisualElement();
bar.style.position = Position.Absolute; bar.style.backgroundColor = color; bar.style.height = 12; bar.style.top = top;
bar.style.borderBottomLeftRadius = 2; bar.style.borderBottomRightRadius = 2; bar.style.borderTopLeftRadius = 2; bar.style.borderTopRightRadius = 2;
float startX = (float)(start - projectStartDate).TotalDays * pixelsPerDay;
float width = Mathf.Max((float)(end - start).TotalDays * pixelsPerDay, 6);
bar.style.left = startX; bar.style.width = width;
bar.tooltip = $"{task.GetDisplayName()}\n{start:MM/dd} ~ {end:MM/dd}";
return bar;
}
float CalculateProgress(ShipblockTask task)
{
var planStart = task.GetPlanStart(); var planEnd = task.GetPlanEnd(); var actualEnd = task.GetActualEnd();
if (!planStart.HasValue || !planEnd.HasValue) return 0;
float total = (float)(planEnd.Value - planStart.Value).TotalDays; if (total <= 0) return 0;
if (actualEnd.HasValue)
{ float actualDays = (float)(actualEnd.Value - planStart.Value).TotalDays; return Mathf.Clamp(actualDays / total * 100, 0, 100); }
return 0;
}
}
[Serializable]
public class JSONWrapper { public List<ShipblockTask> items; }