diff --git a/src/KonvaTimeline/yearly-scenario.stories.tsx b/src/KonvaTimeline/yearly-scenario.stories.tsx index 074e055..50daee9 100644 --- a/src/KonvaTimeline/yearly-scenario.stories.tsx +++ b/src/KonvaTimeline/yearly-scenario.stories.tsx @@ -27,6 +27,31 @@ const yearlyStoryData = generateStoryData({ export const YearlyReport: Story = { args: { ...yearlyStoryData, - resolution: "1day", + resolution: "30min", + columnWidth: 120, + range: { + start: 1698357600000, + end: 1712095200000, + }, + tasks: [ + { + id: "1", + label: "1Novembre", + resourceId: "1", + time: { + start: 1698793200000, + end: 1700434800000, + }, + }, + { + id: "2", + label: "26Marzo", + resourceId: "1", + time: { + start: 1711839600000, + end: 1711922399000, + }, + }, + ], }, }; diff --git a/src/grid/Cell/index.tsx b/src/grid/Cell/index.tsx index edd2053..a22a2a3 100644 --- a/src/grid/Cell/index.tsx +++ b/src/grid/Cell/index.tsx @@ -9,9 +9,13 @@ interface GridCellProps { column: Interval; height: number; index: number; + hourInfo: { + backHour?: boolean; + nextHour?: boolean; + }; } -const GridCell = ({ column, height, index }: GridCellProps) => { +const GridCell = ({ column, height, index, hourInfo: visibleDayInfo }: GridCellProps) => { const { blocksOffset, columnWidth, @@ -22,7 +26,28 @@ const GridCell = ({ column, height, index }: GridCellProps) => { const cellLabel = useMemo(() => displayInterval(column, resolutionUnit), [column, resolutionUnit]); - const xPos = useMemo(() => columnWidth * (index + blocksOffset), [blocksOffset, columnWidth, index]); + const xPos = useMemo(() => { + if (resolutionUnit === "day") { + if (visibleDayInfo.backHour) { + return columnWidth * (index + blocksOffset) + columnWidth / 24; + } + + if (visibleDayInfo.nextHour) { + return columnWidth * (index + blocksOffset) - columnWidth / 24; + } + } + if (resolutionUnit === "week") { + if (visibleDayInfo.backHour) { + return columnWidth * (index + blocksOffset) + columnWidth / 168; + } + + if (visibleDayInfo.nextHour) { + return columnWidth * (index + blocksOffset) - columnWidth / 168; + } + } + + return columnWidth * (index + blocksOffset); + }, [blocksOffset, columnWidth, index, visibleDayInfo, resolutionUnit]); const yPos = useMemo(() => rowHeight * 0.8, [rowHeight]); diff --git a/src/grid/CellGroup/index.tsx b/src/grid/CellGroup/index.tsx index 60edca7..936f722 100644 --- a/src/grid/CellGroup/index.tsx +++ b/src/grid/CellGroup/index.tsx @@ -7,11 +7,18 @@ import { displayAboveInterval } from "../../utils/time-resolution"; interface GridCellGroupProps { column: Interval; - height: number; index: number; + dayInfo?: { + thisMonth?: number; + untilNow?: number; + }[]; + hourInfo?: { + backHour?: boolean; + nextHour?: boolean; + }; } -const GridCellGroup = ({ column, height, index }: GridCellGroupProps) => { +const GridCellGroup = ({ column, index, dayInfo, hourInfo }: GridCellGroupProps) => { const { columnWidth, resolution: { sizeInUnits, unit, unitAbove }, @@ -21,24 +28,82 @@ const GridCellGroup = ({ column, height, index }: GridCellGroupProps) => { const cellLabel = useMemo(() => displayAboveInterval(column, unitAbove), [column, unitAbove]); - const points = useMemo(() => [0, 0, 0, height], [height]); + const points = useMemo(() => [0, 0, 0, rowHeight], [rowHeight]); - const unitAboveInUnitBelow = useMemo( - () => Duration.fromObject({ [unitAbove]: 1 }).as(unit) / sizeInUnits, - [sizeInUnits, unit, unitAbove] - ); + const unitAboveInUnitBelow = useMemo(() => { + if (unitAbove === "month") { + return Duration.fromObject({ ["day"]: dayInfo![index].thisMonth }).as("week") / sizeInUnits; + } + + return Duration.fromObject({ [unitAbove]: 1 }).as(unit) / sizeInUnits; + }, [sizeInUnits, dayInfo, index, unitAbove, unit]); + + const unitAboveSpanInPx = useMemo(() => { + return unitAboveInUnitBelow * columnWidth; + }, [columnWidth, unitAboveInUnitBelow]); + + const xPos = useMemo(() => { + if (unitAbove === "month") { + const pxUntil = + index !== 0 ? Duration.fromObject({ ["day"]: dayInfo![index - 1].untilNow }).as("week") / sizeInUnits : 0; + + if (hourInfo!.backHour) { + const hourInMonthPx = columnWidth / 168; + return pxUntil * columnWidth + unitAboveSpanInPx + hourInMonthPx; + } + + if (hourInfo!.nextHour) { + const hourInMonthPx = columnWidth / 168; + return pxUntil * columnWidth + unitAboveSpanInPx - hourInMonthPx; + } - const unitAboveSpanInPx = useMemo(() => unitAboveInUnitBelow * columnWidth, [columnWidth, unitAboveInUnitBelow]); + return pxUntil * columnWidth + unitAboveSpanInPx; + } - const xPos = useMemo(() => index * unitAboveSpanInPx, [index, unitAboveSpanInPx]); + if (unitAbove === "day") { + if (hourInfo!.backHour) { + return index * unitAboveSpanInPx + columnWidth / sizeInUnits; + } + + if (hourInfo!.nextHour) { + return index * unitAboveSpanInPx - columnWidth / sizeInUnits; + } + } + + if (unitAbove === "week") { + if (hourInfo!.backHour) { + return index * unitAboveSpanInPx + columnWidth / 24; + } + + if (hourInfo!.nextHour) { + return index * unitAboveSpanInPx - columnWidth / 24; + } + } + + return index * unitAboveSpanInPx; + }, [index, unitAboveSpanInPx, columnWidth, sizeInUnits, dayInfo, unitAbove, hourInfo]); const yPos = useMemo(() => rowHeight * 0.3, [rowHeight]); + const xPosLabel = useMemo(() => { + if (unitAbove === "month") { + return xPos - unitAboveSpanInPx; + } + return index * unitAboveSpanInPx; + }, [xPos, unitAboveSpanInPx, unitAbove, index]); + return ( - + ); }; diff --git a/src/grid/Cells/index.tsx b/src/grid/Cells/index.tsx index 2a80bd4..be4b359 100644 --- a/src/grid/Cells/index.tsx +++ b/src/grid/Cells/index.tsx @@ -1,7 +1,8 @@ -import React, { memo } from "react"; +import React, { memo, useMemo } from "react"; import { KonvaGroup } from "../../@konva"; import { useTimelineContext } from "../../timeline/TimelineContext"; +import { dayDetail, timeBlockTz } from "../../utils/timeBlockArray"; import GridCell from "../Cell"; import GridCellGroup from "../CellGroup"; @@ -10,15 +11,42 @@ interface GridCellsProps { } const GridCells = ({ height }: GridCellsProps) => { - const { aboveTimeBlocks, visibleTimeBlocks } = useTimelineContext(); + const { + interval, + aboveTimeBlocks, + visibleTimeBlocks, + resolution: { unitAbove }, + } = useTimelineContext(); + + const tz = useMemo(() => interval.start!.toISO()!.slice(-6), [interval]); + + const dayInfo = useMemo( + () => dayDetail(unitAbove, aboveTimeBlocks, interval), + [unitAbove, aboveTimeBlocks, interval] + ); + + const aboveHourInfo = useMemo(() => timeBlockTz(aboveTimeBlocks, tz), [tz, aboveTimeBlocks]); + const visibileHourInfo = useMemo(() => timeBlockTz(visibleTimeBlocks, tz), [tz, visibleTimeBlocks]); return ( {aboveTimeBlocks.map((column, index) => ( - + ))} {visibleTimeBlocks.map((column, index) => ( - + ))} ); diff --git a/src/timeline/TimelineContext.tsx b/src/timeline/TimelineContext.tsx index d8eed55..c905b41 100644 --- a/src/timeline/TimelineContext.tsx +++ b/src/timeline/TimelineContext.tsx @@ -105,7 +105,7 @@ export const TimelineProvider = ({ columnWidth: externalColumnWidth, debug = false, displayTasksLabel = false, - dragResolution: externalDragResolution, + dragResolution: externalDragResolution = "1min", enableDrag = true, enableResize = true, headerLabel, @@ -116,7 +116,7 @@ export const TimelineProvider = ({ onTaskChange, tasks: externalTasks = [], range: externalRange, - resolution: externalResolution = "1min", + resolution: externalResolution = "1hrs", resources: externalResources, rowHeight: externalRowHeight, timezone: externalTimezone, @@ -206,7 +206,27 @@ export const TimelineProvider = ({ [interval, resolution] ); - const aboveTimeBlocks = useMemo(() => interval.splitBy({ [resolution.unitAbove]: 1 }), [interval, resolution]); + const aboveTimeBlocks = useMemo(() => { + const { unitAbove } = resolution; + const blocks: Interval[] = []; + const intervalStart = interval.start!; + const intervalEnd = interval.end!; + + let blockStart = intervalStart; + + while (blockStart < intervalEnd) { + let blockEnd = blockStart.endOf(unitAbove); + + if (blockEnd > intervalEnd) { + blockEnd = intervalEnd; + } + + blocks.push(Interval.fromDateTimes(blockStart, blockEnd)); + blockStart = blockEnd.startOf(unitAbove).plus({ [unitAbove]: 1 }); + } + + return blocks; + }, [interval, resolution]); const columnWidth = useMemo(() => { logDebug("TimelineProvider", "Calculating columnWidth..."); diff --git a/src/utils/time-resolution.ts b/src/utils/time-resolution.ts index b092433..7331dd2 100644 --- a/src/utils/time-resolution.ts +++ b/src/utils/time-resolution.ts @@ -1,4 +1,4 @@ -import { Interval } from "luxon"; +import { DateTime, Interval } from "luxon"; import { DEFAULT_GRID_COLUMN_WIDTH } from "./dimensions"; @@ -148,16 +148,45 @@ export const displayAboveInterval = (interval: Interval, unit: Scale): string => case "hour": return start.toFormat("dd/MM/yy HH:mm"); case "day": - return start.toFormat("ccc dd MMM yyyy"); + return start.toFormat("ccc dd yyyy"); case "week": return `${start.toFormat("MMM yyyy")} CW ${start.toFormat("WW")}`; case "month": - return start.toFormat("yyyy"); + return start.toFormat("MMM yyyy"); default: return "N/A"; } }; +export const getMonth = (interval: Interval): string => { + const { start } = interval; + if (!start) { + return "-"; + } + + return start.toFormat("M"); +}; +export const getYear = (interval: Interval): string => { + const { start } = interval; + if (!start) { + return "-"; + } + + return start.toFormat("yyyy"); +}; + +export const getStartMonthsDay = (start: DateTime): string => { + if (!start) { + return "-"; + } + + return start.toFormat("d"); +}; + +export const daysInMonth = (month: number, year: number) => { + return new Date(year, month, 0).getDate(); +}; + /** * Util to display an interval in a human readable format * @param interval the interval to display diff --git a/src/utils/time.ts b/src/utils/time.ts index 2dc7a5a..ef1b521 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -71,7 +71,11 @@ export const getIntervalFromInternalTimeRange = ( timezone: string | undefined ): Interval => { const tz = timezone || "system"; - const startDateTime = DateTime.fromMillis(start, { zone: tz }).startOf(resolution.unitAbove); - const endDateTime = DateTime.fromMillis(end, { zone: tz }).endOf(resolution.unitAbove); + const startDateTime = DateTime.fromMillis(start, { zone: tz }).startOf( + resolution.unitAbove !== "month" ? resolution.unitAbove : resolution.unit + ); + const endDateTime = DateTime.fromMillis(end, { zone: tz }).endOf( + resolution.unitAbove !== "month" ? resolution.unitAbove : resolution.unit + ); return Interval.fromDateTimes(startDateTime, endDateTime); }; diff --git a/src/utils/timeBlockArray.ts b/src/utils/timeBlockArray.ts new file mode 100644 index 0000000..7a19c4d --- /dev/null +++ b/src/utils/timeBlockArray.ts @@ -0,0 +1,78 @@ +import { Interval } from "luxon"; + +import { daysInMonth, getMonth, getStartMonthsDay, getYear, Scale } from "./time-resolution"; + +interface VisibleHourInfoProps { + backHour?: boolean; + nextHour?: boolean; +} + +interface DayDetailProps { + thisMonth?: number; + untilNow?: number; +} + +export const timeBlockTz = (timeBlock: Interval[], initialTz?: string) => { + const dayInfoArray: VisibleHourInfoProps[] = []; + + timeBlock.forEach((column) => { + const tzStart = column.start!.toISO()?.slice(-6); + + if (initialTz !== tzStart) { + if (Number(initialTz?.slice(1, 3)) - Number(tzStart!.slice(1, 3)) > 0) { + dayInfoArray.push({ + backHour: true, + nextHour: false, + }); + + return; + } + + dayInfoArray.push({ + backHour: false, + nextHour: true, + }); + } + + dayInfoArray.push({ + backHour: false, + nextHour: false, + }); + + return; + }); + + return dayInfoArray; +}; + +export const dayDetail = (unitAbove: Scale, aboveTimeBlocks: Interval[], interval: Interval) => { + if (unitAbove === "month") { + const dayInfo: DayDetailProps[] = []; + + aboveTimeBlocks.forEach((column, index) => { + const month = getMonth(column); + const year = getYear(column); + const currentMonthDays = daysInMonth(Number(month), Number(year)); + + if (index === 0) { + const startDay = getStartMonthsDay(interval.start!); + const daysToMonthEnd = currentMonthDays - Number(startDay) + 1; + dayInfo.push({ + thisMonth: daysToMonthEnd, + untilNow: daysToMonthEnd, + }); + + return; + } + + const n = dayInfo[index - 1].untilNow! + currentMonthDays; + dayInfo.push({ + thisMonth: currentMonthDays, + untilNow: n, + }); + }); + + return dayInfo; + } + return []; +};