@glatam/calendar-core 1.0.1 → 1.0.3

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/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@glatam/calendar-core",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Lógica pura del calendario @glatam/calendar",
5
5
  "type": "module",
6
6
  "author": "Santiago Hernández Saldarriaga",
7
7
  "license": "MIT",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
10
13
  "repository": {
11
14
  "type": "git",
12
15
  "url": "git+https://github.com/santiagoshs/glatam-calendar.git"
@@ -24,6 +27,7 @@
24
27
  ],
25
28
  "scripts": {
26
29
  "build": "tsc",
30
+ "prepublishOnly": "npm run build",
27
31
  "test": "vitest run"
28
32
  },
29
33
  "devDependencies": {
package/src/index.ts DELETED
@@ -1,116 +0,0 @@
1
- import { BlockingRule, CalendarDay, TimeSlot, EvaluatedTimeSlot } from './types.js';
2
- import { BlockingStrategyFactory } from './strategies/factory.js';
3
- import { formatISODate, getStartOfWeek } from './utils/date-utils.js';
4
-
5
- export * from './types.js';
6
- export * from './utils/date-utils.js';
7
- export * from './strategies/strategy.interface.js';
8
- export * from './strategies/weekly.strategy.js';
9
- export * from './strategies/date-range.strategy.js';
10
- export * from './strategies/factory.js';
11
-
12
- /**
13
- * Checks if a specific date or time slot is blocked and returns the matching rule.
14
- */
15
- export function getBlockingRule(date: Date, slot: TimeSlot | undefined, rules: BlockingRule[]): BlockingRule | undefined {
16
- for (const rule of rules) {
17
- const strategy = BlockingStrategyFactory.getStrategy(rule.type);
18
- if (slot) {
19
- if (strategy.isSlotBlocked(date, slot, rule)) return rule;
20
- } else {
21
- if (strategy.isDateBlocked(date, rule)) return rule;
22
- }
23
- }
24
- return undefined;
25
- }
26
-
27
- export function isTimeBlocked(date: Date, slot: TimeSlot | undefined, rules: BlockingRule[]): boolean {
28
- return !!getBlockingRule(date, slot, rules);
29
- }
30
-
31
- /**
32
- * Generates an array of CalendarDay items representing a monthly calendar grid.
33
- */
34
- export function generateMonthDays(
35
- year: number,
36
- month: number,
37
- rules: BlockingRule[],
38
- slotsPerDay: TimeSlot[] = [],
39
- startOfWeekDay = 0
40
- ): CalendarDay[] {
41
- const firstDayOfMonth = new Date(year, month, 1);
42
- const startGridDate = getStartOfWeek(firstDayOfMonth, startOfWeekDay);
43
-
44
- const days: CalendarDay[] = [];
45
- const currentDate = new Date(startGridDate);
46
-
47
- for (let i = 0; i < 42; i++) {
48
- const dateCopy = new Date(currentDate);
49
- const dateString = formatISODate(dateCopy);
50
-
51
- const evaluatedSlots: EvaluatedTimeSlot[] = slotsPerDay.map((slot) => {
52
- const match = getBlockingRule(dateCopy, slot, rules);
53
- return { ...slot, isBlocked: !!match, rule: match };
54
- });
55
-
56
- const dayRule = getBlockingRule(dateCopy, undefined, rules);
57
- const isDayFullyBlocked = !!dayRule || (evaluatedSlots.length > 0 && evaluatedSlots.every((s) => s.isBlocked));
58
-
59
- days.push({
60
- dateString,
61
- date: dateCopy,
62
- isBlocked: isDayFullyBlocked,
63
- rule: dayRule || (evaluatedSlots.length > 0 && evaluatedSlots.every((s) => s.isBlocked) ? evaluatedSlots[0].rule : undefined),
64
- dayOfWeek: dateCopy.getDay(),
65
- dayNumber: dateCopy.getDate(),
66
- isCurrentMonth: dateCopy.getMonth() === month,
67
- slots: evaluatedSlots,
68
- });
69
-
70
- currentDate.setDate(currentDate.getDate() + 1);
71
- }
72
-
73
- return days;
74
- }
75
-
76
- /**
77
- * Generates an array of 7 CalendarDay items representing a single week view.
78
- */
79
- export function generateWeekDays(
80
- referenceDate: Date,
81
- rules: BlockingRule[],
82
- slotsPerDay: TimeSlot[] = [],
83
- startOfWeekDay = 0
84
- ): CalendarDay[] {
85
- const startOfWeek = getStartOfWeek(referenceDate, startOfWeekDay);
86
- const days: CalendarDay[] = [];
87
- const currentDate = new Date(startOfWeek);
88
-
89
- for (let i = 0; i < 7; i++) {
90
- const dateCopy = new Date(currentDate);
91
- const dateString = formatISODate(dateCopy);
92
-
93
- const evaluatedSlots: EvaluatedTimeSlot[] = slotsPerDay.map((slot) => {
94
- const match = getBlockingRule(dateCopy, slot, rules);
95
- return { ...slot, isBlocked: !!match, rule: match };
96
- });
97
-
98
- const dayRule = getBlockingRule(dateCopy, undefined, rules);
99
- const isDayFullyBlocked = !!dayRule || (evaluatedSlots.length > 0 && evaluatedSlots.every((s) => s.isBlocked));
100
-
101
- days.push({
102
- dateString,
103
- date: dateCopy,
104
- isBlocked: isDayFullyBlocked,
105
- rule: dayRule || (evaluatedSlots.length > 0 && evaluatedSlots.every((s) => s.isBlocked) ? evaluatedSlots[0].rule : undefined),
106
- dayOfWeek: dateCopy.getDay(),
107
- dayNumber: dateCopy.getDate(),
108
- isCurrentMonth: true,
109
- slots: evaluatedSlots,
110
- });
111
-
112
- currentDate.setDate(currentDate.getDate() + 1);
113
- }
114
-
115
- return days;
116
- }
@@ -1,41 +0,0 @@
1
- import { BlockingRule, TimeSlot } from '../types.js';
2
- import { BlockingStrategy } from './strategy.interface.js';
3
- import { formatISODate, isTimeOverlapping } from '../utils/date-utils.js';
4
-
5
- export class DateRangeStrategy implements BlockingStrategy {
6
- isDateBlocked(date: Date, rule: BlockingRule): boolean {
7
- if (!this.matchesDateRange(date, rule)) {
8
- return false;
9
- }
10
- // If no slots are defined, the entire day is blocked.
11
- return !rule.slots || rule.slots.length === 0;
12
- }
13
-
14
- isSlotBlocked(date: Date, slot: TimeSlot, rule: BlockingRule): boolean {
15
- if (!this.matchesDateRange(date, rule)) {
16
- return false;
17
- }
18
-
19
- // If no slots are defined, the entire day is blocked, so the slot is blocked.
20
- if (!rule.slots || rule.slots.length === 0) {
21
- return true;
22
- }
23
-
24
- // Check if the slot overlaps with any blocked slots in the rule
25
- return rule.slots.some((blockedSlot) => isTimeOverlapping(slot, blockedSlot));
26
- }
27
-
28
- private matchesDateRange(date: Date, rule: BlockingRule): boolean {
29
- const dateStr = formatISODate(date);
30
-
31
- if (rule.startDate && dateStr < rule.startDate) {
32
- return false;
33
- }
34
-
35
- if (rule.endDate && dateStr > rule.endDate) {
36
- return false;
37
- }
38
-
39
- return true;
40
- }
41
- }
@@ -1,22 +0,0 @@
1
- import { BlockingRuleType } from '../types.js';
2
- import { BlockingStrategy } from './strategy.interface.js';
3
- import { WeeklyStrategy } from './weekly.strategy.js';
4
- import { DateRangeStrategy } from './date-range.strategy.js';
5
-
6
- export class BlockingStrategyFactory {
7
- private static strategies: Record<BlockingRuleType, BlockingStrategy> = {
8
- weekly: new WeeklyStrategy(),
9
- 'date-range': new DateRangeStrategy(),
10
- };
11
-
12
- /**
13
- * Retrieves the strategy for a given rule type.
14
- */
15
- static getStrategy(type: BlockingRuleType): BlockingStrategy {
16
- const strategy = this.strategies[type];
17
- if (!strategy) {
18
- throw new Error(`Estrategia de bloqueo no soportada: ${type}`);
19
- }
20
- return strategy;
21
- }
22
- }
@@ -1,16 +0,0 @@
1
- import { BlockingRule, TimeSlot } from '../types.js';
2
-
3
- export interface BlockingStrategy {
4
- /**
5
- * Evaluates if a given date is blocked by the rule.
6
- * If the rule defines specific slots, a date is considered fully blocked
7
- * only if the whole day is covered, or we check slot-by-slot.
8
- * Here, isDateBlocked returns true if there is a day-wide block.
9
- */
10
- isDateBlocked(date: Date, rule: BlockingRule): boolean;
11
-
12
- /**
13
- * Evaluates if a specific time slot on a given date is blocked by this rule.
14
- */
15
- isSlotBlocked(date: Date, slot: TimeSlot, rule: BlockingRule): boolean;
16
- }
@@ -1,46 +0,0 @@
1
- import { BlockingRule, TimeSlot } from '../types.js';
2
- import { BlockingStrategy } from './strategy.interface.js';
3
- import { formatISODate, isTimeOverlapping } from '../utils/date-utils.js';
4
-
5
- export class WeeklyStrategy implements BlockingStrategy {
6
- isDateBlocked(date: Date, rule: BlockingRule): boolean {
7
- if (!this.matchesDateAndDay(date, rule)) {
8
- return false;
9
- }
10
- // If no slots are defined, the entire day is blocked.
11
- return !rule.slots || rule.slots.length === 0;
12
- }
13
-
14
- isSlotBlocked(date: Date, slot: TimeSlot, rule: BlockingRule): boolean {
15
- if (!this.matchesDateAndDay(date, rule)) {
16
- return false;
17
- }
18
-
19
- // If no slots are defined, the entire day is blocked, so the slot is blocked.
20
- if (!rule.slots || rule.slots.length === 0) {
21
- return true;
22
- }
23
-
24
- // Check if the slot overlaps with any blocked slots in the rule
25
- return rule.slots.some((blockedSlot) => isTimeOverlapping(slot, blockedSlot));
26
- }
27
-
28
- private matchesDateAndDay(date: Date, rule: BlockingRule): boolean {
29
- const dayOfWeek = date.getDay();
30
- if (!rule.daysOfWeek || !rule.daysOfWeek.includes(dayOfWeek)) {
31
- return false;
32
- }
33
-
34
- const dateStr = formatISODate(date);
35
-
36
- if (rule.startDate && dateStr < rule.startDate) {
37
- return false;
38
- }
39
-
40
- if (rule.endDate && dateStr > rule.endDate) {
41
- return false;
42
- }
43
-
44
- return true;
45
- }
46
- }
package/src/types.ts DELETED
@@ -1,35 +0,0 @@
1
- export interface TimeSlot {
2
- start: string;
3
- end: string;
4
- }
5
-
6
- export type BlockingRuleType = 'weekly' | 'date-range';
7
-
8
- export interface BlockingRule {
9
- id: string;
10
- type: BlockingRuleType;
11
- startDate?: string;
12
- endDate?: string;
13
- daysOfWeek?: number[];
14
- slots?: TimeSlot[];
15
- description?: string; // Descriptive title/notes for the task/block
16
- [key: string]: any; // Allow custom metadata (assignees, status, priority)
17
- }
18
-
19
- export interface CalendarDay {
20
- dateString: string;
21
- date: Date;
22
- isBlocked: boolean;
23
- rule?: BlockingRule; // The rule blocking the whole day
24
- dayOfWeek: number;
25
- dayNumber: number;
26
- isCurrentMonth: boolean;
27
- slots: EvaluatedTimeSlot[];
28
- }
29
-
30
- export interface EvaluatedTimeSlot {
31
- start: string;
32
- end: string;
33
- isBlocked: boolean;
34
- rule?: BlockingRule; // The rule that caused the block
35
- }
@@ -1,59 +0,0 @@
1
- import { TimeSlot } from '../types.js';
2
-
3
- /**
4
- * Parses an ISO date string (YYYY-MM-DD) into a local Date object.
5
- * Prevents timezone shifting issues.
6
- */
7
- export function parseISODate(dateStr: string): Date {
8
- const parts = dateStr.split('T')[0].split('-');
9
- if (parts.length !== 3) {
10
- return new Date(dateStr);
11
- }
12
- const year = parseInt(parts[0], 10);
13
- const month = parseInt(parts[1], 10) - 1; // 0-indexed
14
- const day = parseInt(parts[2], 10);
15
- return new Date(year, month, day);
16
- }
17
-
18
- /**
19
- * Formats a Date object to YYYY-MM-DD in the local timezone.
20
- */
21
- export function formatISODate(date: Date): string {
22
- const year = date.getFullYear();
23
- const month = String(date.getMonth() + 1).padStart(2, '0');
24
- const day = String(date.getDate()).padStart(2, '0');
25
- return `${year}-${month}-${day}`;
26
- }
27
-
28
- /**
29
- * Converts a time string "HH:MM" to minutes from midnight.
30
- */
31
- export function timeToMinutes(timeStr: string): number {
32
- const [hours, minutes] = timeStr.split(':').map(Number);
33
- return (hours || 0) * 60 + (minutes || 0);
34
- }
35
-
36
- /**
37
- * Checks if two time slots overlap.
38
- */
39
- export function isTimeOverlapping(slot1: TimeSlot, slot2: TimeSlot): boolean {
40
- const start1 = timeToMinutes(slot1.start);
41
- const end1 = timeToMinutes(slot1.end);
42
- const start2 = timeToMinutes(slot2.start);
43
- const end2 = timeToMinutes(slot2.end);
44
-
45
- return start1 < end2 && start2 < end1;
46
- }
47
-
48
- /**
49
- * Gets the start of the week for a given date.
50
- * @param date The date reference.
51
- * @param startOfWeekDay The day the week starts (0 = Sunday, 1 = Monday). Default is 0.
52
- */
53
- export function getStartOfWeek(date: Date, startOfWeekDay = 0): Date {
54
- const result = new Date(date.getFullYear(), date.getMonth(), date.getDate());
55
- const day = result.getDay();
56
- const diff = (day < startOfWeekDay ? 7 : 0) + day - startOfWeekDay;
57
- result.setDate(result.getDate() - diff);
58
- return result;
59
- }
@@ -1,100 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- isTimeBlocked,
4
- generateMonthDays,
5
- generateWeekDays,
6
- parseISODate,
7
- formatISODate,
8
- BlockingRule,
9
- TimeSlot,
10
- } from '../src/index.js';
11
-
12
- describe('Calendar Core tests', () => {
13
- describe('Date Utilities', () => {
14
- it('should parse YYYY-MM-DD to local Date object timezone-neutrally', () => {
15
- const parsed = parseISODate('2026-07-18');
16
- expect(parsed.getFullYear()).toBe(2026);
17
- expect(parsed.getMonth()).toBe(6); // July is 6
18
- expect(parsed.getDate()).toBe(18);
19
- });
20
-
21
- it('should format Date object to YYYY-MM-DD string', () => {
22
- const date = new Date(2026, 6, 18);
23
- expect(formatISODate(date)).toBe('2026-07-18');
24
- });
25
- });
26
-
27
- describe('isTimeBlocked with WeeklyStrategy', () => {
28
- const rules: BlockingRule[] = [
29
- {
30
- id: '1',
31
- type: 'weekly',
32
- daysOfWeek: [1, 3], // Monday and Wednesday
33
- slots: [
34
- { start: '09:00', end: '11:00' },
35
- { start: '14:00', end: '16:00' },
36
- ],
37
- },
38
- ];
39
-
40
- it('should block matching slots on Monday', () => {
41
- const monday = new Date(2026, 6, 13); // Monday
42
- const slot: TimeSlot = { start: '09:30', end: '10:30' };
43
- expect(isTimeBlocked(monday, slot, rules)).toBe(true);
44
- });
45
-
46
- it('should NOT block non-matching slots on Monday', () => {
47
- const monday = new Date(2026, 6, 13); // Monday
48
- const slot: TimeSlot = { start: '11:30', end: '13:00' };
49
- expect(isTimeBlocked(monday, slot, rules)).toBe(false);
50
- });
51
-
52
- it('should NOT block any slots on Tuesday', () => {
53
- const tuesday = new Date(2026, 6, 14); // Tuesday
54
- const slot: TimeSlot = { start: '09:30', end: '10:30' };
55
- expect(isTimeBlocked(tuesday, slot, rules)).toBe(false);
56
- });
57
- });
58
-
59
- describe('isTimeBlocked with DateRangeStrategy', () => {
60
- const rules: BlockingRule[] = [
61
- {
62
- id: '2',
63
- type: 'date-range',
64
- startDate: '2026-07-20',
65
- endDate: '2026-07-25',
66
- // No slots means block the entire day
67
- },
68
- ];
69
-
70
- it('should block any time on dates within range', () => {
71
- const testDate = new Date(2026, 6, 22); // July 22, 2026
72
- expect(isTimeBlocked(testDate, undefined, rules)).toBe(true);
73
- });
74
-
75
- it('should NOT block dates outside range', () => {
76
- const testDate = new Date(2026, 6, 26); // July 26, 2026
77
- expect(isTimeBlocked(testDate, undefined, rules)).toBe(false);
78
- });
79
- });
80
-
81
- describe('Grid Generation', () => {
82
- it('should generate 42 days for month view grid', () => {
83
- const rules: BlockingRule[] = [];
84
- const days = generateMonthDays(2026, 6, rules); // July 2026
85
- expect(days.length).toBe(42);
86
- // July 1st, 2026 is a Wednesday.
87
- // If startOfWeekDay is 0 (Sunday), the first day of grid should be June 28th.
88
- expect(days[0].dateString).toBe('2026-06-28');
89
- });
90
-
91
- it('should generate 7 days for week view grid', () => {
92
- const rules: BlockingRule[] = [];
93
- const days = generateWeekDays(new Date(2026, 6, 18), rules, [], 1); // July 18, 2026 (Saturday), start Monday
94
- expect(days.length).toBe(7);
95
- // Mon of that week is July 13th
96
- expect(days[0].dateString).toBe('2026-07-13');
97
- expect(days[6].dateString).toBe('2026-07-19');
98
- });
99
- });
100
- });
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"]
8
- }