📝(docs) reorganize docs in docs/
This commit is contained in:
@@ -1,352 +0,0 @@
|
||||
# ✅ Recurring Events Implementation Checklist
|
||||
|
||||
## 📋 Implementation Status
|
||||
|
||||
### ✅ COMPLETED (Ready to Use)
|
||||
|
||||
#### Frontend Components
|
||||
- [x] **RecurrenceEditor.tsx** - Complete UI component
|
||||
- [x] Simple mode (Daily/Weekly/Monthly/Yearly)
|
||||
- [x] Custom mode with full controls
|
||||
- [x] DAILY: Interval support
|
||||
- [x] WEEKLY: Day selection + interval
|
||||
- [x] MONTHLY: Day of month (1-31)
|
||||
- [x] YEARLY: Month + day selection
|
||||
- [x] End conditions (Never/Until/Count)
|
||||
- [x] Date validation with warnings
|
||||
|
||||
#### Styles
|
||||
- [x] **RecurrenceEditor.scss** - Complete styles
|
||||
- [x] BEM methodology
|
||||
- [x] Weekday buttons
|
||||
- [x] Warning messages
|
||||
- [x] Responsive layout
|
||||
- [x] Integrated with design system
|
||||
|
||||
#### Translations
|
||||
- [x] **translations.json** - All languages
|
||||
- [x] English (en)
|
||||
- [x] French (fr)
|
||||
- [x] Dutch (nl)
|
||||
- [x] All UI strings
|
||||
- [x] Month names
|
||||
- [x] Validation warnings
|
||||
|
||||
#### Tests
|
||||
- [x] **RecurrenceEditor.test.tsx** - Full test suite
|
||||
- [x] 15+ test cases
|
||||
- [x] Component rendering
|
||||
- [x] User interactions
|
||||
- [x] All frequency types
|
||||
- [x] Date validation
|
||||
- [x] End conditions
|
||||
- [x] Edge cases
|
||||
|
||||
#### Documentation
|
||||
- [x] **README_RECURRENCE.md** - Main entry point
|
||||
- [x] **RECURRENCE_SUMMARY.md** - Quick reference
|
||||
- [x] **RECURRENCE_IMPLEMENTATION.md** - Technical guide
|
||||
- [x] **SCHEDULER_RECURRENCE_INTEGRATION.md** - Integration steps
|
||||
- [x] **RECURRENCE_EXAMPLES.md** - Real-world examples
|
||||
- [x] **IMPLEMENTATION_CHECKLIST.md** - This file
|
||||
|
||||
---
|
||||
|
||||
## 🔄 PENDING (Needs Integration)
|
||||
|
||||
### Integration Tasks
|
||||
|
||||
- [ ] **Add RecurrenceEditor to EventModal**
|
||||
- [ ] Import component in Scheduler.tsx
|
||||
- [ ] Add recurrence state
|
||||
- [ ] Add toggle button
|
||||
- [ ] Reset state in useEffect
|
||||
- [ ] Include in IcsEvent save
|
||||
- [ ] Test in browser
|
||||
|
||||
### Steps to Complete Integration
|
||||
|
||||
Follow [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md):
|
||||
|
||||
1. Import RecurrenceEditor
|
||||
2. Add state management
|
||||
3. Add UI toggle button
|
||||
4. Include recurrence in save
|
||||
5. Test end-to-end
|
||||
|
||||
**Estimated time:** 30-45 minutes
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Enhancements (Optional)
|
||||
|
||||
### Not Required for MVP
|
||||
|
||||
- [ ] **Advanced Patterns**
|
||||
- [ ] BYSETPOS support ("1st Monday", "Last Friday")
|
||||
- [ ] Position-based recurrence
|
||||
- [ ] Complex patterns UI
|
||||
|
||||
- [ ] **UI Improvements**
|
||||
- [ ] Visual calendar preview of pattern
|
||||
- [ ] Natural language summary ("Every 2 weeks on Monday")
|
||||
- [ ] Recurring event icon in calendar view
|
||||
|
||||
- [ ] **Editing Features**
|
||||
- [ ] Edit single instance vs series UI
|
||||
- [ ] Delete options (this/future/all)
|
||||
- [ ] Exception handling UI
|
||||
- [ ] RECURRENCE-ID support in UI
|
||||
|
||||
- [ ] **Time Zone**
|
||||
- [ ] Better time zone handling for UNTIL
|
||||
- [ ] Time zone selector
|
||||
- [ ] DST handling
|
||||
|
||||
---
|
||||
|
||||
## 📊 Feature Coverage
|
||||
|
||||
### Supported ✅
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| DAILY recurrence | ✅ | With interval |
|
||||
| WEEKLY recurrence | ✅ | Multiple days |
|
||||
| MONTHLY recurrence | ✅ | Day 1-31 |
|
||||
| YEARLY recurrence | ✅ | Month + day |
|
||||
| Never-ending | ✅ | No UNTIL or COUNT |
|
||||
| Until date | ✅ | UNTIL parameter |
|
||||
| After N occurrences | ✅ | COUNT parameter |
|
||||
| Interval (every X) | ✅ | All frequencies |
|
||||
| Date validation | ✅ | Feb 29, month lengths |
|
||||
| Warning messages | ✅ | Invalid dates |
|
||||
| Translations | ✅ | EN, FR, NL |
|
||||
| Tests | ✅ | 15+ cases |
|
||||
| Documentation | ✅ | Complete |
|
||||
|
||||
### Not Supported (Yet) ❌
|
||||
|
||||
| Feature | Status | Reason |
|
||||
|---------|--------|--------|
|
||||
| nth occurrence | ❌ | Needs BYSETPOS UI |
|
||||
| Last occurrence | ❌ | Needs BYSETPOS=-1 UI |
|
||||
| Edit single instance | ❌ | Needs RECURRENCE-ID UI |
|
||||
| Multiple months | ❌ | UI not implemented |
|
||||
| Complex patterns | ❌ | Advanced use case |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 RFC 5545 Compliance
|
||||
|
||||
### Implemented RRULE Parameters
|
||||
|
||||
- [x] `FREQ` - Frequency (DAILY/WEEKLY/MONTHLY/YEARLY)
|
||||
- [x] `INTERVAL` - Recurrence interval (every X periods)
|
||||
- [x] `BYDAY` - Days of week (for WEEKLY)
|
||||
- [x] `BYMONTHDAY` - Day of month (1-31)
|
||||
- [x] `BYMONTH` - Month (1-12)
|
||||
- [x] `COUNT` - Number of occurrences
|
||||
- [x] `UNTIL` - End date
|
||||
|
||||
### Not Implemented
|
||||
|
||||
- [ ] `BYSETPOS` - Position in set (1st, 2nd, last)
|
||||
- [ ] `BYYEARDAY` - Day of year
|
||||
- [ ] `BYWEEKNO` - Week number
|
||||
- [ ] `BYHOUR` - Hour (not applicable for calendar events)
|
||||
- [ ] `BYMINUTE` - Minute (not applicable)
|
||||
- [ ] `BYSECOND` - Second (not applicable)
|
||||
- [ ] `WKST` - Week start (using default)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test Coverage
|
||||
|
||||
### Unit Tests ✅
|
||||
|
||||
```bash
|
||||
npm test RecurrenceEditor
|
||||
```
|
||||
|
||||
**Coverage:**
|
||||
- Component rendering: ✅
|
||||
- Simple mode selection: ✅
|
||||
- Custom mode UI: ✅
|
||||
- Weekly day toggles: ✅
|
||||
- Monthly day input: ✅
|
||||
- Yearly month/day: ✅
|
||||
- End conditions: ✅
|
||||
- Date validation: ✅
|
||||
- Warning messages: ✅
|
||||
|
||||
### Integration Tests ⏳
|
||||
|
||||
- [ ] Create recurring event in Scheduler
|
||||
- [ ] Edit recurring event
|
||||
- [ ] Delete recurring event
|
||||
- [ ] View recurring instances in calendar
|
||||
- [ ] Sync with CalDAV server
|
||||
- [ ] Email invitations for recurring events
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
See [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md#testing) for full checklist.
|
||||
|
||||
**Priority test cases:**
|
||||
- [ ] Daily with interval 3
|
||||
- [ ] Weekly on Mon/Wed/Fri
|
||||
- [ ] Monthly on 31st (edge case)
|
||||
- [ ] Yearly on Feb 29 (leap year)
|
||||
- [ ] Until date
|
||||
- [ ] Count 10 occurrences
|
||||
- [ ] Edit existing recurring event
|
||||
|
||||
---
|
||||
|
||||
## 📦 Files Summary
|
||||
|
||||
### New Files Created (9)
|
||||
|
||||
#### Code Files (3)
|
||||
```
|
||||
src/frontend/apps/calendars/src/features/calendar/components/
|
||||
├── RecurrenceEditor.tsx ✅ 377 lines
|
||||
├── RecurrenceEditor.scss ✅ 58 lines
|
||||
└── __tests__/
|
||||
└── RecurrenceEditor.test.tsx ✅ 300+ lines
|
||||
```
|
||||
|
||||
#### Documentation Files (6)
|
||||
```
|
||||
(project root)
|
||||
├── README_RECURRENCE.md ✅ Main README
|
||||
├── RECURRENCE_SUMMARY.md ✅ Quick reference
|
||||
├── RECURRENCE_IMPLEMENTATION.md ✅ Technical docs
|
||||
├── SCHEDULER_RECURRENCE_INTEGRATION.md ✅ Integration guide
|
||||
├── RECURRENCE_EXAMPLES.md ✅ Usage examples
|
||||
└── IMPLEMENTATION_CHECKLIST.md ✅ This file
|
||||
```
|
||||
|
||||
### Modified Files (1)
|
||||
|
||||
```
|
||||
src/frontend/apps/calendars/src/features/i18n/
|
||||
└── translations.json ✅ Added recurrence keys
|
||||
```
|
||||
|
||||
**Total lines of code:** ~750+
|
||||
**Total documentation:** ~3000+ lines
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Knowledge Resources
|
||||
|
||||
### Internal Documentation
|
||||
1. [README_RECURRENCE.md](./README_RECURRENCE.md) - Start here
|
||||
2. [RECURRENCE_SUMMARY.md](./RECURRENCE_SUMMARY.md) - Quick reference
|
||||
3. [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md) - Deep dive
|
||||
4. [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md) - How to integrate
|
||||
5. [RECURRENCE_EXAMPLES.md](./RECURRENCE_EXAMPLES.md) - Real examples
|
||||
|
||||
### External Resources
|
||||
- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE Specification](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics Documentation](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [Sabre/dav Documentation](https://sabre.io/dav/)
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Current Status
|
||||
|
||||
### ✅ Ready for Integration
|
||||
|
||||
**The RecurrenceEditor component is complete and production-ready!**
|
||||
|
||||
All you need to do:
|
||||
1. Follow [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md)
|
||||
2. Add 5 simple changes to Scheduler.tsx
|
||||
3. Test in browser
|
||||
|
||||
### 📈 Progress
|
||||
|
||||
```
|
||||
Implementation: ████████████████████ 100% COMPLETE
|
||||
Integration: ░░░░░░░░░░░░░░░░░░░░ 0% PENDING
|
||||
Testing: ██████████░░░░░░░░░░ 50% PARTIAL
|
||||
Documentation: ████████████████████ 100% COMPLETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Next Steps
|
||||
|
||||
### Immediate (Required)
|
||||
|
||||
1. **Read integration guide**
|
||||
→ [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md)
|
||||
|
||||
2. **Integrate in Scheduler**
|
||||
→ Follow 5-step guide (30-45 min)
|
||||
|
||||
3. **Test in browser**
|
||||
→ Create/edit recurring events
|
||||
|
||||
### Soon (Recommended)
|
||||
|
||||
1. **Run test suite**
|
||||
```bash
|
||||
npm test RecurrenceEditor
|
||||
```
|
||||
|
||||
2. **Manual testing**
|
||||
→ Use [testing checklist](./RECURRENCE_IMPLEMENTATION.md#testing)
|
||||
|
||||
3. **User feedback**
|
||||
→ Gather feedback from team
|
||||
|
||||
### Later (Optional)
|
||||
|
||||
1. **Consider enhancements**
|
||||
→ BYSETPOS patterns, edit single instance
|
||||
|
||||
2. **Add visual preview**
|
||||
→ Calendar preview of recurrence pattern
|
||||
|
||||
3. **Natural language summary**
|
||||
→ "Every 2 weeks on Monday and Friday"
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you encounter issues during integration:
|
||||
|
||||
1. Check [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md) troubleshooting section
|
||||
2. Review [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md)
|
||||
3. Check browser console for errors
|
||||
4. Verify ts-ics is correctly serializing RRULE
|
||||
|
||||
---
|
||||
|
||||
## ✨ Summary
|
||||
|
||||
**✅ COMPLETE: Implementation**
|
||||
- RecurrenceEditor component
|
||||
- Styles & translations
|
||||
- Tests & documentation
|
||||
|
||||
**⏳ PENDING: Integration**
|
||||
- Add to Scheduler modal
|
||||
- Test end-to-end
|
||||
|
||||
**🚀 READY: To Use**
|
||||
- All patterns supported
|
||||
- All validations working
|
||||
- All documentation complete
|
||||
|
||||
**Total effort to complete:** ~30-45 minutes of integration work
|
||||
|
||||
---
|
||||
|
||||
**Let's integrate it! Start here:** [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md)
|
||||
@@ -1,364 +0,0 @@
|
||||
# 🔄 Recurring Events Implementation
|
||||
|
||||
**Complete implementation of recurring events for your CalDAV calendar application**
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
The RecurrenceEditor component is **ready to use** in your application!
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
|
||||
function MyForm() {
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule>();
|
||||
|
||||
return (
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
### ✅ Complete Implementation
|
||||
|
||||
1. **RecurrenceEditor Component**
|
||||
- Full UI for all recurrence types
|
||||
- Date validation
|
||||
- Multi-language support (EN/FR/NL)
|
||||
|
||||
2. **Styles (SCSS)**
|
||||
- BEM methodology
|
||||
- Responsive design
|
||||
- Integrated with your design system
|
||||
|
||||
3. **Tests**
|
||||
- 15+ test cases
|
||||
- Full coverage
|
||||
|
||||
4. **Documentation**
|
||||
- Implementation guide
|
||||
- Integration guide
|
||||
- Examples
|
||||
- Troubleshooting
|
||||
|
||||
## 📚 Documentation Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| **[README_RECURRENCE.md](./README_RECURRENCE.md)** | **👈 START HERE** - This file |
|
||||
| [RECURRENCE_SUMMARY.md](./RECURRENCE_SUMMARY.md) | Quick reference & overview |
|
||||
| [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md) | Complete technical docs |
|
||||
| [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md) | How to integrate in Scheduler |
|
||||
| [RECURRENCE_EXAMPLES.md](./RECURRENCE_EXAMPLES.md) | Real-world usage examples |
|
||||
|
||||
## 🎯 Supported Recurrence Patterns
|
||||
|
||||
| Type | Example | Status |
|
||||
|------|---------|--------|
|
||||
| **Daily** | Every day, every 3 days | ✅ Implemented |
|
||||
| **Weekly** | Monday & Friday, every 2 weeks | ✅ Implemented |
|
||||
| **Monthly** | 15th of each month | ✅ Implemented |
|
||||
| **Yearly** | March 15th every year | ✅ Implemented |
|
||||
| **End Conditions** | Never / Until date / After N times | ✅ Implemented |
|
||||
| **Date Validation** | Feb 29, month lengths | ✅ Implemented |
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
src/frontend/apps/calendars/src/features/calendar/components/
|
||||
├── RecurrenceEditor.tsx # Main component
|
||||
├── RecurrenceEditor.scss # Styles
|
||||
└── __tests__/
|
||||
└── RecurrenceEditor.test.tsx # Tests
|
||||
|
||||
src/frontend/apps/calendars/src/features/i18n/
|
||||
└── translations.json # Translations (EN/FR/NL)
|
||||
|
||||
Documentation (project root):
|
||||
├── README_RECURRENCE.md # This file
|
||||
├── RECURRENCE_SUMMARY.md # Quick reference
|
||||
├── RECURRENCE_IMPLEMENTATION.md # Technical docs
|
||||
├── SCHEDULER_RECURRENCE_INTEGRATION.md # Integration guide
|
||||
└── RECURRENCE_EXAMPLES.md # Usage examples
|
||||
```
|
||||
|
||||
## 🔧 Integration Steps
|
||||
|
||||
### Step 1: Use the Component
|
||||
|
||||
The component is already created! Just import and use it:
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
import type { IcsRecurrenceRule } from 'ts-ics';
|
||||
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>();
|
||||
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
```
|
||||
|
||||
### Step 2: Include in IcsEvent
|
||||
|
||||
```tsx
|
||||
const event: IcsEvent = {
|
||||
uid: crypto.randomUUID(),
|
||||
summary: "Team Meeting",
|
||||
start: { date: new Date() },
|
||||
end: { date: new Date() },
|
||||
recurrenceRule: recurrence, // ← Add this
|
||||
};
|
||||
```
|
||||
|
||||
### Step 3: That's It!
|
||||
|
||||
CalDAV handles everything else:
|
||||
- ✅ Stores RRULE in .ics file
|
||||
- ✅ Expands recurring instances
|
||||
- ✅ Syncs with other calendar apps
|
||||
|
||||
## 🎨 UI Preview
|
||||
|
||||
### Simple Mode
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ Repeat: [Daily ▼] │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
### Custom Mode - Weekly
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Repeat every [2] [weeks ▼] │
|
||||
│ │
|
||||
│ Repeat on: │
|
||||
│ [M] [T] [W] [T] [F] [S] [S] │
|
||||
│ ✓ ✓ │
|
||||
│ │
|
||||
│ Ends: │
|
||||
│ ○ Never │
|
||||
│ ○ On [2025-12-31] │
|
||||
│ ⦿ After [10] occurrences │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Custom Mode - Monthly with Warning
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Repeat every [1] [months ▼] │
|
||||
│ │
|
||||
│ Repeat on day: │
|
||||
│ Day [30] │
|
||||
│ │
|
||||
│ ⚠️ This month has at most 30 days │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🌍 Internationalization
|
||||
|
||||
Fully translated in:
|
||||
- 🇬🇧 English
|
||||
- 🇫🇷 French
|
||||
- 🇳🇱 Dutch
|
||||
|
||||
All UI strings, month names, and warning messages.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
npm test RecurrenceEditor
|
||||
```
|
||||
|
||||
Test coverage:
|
||||
- ✅ Component rendering
|
||||
- ✅ User interactions
|
||||
- ✅ All frequency types
|
||||
- ✅ Date validation
|
||||
- ✅ End conditions
|
||||
- ✅ Edge cases
|
||||
|
||||
## 📖 Common Use Cases
|
||||
|
||||
### 1. Daily Standup (Every Weekday)
|
||||
|
||||
```typescript
|
||||
{
|
||||
frequency: 'WEEKLY',
|
||||
byDay: [
|
||||
{ day: 'MO' },
|
||||
{ day: 'TU' },
|
||||
{ day: 'WE' },
|
||||
{ day: 'TH' },
|
||||
{ day: 'FR' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**RRULE:** `FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR`
|
||||
|
||||
### 2. Bi-Weekly Sprint Planning
|
||||
|
||||
```typescript
|
||||
{
|
||||
frequency: 'WEEKLY',
|
||||
interval: 2,
|
||||
byDay: [{ day: 'MO' }],
|
||||
count: 10
|
||||
}
|
||||
```
|
||||
|
||||
**RRULE:** `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;COUNT=10`
|
||||
|
||||
### 3. Monthly Team Meeting (15th)
|
||||
|
||||
```typescript
|
||||
{
|
||||
frequency: 'MONTHLY',
|
||||
byMonthDay: [15]
|
||||
}
|
||||
```
|
||||
|
||||
**RRULE:** `FREQ=MONTHLY;BYMONTHDAY=15`
|
||||
|
||||
### 4. Annual Birthday
|
||||
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
byMonth: [3],
|
||||
byMonthDay: [15]
|
||||
}
|
||||
```
|
||||
|
||||
**RRULE:** `FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15`
|
||||
|
||||
See [RECURRENCE_EXAMPLES.md](./RECURRENCE_EXAMPLES.md) for 10+ detailed examples!
|
||||
|
||||
## 🔍 Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ RecurrenceEditor Component (React) │
|
||||
│ ↓ │
|
||||
│ IcsRecurrenceRule (ts-ics) │
|
||||
│ ↓ │
|
||||
│ RRULE string (RFC 5545) │
|
||||
│ ↓ │
|
||||
│ .ics file (CalDAV) │
|
||||
│ ↓ │
|
||||
│ Sabre/dav Server (PHP) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**No backend changes needed!** Everything is handled by CalDAV standard.
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
### To Use in Your App
|
||||
|
||||
1. Read [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md)
|
||||
2. Follow the 5-step integration guide
|
||||
3. Test with your event modal
|
||||
|
||||
### To Learn More
|
||||
|
||||
1. Browse [RECURRENCE_EXAMPLES.md](./RECURRENCE_EXAMPLES.md) for real-world scenarios
|
||||
2. Check [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md) for deep dive
|
||||
3. Review [RECURRENCE_SUMMARY.md](./RECURRENCE_SUMMARY.md) for quick reference
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
### Q: Does this work with existing CalDAV events?
|
||||
|
||||
**A:** Yes! The component uses standard RRULE format compatible with all CalDAV clients (Apple Calendar, Google Calendar, Outlook, etc.).
|
||||
|
||||
### Q: Can users edit existing recurring events?
|
||||
|
||||
**A:** Yes! The component loads existing recurrence rules from events and allows editing the entire series.
|
||||
|
||||
### Q: What about editing single instances?
|
||||
|
||||
**A:** Not yet implemented in UI. CalDAV supports it via RECURRENCE-ID, but the UI for "Edit this occurrence" vs "Edit series" is a future enhancement.
|
||||
|
||||
### Q: Do recurring events sync with other calendar apps?
|
||||
|
||||
**A:** Yes! All patterns are standard RFC 5545 RRULE format.
|
||||
|
||||
### Q: Can I create "First Monday of month" patterns?
|
||||
|
||||
**A:** Not yet. That requires BYSETPOS which is a future enhancement.
|
||||
|
||||
### Q: What happens with February 30th?
|
||||
|
||||
**A:** The UI shows a warning, and CalDAV will skip occurrences on invalid dates.
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Events not appearing as recurring
|
||||
|
||||
1. Check browser console for errors
|
||||
2. Verify `recurrenceRule` is in IcsEvent object
|
||||
3. Check CalDAV server supports RRULE
|
||||
4. Inspect .ics file in network tab
|
||||
|
||||
### Translations not showing
|
||||
|
||||
1. Verify translations.json includes new keys
|
||||
2. Check i18n is initialized
|
||||
3. Reload page after adding translations
|
||||
|
||||
### Styles not applying
|
||||
|
||||
1. Ensure RecurrenceEditor.scss is imported in globals.scss
|
||||
2. Check for CSS conflicts
|
||||
3. Verify BEM class names
|
||||
|
||||
See [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md#troubleshooting) for more help.
|
||||
|
||||
## 📊 Feature Matrix
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Daily recurrence | ✅ | With interval |
|
||||
| Weekly recurrence | ✅ | Multiple days |
|
||||
| Monthly recurrence | ✅ | Day 1-31 |
|
||||
| Yearly recurrence | ✅ | Month + day |
|
||||
| Never ends | ✅ | |
|
||||
| Until date | ✅ | |
|
||||
| After N times | ✅ | |
|
||||
| Date validation | ✅ | Feb 29, month lengths |
|
||||
| Translations | ✅ | EN, FR, NL |
|
||||
| Tests | ✅ | 15+ cases |
|
||||
| nth occurrence | ❌ | Future (BYSETPOS) |
|
||||
| Edit single instance | ❌ | Future (RECURRENCE-ID UI) |
|
||||
|
||||
## 🎓 Resources
|
||||
|
||||
- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE Spec](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics Library](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [Sabre/dav Docs](https://sabre.io/dav/)
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
Implementation follows RFC 5545 (iCalendar) standard and integrates with:
|
||||
- ts-ics for ICS generation
|
||||
- tsdav for CalDAV client
|
||||
- @event-calendar/core for calendar UI
|
||||
- Sabre/dav for CalDAV server
|
||||
|
||||
## 📝 License
|
||||
|
||||
Part of the calendars application.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ready to Get Started?
|
||||
|
||||
1. **Quick integration:** Read [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md)
|
||||
2. **See examples:** Check [RECURRENCE_EXAMPLES.md](./RECURRENCE_EXAMPLES.md)
|
||||
3. **Deep dive:** Read [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md)
|
||||
|
||||
**The RecurrenceEditor is production-ready and waiting for you to integrate it!** 🎉
|
||||
@@ -1,523 +0,0 @@
|
||||
# Recurring Events - Usage Examples
|
||||
|
||||
## Real-World Scenarios
|
||||
|
||||
This document provides concrete examples of how to use the RecurrenceEditor for common recurring event patterns.
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 1: Daily Standup Meeting
|
||||
|
||||
**Requirement:** Team standup every weekday (Monday-Friday) at 9:00 AM
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'WEEKLY',
|
||||
interval: 1,
|
||||
byDay: [
|
||||
{ day: 'MO' },
|
||||
{ day: 'TU' },
|
||||
{ day: 'WE' },
|
||||
{ day: 'TH' },
|
||||
{ day: 'FR' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "weeks" frequency
|
||||
3. Click all weekday buttons: M T W T F
|
||||
4. Leave interval at 1
|
||||
5. Select "Never" for end condition
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 2: Bi-Weekly Sprint Planning
|
||||
|
||||
**Requirement:** Sprint planning every 2 weeks on Monday at 10:00 AM for 10 sprints
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'WEEKLY',
|
||||
interval: 2,
|
||||
byDay: [{ day: 'MO' }],
|
||||
count: 10
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Set interval to "2"
|
||||
3. Choose "weeks" frequency
|
||||
4. Click "M" (Monday)
|
||||
5. Select "After" and enter "10" occurrences
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;COUNT=10
|
||||
```
|
||||
|
||||
### Resulting Dates (starting Jan 6, 2025)
|
||||
- Jan 6, 2025
|
||||
- Jan 20, 2025
|
||||
- Feb 3, 2025
|
||||
- Feb 17, 2025
|
||||
- Mar 3, 2025
|
||||
- Mar 17, 2025
|
||||
- Mar 31, 2025
|
||||
- Apr 14, 2025
|
||||
- Apr 28, 2025
|
||||
- May 12, 2025 (last occurrence)
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 3: Monthly All-Hands Meeting
|
||||
|
||||
**Requirement:** First Monday of each month at 2:00 PM
|
||||
|
||||
⚠️ **Note:** "First Monday" pattern requires BYSETPOS (not yet implemented).
|
||||
**Workaround:** Use specific date if consistent, or create manually each month.
|
||||
|
||||
**Alternative - Specific Day of Month:**
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'MONTHLY',
|
||||
interval: 1,
|
||||
byMonthDay: [5] // 5th of every month
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "months" frequency
|
||||
3. Enter "5" for day of month
|
||||
4. Select "Never"
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=MONTHLY;BYMONTHDAY=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 4: Quarterly Business Review
|
||||
|
||||
**Requirement:** Last day of March, June, September, December at 3:00 PM
|
||||
|
||||
⚠️ **Current Implementation:** Set up as 4 separate yearly events.
|
||||
|
||||
**Future Implementation:** Would use BYMONTH with multiple months.
|
||||
|
||||
### Configuration (Workaround)
|
||||
|
||||
Create 4 separate yearly events:
|
||||
|
||||
**Q1 (March 31):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [3],
|
||||
byMonthDay: [31]
|
||||
}
|
||||
```
|
||||
|
||||
**Q2 (June 30):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [6],
|
||||
byMonthDay: [30]
|
||||
}
|
||||
```
|
||||
|
||||
**Q3 (September 30):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [9],
|
||||
byMonthDay: [30]
|
||||
}
|
||||
```
|
||||
|
||||
**Q4 (December 31):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [12],
|
||||
byMonthDay: [31]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 5: Birthday Reminder
|
||||
|
||||
**Requirement:** Annual reminder on March 15th
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [3],
|
||||
byMonthDay: [15]
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "years" frequency
|
||||
3. Select "March" from month dropdown
|
||||
4. Enter "15" for day
|
||||
5. Select "Never"
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 6: Payroll Processing
|
||||
|
||||
**Requirement:** 1st and 15th of every month
|
||||
|
||||
⚠️ **Current Implementation:** Create as 2 separate events:
|
||||
|
||||
**First event (1st):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'MONTHLY',
|
||||
interval: 1,
|
||||
byMonthDay: [1]
|
||||
}
|
||||
```
|
||||
|
||||
**Second event (15th):**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'MONTHLY',
|
||||
interval: 1,
|
||||
byMonthDay: [15]
|
||||
}
|
||||
```
|
||||
|
||||
### UI Steps (for each)
|
||||
1. Select "Custom..."
|
||||
2. Choose "months"
|
||||
3. Enter day (1 or 15)
|
||||
4. Select "Never"
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 7: Project Deadline (Fixed End Date)
|
||||
|
||||
**Requirement:** Daily check-ins until project ends on December 31, 2025
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'DAILY',
|
||||
interval: 1,
|
||||
until: {
|
||||
type: 'DATE',
|
||||
date: new Date('2025-12-31')
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "days" frequency
|
||||
3. Keep interval at 1
|
||||
4. Select "On"
|
||||
5. Choose date: 2025-12-31
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=DAILY;UNTIL=20251231T235959Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 8: Gym Schedule (Mon/Wed/Fri)
|
||||
|
||||
**Requirement:** Gym sessions 3 times per week
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'WEEKLY',
|
||||
interval: 1,
|
||||
byDay: [
|
||||
{ day: 'MO' },
|
||||
{ day: 'WE' },
|
||||
{ day: 'FR' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "weeks"
|
||||
3. Click M, W, F buttons
|
||||
4. Select "Never"
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 9: Leap Year Celebration
|
||||
|
||||
**Requirement:** February 29th celebration (only on leap years)
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const recurrence: IcsRecurrenceRule = {
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [2],
|
||||
byMonthDay: [29]
|
||||
};
|
||||
```
|
||||
|
||||
### UI Steps
|
||||
1. Select "Custom..."
|
||||
2. Choose "years"
|
||||
3. Select "February"
|
||||
4. Enter "29"
|
||||
5. ⚠️ Warning appears: "This date (Feb 29) only exists in leap years"
|
||||
6. Select "Never"
|
||||
|
||||
### Generated RRULE
|
||||
```
|
||||
RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29
|
||||
```
|
||||
|
||||
### Occurrences
|
||||
- Feb 29, 2024 ✅
|
||||
- Feb 29, 2028 ✅
|
||||
- Feb 29, 2032 ✅
|
||||
- (Skips 2025, 2026, 2027, 2029, 2030, 2031)
|
||||
|
||||
---
|
||||
|
||||
## 📅 Example 10: Seasonal Team Offsite
|
||||
|
||||
**Requirement:** First day of each season (March, June, September, December)
|
||||
|
||||
Create 4 separate yearly events or use the pattern:
|
||||
|
||||
### Configuration (One event, workaround)
|
||||
|
||||
**For March 1:**
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
byMonth: [3],
|
||||
byMonthDay: [1]
|
||||
}
|
||||
```
|
||||
|
||||
Repeat for months 6, 9, 12 as separate events.
|
||||
|
||||
**Better approach when BYMONTH allows multiple values:**
|
||||
```typescript
|
||||
// Future implementation
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
byMonth: [3, 6, 9, 12], // Not yet supported in UI
|
||||
byMonthDay: [1]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Complex Patterns Comparison
|
||||
|
||||
| Pattern | Status | Implementation |
|
||||
|---------|--------|----------------|
|
||||
| "Every day" | ✅ Supported | `FREQ=DAILY` |
|
||||
| "Every weekday" | ✅ Supported | `FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR` |
|
||||
| "Every Monday" | ✅ Supported | `FREQ=WEEKLY;BYDAY=MO` |
|
||||
| "1st of every month" | ✅ Supported | `FREQ=MONTHLY;BYMONTHDAY=1` |
|
||||
| "Last day of month" | ✅ Supported (with caveat) | `FREQ=MONTHLY;BYMONTHDAY=31` |
|
||||
| "1st Monday of month" | ❌ Future | Needs BYDAY + BYSETPOS |
|
||||
| "Last Friday of month" | ❌ Future | Needs BYDAY + BYSETPOS=-1 |
|
||||
| "Every 2 hours" | ❌ Not applicable | Events, not intraday recurrence |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Patterns
|
||||
|
||||
### Test Case 1: Edge Case - February 30th
|
||||
|
||||
```typescript
|
||||
// User selects:
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
byMonth: [2],
|
||||
byMonthDay: [30]
|
||||
}
|
||||
```
|
||||
|
||||
**Expected:** ⚠️ Warning: "February has at most 29 days"
|
||||
**Behavior:** Event will never occur (no year has Feb 30)
|
||||
|
||||
### Test Case 2: Month Overflow - April 31st
|
||||
|
||||
```typescript
|
||||
// User selects:
|
||||
{
|
||||
frequency: 'MONTHLY',
|
||||
byMonthDay: [31]
|
||||
}
|
||||
```
|
||||
|
||||
**Occurrences:**
|
||||
- January 31 ✅
|
||||
- February 31 ❌ (skipped)
|
||||
- March 31 ✅
|
||||
- April 31 ❌ (skipped - only 30 days)
|
||||
- May 31 ✅
|
||||
- June 31 ❌ (skipped - only 30 days)
|
||||
- July 31 ✅
|
||||
|
||||
**Warning shown for months with 30 days when setting up yearly recurrence**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Reference
|
||||
|
||||
### Frequency Types
|
||||
|
||||
```typescript
|
||||
frequency: 'DAILY' // Every day
|
||||
frequency: 'WEEKLY' // Every week (specify days)
|
||||
frequency: 'MONTHLY' // Every month (specify day 1-31)
|
||||
frequency: 'YEARLY' // Every year (specify month + day)
|
||||
```
|
||||
|
||||
### Intervals
|
||||
|
||||
```typescript
|
||||
interval: 1 // Every [frequency]
|
||||
interval: 2 // Every 2 [frequency]
|
||||
interval: 3 // Every 3 [frequency]
|
||||
// etc.
|
||||
```
|
||||
|
||||
### Days of Week (WEEKLY)
|
||||
|
||||
```typescript
|
||||
byDay: [
|
||||
{ day: 'MO' }, // Monday
|
||||
{ day: 'TU' }, // Tuesday
|
||||
{ day: 'WE' }, // Wednesday
|
||||
{ day: 'TH' }, // Thursday
|
||||
{ day: 'FR' }, // Friday
|
||||
{ day: 'SA' }, // Saturday
|
||||
{ day: 'SU' } // Sunday
|
||||
]
|
||||
```
|
||||
|
||||
### Day of Month (MONTHLY, YEARLY)
|
||||
|
||||
```typescript
|
||||
byMonthDay: [15] // 15th of month
|
||||
byMonthDay: [1] // 1st of month
|
||||
byMonthDay: [31] // 31st of month (with caveats)
|
||||
```
|
||||
|
||||
### Month (YEARLY)
|
||||
|
||||
```typescript
|
||||
byMonth: [1] // January
|
||||
byMonth: [2] // February
|
||||
// ...
|
||||
byMonth: [12] // December
|
||||
```
|
||||
|
||||
### End Conditions
|
||||
|
||||
```typescript
|
||||
// Never ends
|
||||
(no count or until)
|
||||
|
||||
// Ends on date
|
||||
until: {
|
||||
type: 'DATE',
|
||||
date: new Date('2025-12-31')
|
||||
}
|
||||
|
||||
// Ends after N occurrences
|
||||
count: 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips & Best Practices
|
||||
|
||||
### 1. Use Simple Mode for Common Patterns
|
||||
|
||||
Simple mode is sufficient for:
|
||||
- Daily recurrence (every day)
|
||||
- Weekly recurrence (every week, same days)
|
||||
- Monthly recurrence (same date each month)
|
||||
- Yearly recurrence (same date each year)
|
||||
|
||||
### 2. Use Custom Mode for Advanced Patterns
|
||||
|
||||
Custom mode is needed for:
|
||||
- Intervals > 1 (every 2 weeks, every 3 months)
|
||||
- Multiple days per week
|
||||
- End dates or occurrence counts
|
||||
- Specific validation
|
||||
|
||||
### 3. Date Validation
|
||||
|
||||
Always check for warnings when selecting:
|
||||
- February dates (29, 30, 31)
|
||||
- Month-end dates for monthly recurrence
|
||||
- Day 31 for months with 30 days
|
||||
|
||||
### 4. CalDAV Compatibility
|
||||
|
||||
All patterns generated by RecurrenceEditor are standard RRULE format compatible with:
|
||||
- Apple Calendar
|
||||
- Google Calendar
|
||||
- Microsoft Outlook
|
||||
- Mozilla Thunderbird
|
||||
- Any RFC 5545 compliant calendar
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- [RECURRENCE_IMPLEMENTATION.md](./RECURRENCE_IMPLEMENTATION.md) - Technical implementation
|
||||
- [SCHEDULER_RECURRENCE_INTEGRATION.md](./SCHEDULER_RECURRENCE_INTEGRATION.md) - Integration guide
|
||||
- [RECURRENCE_SUMMARY.md](./RECURRENCE_SUMMARY.md) - Quick reference
|
||||
@@ -1,406 +0,0 @@
|
||||
# Recurring Events Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the complete implementation of recurring events in the CalDAV calendar application. The implementation follows the iCalendar RFC 5545 standard for RRULE (Recurrence Rule).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (Next.js) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ RecurrenceEditor Component │
|
||||
│ ├─ UI for frequency selection (DAILY/WEEKLY/MONTHLY/YEARLY) │
|
||||
│ ├─ Interval input │
|
||||
│ ├─ Day/Month/Date selection │
|
||||
│ └─ End conditions (never/until/count) │
|
||||
│ │
|
||||
│ EventCalendarAdapter │
|
||||
│ ├─ Converts IcsRecurrenceRule to RRULE string │
|
||||
│ └─ Parses RRULE to IcsRecurrenceRule │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ts-ics Library │
|
||||
│ IcsRecurrenceRule interface │
|
||||
│ ├─ frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY' │
|
||||
│ ├─ interval?: number │
|
||||
│ ├─ byDay?: IcsWeekDay[] │
|
||||
│ ├─ byMonthDay?: number[] │
|
||||
│ ├─ byMonth?: number[] │
|
||||
│ ├─ count?: number │
|
||||
│ └─ until?: IcsDateObject │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ CalDAV Protocol │
|
||||
│ RRULE property in VEVENT │
|
||||
│ Example: RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Sabre/dav Server (PHP) │
|
||||
│ Stores and serves iCalendar (.ics) files │
|
||||
│ Handles recurring event expansion │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
### RecurrenceEditor Component
|
||||
|
||||
Location: `src/features/calendar/components/RecurrenceEditor.tsx`
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface RecurrenceEditorProps {
|
||||
value?: IcsRecurrenceRule;
|
||||
onChange: (rule: IcsRecurrenceRule | undefined) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- ✅ Simple mode: Quick selection (None, Daily, Weekly, Monthly, Yearly)
|
||||
- ✅ Custom mode: Full control over all recurrence parameters
|
||||
- ✅ DAILY: Interval support (every X days)
|
||||
- ✅ WEEKLY: Interval + day selection (MO, TU, WE, TH, FR, SA, SU)
|
||||
- ✅ MONTHLY: Day of month (1-31) with validation
|
||||
- ✅ YEARLY: Month + day selection with leap year support
|
||||
- ✅ End conditions: Never / Until date / After N occurrences
|
||||
- ✅ Date validation warnings (Feb 30th, Feb 29th leap year, etc.)
|
||||
|
||||
### Example Usage
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
import { useState } from 'react';
|
||||
import type { IcsRecurrenceRule } from 'ts-ics';
|
||||
|
||||
function EventForm() {
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>();
|
||||
|
||||
return (
|
||||
<form>
|
||||
{/* Other event fields */}
|
||||
|
||||
<RecurrenceEditor
|
||||
value={recurrence}
|
||||
onChange={setRecurrence}
|
||||
/>
|
||||
|
||||
{/* Save button */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Scheduler
|
||||
|
||||
To integrate the RecurrenceEditor into the existing Scheduler modal, add the following:
|
||||
|
||||
### 1. Add recurrence state
|
||||
|
||||
```typescript
|
||||
// In EventModal component
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>(
|
||||
event?.recurrenceRule
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Add RecurrenceEditor to the form
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '../RecurrenceEditor';
|
||||
|
||||
// In the modal JSX, after location/description fields
|
||||
<RecurrenceEditor
|
||||
value={recurrence}
|
||||
onChange={setRecurrence}
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. Include recurrence in event save
|
||||
|
||||
```typescript
|
||||
const icsEvent: IcsEvent = {
|
||||
// ... existing fields
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Reset recurrence when modal opens
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
// ... existing resets
|
||||
setRecurrence(event?.recurrenceRule);
|
||||
}, [event]);
|
||||
```
|
||||
|
||||
## RRULE Examples
|
||||
|
||||
### Daily Recurrence
|
||||
|
||||
**Every day:**
|
||||
```
|
||||
RRULE:FREQ=DAILY;INTERVAL=1
|
||||
```
|
||||
|
||||
**Every 3 days:**
|
||||
```
|
||||
RRULE:FREQ=DAILY;INTERVAL=3
|
||||
```
|
||||
|
||||
**Daily for 10 occurrences:**
|
||||
```
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
```
|
||||
|
||||
**Daily until Dec 31, 2025:**
|
||||
```
|
||||
RRULE:FREQ=DAILY;UNTIL=20251231T235959Z
|
||||
```
|
||||
|
||||
### Weekly Recurrence
|
||||
|
||||
**Every week on Monday:**
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO
|
||||
```
|
||||
|
||||
**Every 2 weeks on Monday and Friday:**
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR
|
||||
```
|
||||
|
||||
**Weekly on weekdays (Mon-Fri):**
|
||||
```
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
|
||||
```
|
||||
|
||||
### Monthly Recurrence
|
||||
|
||||
**Every month on the 15th:**
|
||||
```
|
||||
RRULE:FREQ=MONTHLY;BYMONTHDAY=15
|
||||
```
|
||||
|
||||
**Every 3 months on the 1st:**
|
||||
```
|
||||
RRULE:FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=1
|
||||
```
|
||||
|
||||
**Monthly on the last day (31st with fallback):**
|
||||
```
|
||||
RRULE:FREQ=MONTHLY;BYMONTHDAY=31
|
||||
```
|
||||
Note: For months with fewer than 31 days, most implementations skip that occurrence.
|
||||
|
||||
### Yearly Recurrence
|
||||
|
||||
**Every year on March 15th:**
|
||||
```
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15
|
||||
```
|
||||
|
||||
**Every year on February 29th (leap years only):**
|
||||
```
|
||||
RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29
|
||||
```
|
||||
|
||||
**Every 2 years on December 25th:**
|
||||
```
|
||||
RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=12;BYMONTHDAY=25
|
||||
```
|
||||
|
||||
## Date Validation
|
||||
|
||||
The RecurrenceEditor includes smart validation for invalid dates:
|
||||
|
||||
### February 30th/31st
|
||||
**Warning:** "February has at most 29 days"
|
||||
|
||||
### February 29th
|
||||
**Warning:** "This date (Feb 29) only exists in leap years"
|
||||
|
||||
### April 31st, June 31st, etc.
|
||||
**Warning:** "This month has at most 30 days"
|
||||
|
||||
### Day > 31
|
||||
**Warning:** "Day must be between 1 and 31"
|
||||
|
||||
## IcsRecurrenceRule Interface (ts-ics)
|
||||
|
||||
```typescript
|
||||
interface IcsRecurrenceRule {
|
||||
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
interval?: number; // Default: 1
|
||||
count?: number; // Number of occurrences
|
||||
until?: IcsDateObject; // End date
|
||||
byDay?: IcsWeekDay[]; // Days of week (WEEKLY)
|
||||
byMonthDay?: number[]; // Days of month (MONTHLY, YEARLY)
|
||||
byMonth?: number[]; // Months (YEARLY)
|
||||
bySetPos?: number[]; // Position (e.g., 1st Monday)
|
||||
weekStart?: IcsWeekDay; // Week start day
|
||||
}
|
||||
|
||||
type IcsWeekDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
||||
```
|
||||
|
||||
## Backend Considerations
|
||||
|
||||
The Django backend **does not need modifications** for recurring events. CalDAV handles recurrence natively:
|
||||
|
||||
1. **Storage:** RRULE is stored as a property in the VEVENT within the .ics file
|
||||
2. **Expansion:** Sabre/dav handles recurring event expansion when clients query date ranges
|
||||
3. **Modifications:** Individual instances can be modified by creating exception events with RECURRENCE-ID
|
||||
|
||||
### Example .ics file with recurrence
|
||||
|
||||
```ics
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//CalDavService//NONSGML v1.0//EN
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123-def-456
|
||||
DTSTART:20260125T140000Z
|
||||
DTEND:20260125T150000Z
|
||||
SUMMARY:Weekly Team Meeting
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20261231T235959Z
|
||||
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||
ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob@example.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [ ] Daily recurrence with interval 1, 3, 7
|
||||
- [ ] Weekly recurrence with single day (Monday)
|
||||
- [ ] Weekly recurrence with multiple days (Mon, Wed, Fri)
|
||||
- [ ] Weekly recurrence with interval 2
|
||||
- [ ] Monthly recurrence on day 1, 15, 31
|
||||
- [ ] Monthly recurrence with February validation
|
||||
- [ ] Yearly recurrence on Jan 1, Dec 25
|
||||
- [ ] Yearly recurrence on Feb 29 with leap year warning
|
||||
- [ ] Never-ending recurrence
|
||||
- [ ] Until date recurrence
|
||||
- [ ] Count-based recurrence (10 occurrences)
|
||||
- [ ] Edit recurring event
|
||||
- [ ] Delete recurring event
|
||||
|
||||
### Test Cases
|
||||
|
||||
```typescript
|
||||
// Test: Weekly on Monday and Friday
|
||||
const rule: IcsRecurrenceRule = {
|
||||
frequency: 'WEEKLY',
|
||||
interval: 1,
|
||||
byDay: [{ day: 'MO' }, { day: 'FR' }],
|
||||
};
|
||||
// Expected RRULE: FREQ=WEEKLY;BYDAY=MO,FR
|
||||
|
||||
// Test: Monthly on 31st (handles months with fewer days)
|
||||
const rule: IcsRecurrenceRule = {
|
||||
frequency: 'MONTHLY',
|
||||
interval: 1,
|
||||
byMonthDay: [31],
|
||||
};
|
||||
// Expected RRULE: FREQ=MONTHLY;BYMONTHDAY=31
|
||||
|
||||
// Test: Yearly on Feb 29
|
||||
const rule: IcsRecurrenceRule = {
|
||||
frequency: 'YEARLY',
|
||||
interval: 1,
|
||||
byMonth: [2],
|
||||
byMonthDay: [29],
|
||||
count: 10,
|
||||
};
|
||||
// Expected RRULE: FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29;COUNT=10
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
All UI strings are internationalized (i18n) with support for:
|
||||
- 🇬🇧 English
|
||||
- 🇫🇷 French
|
||||
- 🇳🇱 Dutch
|
||||
|
||||
Translation keys are defined in `src/features/i18n/translations.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"calendar": {
|
||||
"recurrence": {
|
||||
"label": "Repeat",
|
||||
"daily": "Daily",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"yearly": "Yearly",
|
||||
"repeatOnDay": "Repeat on day",
|
||||
"repeatOnDate": "Repeat on date",
|
||||
"dayOfMonth": "Day",
|
||||
"months": {
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
...
|
||||
},
|
||||
"warnings": {
|
||||
"februaryMax": "February has at most 29 days",
|
||||
"leapYear": "This date (Feb 29) only exists in leap years",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Styles are in `RecurrenceEditor.scss` using BEM methodology:
|
||||
|
||||
```scss
|
||||
.recurrence-editor {
|
||||
&__label { ... }
|
||||
&__weekday-button { ... }
|
||||
&__weekday-button--selected { ... }
|
||||
&__warning { ... }
|
||||
}
|
||||
|
||||
.recurrence-editor-layout {
|
||||
&--row { ... }
|
||||
&--gap-1rem { ... }
|
||||
&--flex-wrap { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **No BYDAY with position** (e.g., "2nd Tuesday of month")
|
||||
- Future enhancement
|
||||
- Requires UI for "1st/2nd/3rd/4th/last" + weekday selection
|
||||
|
||||
2. **No BYSETPOS** (complex patterns)
|
||||
- e.g., "Last Friday of every month"
|
||||
- Requires advanced UI
|
||||
|
||||
3. **Time zone handling**
|
||||
- UNTIL dates are converted to UTC
|
||||
- Local time events use floating time
|
||||
|
||||
4. **Recurring event modifications**
|
||||
- Editing single instance creates exception (RECURRENCE-ID)
|
||||
- Not yet implemented in UI (future work)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Visual calendar preview of recurrence pattern
|
||||
- [ ] Natural language summary ("Every 2 weeks on Monday and Friday")
|
||||
- [ ] Support for BYSETPOS (nth occurrence patterns)
|
||||
- [ ] Exception handling UI for editing single instances
|
||||
- [ ] Recurring event series deletion options (this only / this and future / all)
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE Specification](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics Documentation](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [Sabre/dav Documentation](https://sabre.io/dav/)
|
||||
@@ -1,369 +0,0 @@
|
||||
# Recurring Events Implementation - Summary
|
||||
|
||||
## 🎯 What Was Implemented
|
||||
|
||||
A complete recurring events system following the iCalendar RFC 5545 standard (RRULE).
|
||||
|
||||
### ✅ Features Completed
|
||||
|
||||
1. **RecurrenceEditor Component** (`RecurrenceEditor.tsx`)
|
||||
- ✅ DAILY recurrence with interval support
|
||||
- ✅ WEEKLY recurrence with day selection (Mon-Sun)
|
||||
- ✅ MONTHLY recurrence with day of month (1-31)
|
||||
- ✅ YEARLY recurrence with month + day selection
|
||||
- ✅ End conditions: Never / Until date / After N occurrences
|
||||
- ✅ Smart date validation (Feb 29th, month lengths)
|
||||
- ✅ Visual warnings for invalid dates
|
||||
- ✅ Simple and Custom modes
|
||||
|
||||
2. **Styles** (`RecurrenceEditor.scss`)
|
||||
- ✅ BEM methodology
|
||||
- ✅ Responsive layout
|
||||
- ✅ Weekday button selection
|
||||
- ✅ Warning messages styling
|
||||
- ✅ Integrated with existing design system
|
||||
|
||||
3. **Translations** (`translations.json`)
|
||||
- ✅ English (en)
|
||||
- ✅ French (fr)
|
||||
- ✅ Dutch (nl)
|
||||
- ✅ All UI strings
|
||||
- ✅ Month names
|
||||
- ✅ Validation warnings
|
||||
|
||||
4. **Tests** (`RecurrenceEditor.test.tsx`)
|
||||
- ✅ 15+ test cases
|
||||
- ✅ All recurrence types
|
||||
- ✅ Date validation
|
||||
- ✅ End conditions
|
||||
- ✅ User interactions
|
||||
|
||||
5. **Documentation**
|
||||
- ✅ Complete implementation guide
|
||||
- ✅ Scheduler integration guide
|
||||
- ✅ RRULE examples
|
||||
- ✅ Testing checklist
|
||||
- ✅ Troubleshooting guide
|
||||
|
||||
## 📁 Files Created/Modified
|
||||
|
||||
### New Files
|
||||
```
|
||||
src/frontend/apps/calendars/src/features/calendar/components/
|
||||
├── RecurrenceEditor.tsx ✅ Complete component
|
||||
├── RecurrenceEditor.scss ✅ Styles
|
||||
└── __tests__/
|
||||
└── RecurrenceEditor.test.tsx ✅ Test suite
|
||||
|
||||
Documentation:
|
||||
├── RECURRENCE_IMPLEMENTATION.md ✅ Full implementation guide
|
||||
├── SCHEDULER_RECURRENCE_INTEGRATION.md ✅ Integration guide
|
||||
└── RECURRENCE_SUMMARY.md ✅ This file
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
```
|
||||
src/frontend/apps/calendars/src/features/i18n/
|
||||
└── translations.json ✅ Added recurrence translations (EN/FR/NL)
|
||||
|
||||
src/frontend/apps/calendars/src/styles/
|
||||
└── globals.scss ✅ RecurrenceEditor.scss already imported
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Use RecurrenceEditor in a Form
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
import { useState } from 'react';
|
||||
import type { IcsRecurrenceRule } from 'ts-ics';
|
||||
|
||||
function MyEventForm() {
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule>();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input name="title" />
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Include in IcsEvent
|
||||
|
||||
```typescript
|
||||
const event: IcsEvent = {
|
||||
uid: crypto.randomUUID(),
|
||||
summary: "Team Meeting",
|
||||
start: { date: new Date() },
|
||||
end: { date: new Date() },
|
||||
recurrenceRule: recurrence, // From RecurrenceEditor
|
||||
};
|
||||
```
|
||||
|
||||
### 3. CalDAV Automatically Handles It
|
||||
|
||||
No backend changes needed! The RRULE is stored in the .ics file:
|
||||
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123
|
||||
SUMMARY:Team Meeting
|
||||
DTSTART:20260125T140000Z
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=20
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
## 📊 Supported Patterns
|
||||
|
||||
| Pattern | Example | RRULE |
|
||||
|---------|---------|-------|
|
||||
| **Daily** | Every day | `FREQ=DAILY` |
|
||||
| | Every 3 days | `FREQ=DAILY;INTERVAL=3` |
|
||||
| **Weekly** | Every Monday | `FREQ=WEEKLY;BYDAY=MO` |
|
||||
| | Mon, Wed, Fri | `FREQ=WEEKLY;BYDAY=MO,WE,FR` |
|
||||
| | Every 2 weeks on Thu | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TH` |
|
||||
| **Monthly** | 15th of each month | `FREQ=MONTHLY;BYMONTHDAY=15` |
|
||||
| | Last day (31st) | `FREQ=MONTHLY;BYMONTHDAY=31` |
|
||||
| **Yearly** | March 15th | `FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15` |
|
||||
| | Feb 29 (leap years) | `FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29` |
|
||||
| **End** | Never | (no UNTIL or COUNT) |
|
||||
| | Until Dec 31, 2025 | `UNTIL=20251231T235959Z` |
|
||||
| | 10 times | `COUNT=10` |
|
||||
|
||||
## 🔧 Integration with Scheduler
|
||||
|
||||
To integrate into your EventModal in Scheduler.tsx, follow these 5 steps:
|
||||
|
||||
1. **Import:** `import { RecurrenceEditor } from '../RecurrenceEditor';`
|
||||
2. **State:** `const [recurrence, setRecurrence] = useState<IcsRecurrenceRule>();`
|
||||
3. **UI:** Add button + `<RecurrenceEditor value={recurrence} onChange={setRecurrence} />`
|
||||
4. **Save:** Include `recurrenceRule: recurrence` in IcsEvent
|
||||
5. **Reset:** Add recurrence reset in useEffect
|
||||
|
||||
See `SCHEDULER_RECURRENCE_INTEGRATION.md` for complete code.
|
||||
|
||||
## 🎨 UI Features
|
||||
|
||||
### Simple Mode
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Repeat: [Dropdown: Daily ▼] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
Dropdown options:
|
||||
- No
|
||||
- Daily
|
||||
- Weekly
|
||||
- Monthly
|
||||
- Yearly
|
||||
- Custom...
|
||||
|
||||
### Custom Mode - Weekly Example
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Repeat every [2] [weeks ▼] │
|
||||
│ │
|
||||
│ Repeat on: │
|
||||
│ [M] [T] [W] [T] [F] [S] [S] ← Toggle buttons │
|
||||
│ ✓ ✓ ← Selected │
|
||||
│ │
|
||||
│ Ends: │
|
||||
│ ○ Never │
|
||||
│ ○ On [2025-12-31] │
|
||||
│ ⦿ After [10] occurrences │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Validation Warnings
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Repeat every [1] [years ▼] │
|
||||
│ │
|
||||
│ Repeat on date: │
|
||||
│ [February ▼] [30] │
|
||||
│ │
|
||||
│ ⚠️ February has at most 29 days │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
npm test RecurrenceEditor
|
||||
```
|
||||
|
||||
Manual testing checklist:
|
||||
- [ ] Daily with intervals 1, 3, 7
|
||||
- [ ] Weekly single day (Monday)
|
||||
- [ ] Weekly multiple days (Mon, Wed, Fri)
|
||||
- [ ] Weekly with interval 2
|
||||
- [ ] Monthly on 1st, 15th, 31st
|
||||
- [ ] Monthly February validation
|
||||
- [ ] Yearly Jan 1, Dec 25
|
||||
- [ ] Yearly Feb 29 leap year warning
|
||||
- [ ] Never-ending
|
||||
- [ ] Until date
|
||||
- [ ] Count-based (10 occurrences)
|
||||
|
||||
## 📚 Documentation Files
|
||||
|
||||
1. **RECURRENCE_IMPLEMENTATION.md**
|
||||
- Complete technical documentation
|
||||
- Architecture overview
|
||||
- Component structure
|
||||
- RRULE examples
|
||||
- Backend considerations
|
||||
- Testing guide
|
||||
|
||||
2. **SCHEDULER_RECURRENCE_INTEGRATION.md**
|
||||
- Step-by-step integration guide
|
||||
- Code snippets for each step
|
||||
- Complete example
|
||||
- Troubleshooting
|
||||
|
||||
3. **RECURRENCE_SUMMARY.md** (this file)
|
||||
- Quick reference
|
||||
- Files overview
|
||||
- Quick start guide
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
### Not Yet Implemented (Optional)
|
||||
|
||||
1. **Advanced Patterns**
|
||||
- BYSETPOS (e.g., "2nd Tuesday of month")
|
||||
- Position-based recurrence ("Last Friday")
|
||||
|
||||
2. **UI Enhancements**
|
||||
- Visual calendar preview
|
||||
- Natural language summary ("Every 2 weeks on Monday")
|
||||
- Recurrence icon in calendar
|
||||
|
||||
3. **Editing Features**
|
||||
- Edit single instance vs series
|
||||
- Delete this/future/all options
|
||||
- Exception handling UI
|
||||
|
||||
4. **Time Zone**
|
||||
- Better time zone handling for UNTIL
|
||||
- Time zone selector for events
|
||||
|
||||
## ✅ What Works Now
|
||||
|
||||
- ✅ Create recurring events
|
||||
- ✅ Edit recurring events (entire series)
|
||||
- ✅ Delete recurring events
|
||||
- ✅ View recurring event instances in calendar
|
||||
- ✅ CalDAV sync with other clients (Outlook, Apple Calendar, etc.)
|
||||
- ✅ Email invitations for recurring events
|
||||
- ✅ Attendees on recurring events
|
||||
- ✅ All recurrence patterns (DAILY/WEEKLY/MONTHLY/YEARLY)
|
||||
- ✅ All end conditions (never/until/count)
|
||||
- ✅ Date validation
|
||||
|
||||
## 🐛 Known Limitations
|
||||
|
||||
1. **Single Instance Editing**
|
||||
- Editing modifies entire series
|
||||
- No UI for "Edit this occurrence only"
|
||||
- (CalDAV supports via RECURRENCE-ID, but UI not implemented)
|
||||
|
||||
2. **Advanced Patterns**
|
||||
- No "nth occurrence" (e.g., "2nd Tuesday")
|
||||
- No "last occurrence" (e.g., "last Friday")
|
||||
|
||||
3. **Visual Feedback**
|
||||
- No recurring event icon in calendar view
|
||||
- No summary text showing recurrence pattern
|
||||
|
||||
## 💡 Usage Tips
|
||||
|
||||
### Leap Year Events (Feb 29)
|
||||
|
||||
When creating yearly event on Feb 29:
|
||||
```typescript
|
||||
{
|
||||
frequency: 'YEARLY',
|
||||
byMonth: [2],
|
||||
byMonthDay: [29]
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ UI shows: "This date (Feb 29) only exists in leap years"
|
||||
|
||||
Event will only occur in:
|
||||
- 2024 ✅
|
||||
- 2025 ❌
|
||||
- 2026 ❌
|
||||
- 2027 ❌
|
||||
- 2028 ✅
|
||||
|
||||
### Month-End Events (31st)
|
||||
|
||||
When creating monthly event on 31st:
|
||||
```typescript
|
||||
{
|
||||
frequency: 'MONTHLY',
|
||||
byMonthDay: [31]
|
||||
}
|
||||
```
|
||||
|
||||
Event occurs on:
|
||||
- January 31 ✅
|
||||
- February 31 ❌ (skipped)
|
||||
- March 31 ✅
|
||||
- April 31 ❌ (skipped, only 30 days)
|
||||
- May 31 ✅
|
||||
|
||||
### Weekday Selection
|
||||
|
||||
For "every weekday" (Mon-Fri):
|
||||
```typescript
|
||||
{
|
||||
frequency: 'WEEKLY',
|
||||
byDay: [
|
||||
{ day: 'MO' },
|
||||
{ day: 'TU' },
|
||||
{ day: 'WE' },
|
||||
{ day: 'TH' },
|
||||
{ day: 'FR' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🎓 Learning Resources
|
||||
|
||||
- [RFC 5545 - iCalendar Specification](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE Documentation](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics Library](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [Sabre/dav](https://sabre.io/dav/)
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check `RECURRENCE_IMPLEMENTATION.md` for detailed docs
|
||||
2. Check `SCHEDULER_RECURRENCE_INTEGRATION.md` for integration help
|
||||
3. Run tests: `npm test RecurrenceEditor`
|
||||
4. Check browser console for errors
|
||||
5. Inspect network tab for CalDAV requests
|
||||
|
||||
## 🎉 Summary
|
||||
|
||||
You now have a **complete, production-ready** recurring events system that:
|
||||
|
||||
- ✅ Supports all common recurrence patterns
|
||||
- ✅ Validates user input with helpful warnings
|
||||
- ✅ Integrates seamlessly with CalDAV
|
||||
- ✅ Works with ts-ics and @event-calendar
|
||||
- ✅ Is fully translated (EN/FR/NL)
|
||||
- ✅ Is well-tested and documented
|
||||
- ✅ Follows RFC 5545 standard
|
||||
|
||||
**Next step:** Integrate into Scheduler using `SCHEDULER_RECURRENCE_INTEGRATION.md`! 🚀
|
||||
@@ -1,368 +0,0 @@
|
||||
# Scheduler Recurrence Integration Guide
|
||||
|
||||
## How to Add Recurrence Support to EventModal in Scheduler.tsx
|
||||
|
||||
This guide shows exactly how to integrate the RecurrenceEditor component into the existing Scheduler event modal.
|
||||
|
||||
## Step 1: Import RecurrenceEditor
|
||||
|
||||
Add to imports at the top of `Scheduler.tsx`:
|
||||
|
||||
```typescript
|
||||
import { RecurrenceEditor } from "../RecurrenceEditor";
|
||||
import type { IcsRecurrenceRule } from "ts-ics";
|
||||
```
|
||||
|
||||
## Step 2: Add Recurrence State
|
||||
|
||||
In the `EventModal` component, add recurrence state after the existing useState declarations:
|
||||
|
||||
```typescript
|
||||
// Around line 110, after:
|
||||
const [attendees, setAttendees] = useState<IcsAttendee[]>([]);
|
||||
const [showAttendees, setShowAttendees] = useState(false);
|
||||
|
||||
// Add:
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>(
|
||||
event?.recurrenceRule
|
||||
);
|
||||
const [showRecurrence, setShowRecurrence] = useState(() => {
|
||||
return !!event?.recurrenceRule;
|
||||
});
|
||||
```
|
||||
|
||||
## Step 3: Reset Recurrence When Event Changes
|
||||
|
||||
In the `useEffect` that resets form state, add recurrence reset:
|
||||
|
||||
```typescript
|
||||
// Around line 121-161, in the useEffect(() => { ... }, [event, calendarUrl])
|
||||
useEffect(() => {
|
||||
setTitle(event?.summary || "");
|
||||
setDescription(event?.description || "");
|
||||
setLocation(event?.location || "");
|
||||
setSelectedCalendarUrl(calendarUrl);
|
||||
|
||||
// Initialize attendees from event
|
||||
if (event?.attendees && event.attendees.length > 0) {
|
||||
setAttendees(event.attendees);
|
||||
setShowAttendees(true);
|
||||
} else {
|
||||
setAttendees([]);
|
||||
setShowAttendees(false);
|
||||
}
|
||||
|
||||
// ADD THIS: Initialize recurrence from event
|
||||
if (event?.recurrenceRule) {
|
||||
setRecurrence(event.recurrenceRule);
|
||||
setShowRecurrence(true);
|
||||
} else {
|
||||
setRecurrence(undefined);
|
||||
setShowRecurrence(false);
|
||||
}
|
||||
|
||||
// ... rest of the useEffect
|
||||
}, [event, calendarUrl]);
|
||||
```
|
||||
|
||||
## Step 4: Include Recurrence in Save
|
||||
|
||||
In the `handleSave` function, add recurrence to the IcsEvent object:
|
||||
|
||||
```typescript
|
||||
// Around line 200-227, when creating the icsEvent
|
||||
const icsEvent: IcsEvent = {
|
||||
...eventWithoutDuration,
|
||||
uid: event?.uid || crypto.randomUUID(),
|
||||
summary: title,
|
||||
description: description || undefined,
|
||||
location: location || undefined,
|
||||
start: {
|
||||
date: fakeUtcStart,
|
||||
local: {
|
||||
timezone: BROWSER_TIMEZONE,
|
||||
tzoffset: adapter.getTimezoneOffset(startDate, BROWSER_TIMEZONE),
|
||||
},
|
||||
},
|
||||
end: {
|
||||
date: fakeUtcEnd,
|
||||
local: {
|
||||
timezone: BROWSER_TIMEZONE,
|
||||
tzoffset: adapter.getTimezoneOffset(endDate, BROWSER_TIMEZONE),
|
||||
},
|
||||
},
|
||||
organizer: organizer,
|
||||
attendees: attendees.length > 0 ? attendees : undefined,
|
||||
recurrenceRule: recurrence, // ADD THIS LINE
|
||||
};
|
||||
```
|
||||
|
||||
## Step 5: Add RecurrenceEditor to UI
|
||||
|
||||
In the modal JSX, add a button to show/hide recurrence and the RecurrenceEditor component.
|
||||
|
||||
### Add Feature Button (like the attendees button)
|
||||
|
||||
Around line 350-360, after the attendees button:
|
||||
|
||||
```tsx
|
||||
{/* Existing code */}
|
||||
<button
|
||||
type="button"
|
||||
className={`event-modal__feature-tag ${showAttendees ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => setShowAttendees(!showAttendees)}
|
||||
>
|
||||
<span className="material-icons">group</span>
|
||||
{t('calendar.event.attendees')}
|
||||
</button>
|
||||
|
||||
{/* ADD THIS: Recurrence button */}
|
||||
<button
|
||||
type="button"
|
||||
className={`event-modal__feature-tag ${showRecurrence ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => setShowRecurrence(!showRecurrence)}
|
||||
>
|
||||
<span className="material-icons">repeat</span>
|
||||
{t('calendar.recurrence.label')}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Add RecurrenceEditor Component
|
||||
|
||||
Around line 370, after the AttendeesInput:
|
||||
|
||||
```tsx
|
||||
{/* Existing attendees input */}
|
||||
{showAttendees && (
|
||||
<div className="event-modal__attendees-input">
|
||||
<AttendeesInput
|
||||
attendees={attendees}
|
||||
onChange={setAttendees}
|
||||
organizerEmail={user?.email}
|
||||
organizer={organizer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ADD THIS: Recurrence editor */}
|
||||
{showRecurrence && (
|
||||
<div className="event-modal__recurrence-editor">
|
||||
<RecurrenceEditor
|
||||
value={recurrence}
|
||||
onChange={setRecurrence}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
## Step 6: Add CSS for Recurrence Section
|
||||
|
||||
In `Scheduler.scss`, add styling for the recurrence section:
|
||||
|
||||
```scss
|
||||
.event-modal {
|
||||
// ... existing styles
|
||||
|
||||
&__recurrence-editor {
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
// Ensure feature tags wrap properly
|
||||
&__features {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap; // Add this if not present
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete EventModal Component Structure
|
||||
|
||||
Here's the complete structure with recurrence integrated:
|
||||
|
||||
```typescript
|
||||
const EventModal = ({
|
||||
isOpen,
|
||||
mode,
|
||||
event,
|
||||
calendarUrl,
|
||||
calendars,
|
||||
adapter,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: EventModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState(event?.summary || "");
|
||||
const [description, setDescription] = useState(event?.description || "");
|
||||
const [location, setLocation] = useState(event?.location || "");
|
||||
const [startDateTime, setStartDateTime] = useState("");
|
||||
const [endDateTime, setEndDateTime] = useState("");
|
||||
const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(calendarUrl);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Features state
|
||||
const [attendees, setAttendees] = useState<IcsAttendee[]>([]);
|
||||
const [showAttendees, setShowAttendees] = useState(false);
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>();
|
||||
const [showRecurrence, setShowRecurrence] = useState(false);
|
||||
|
||||
// Calculate organizer
|
||||
const organizer: IcsOrganizer | undefined = event?.organizer || ...;
|
||||
|
||||
// Reset form when event changes
|
||||
useEffect(() => {
|
||||
// Reset basic fields
|
||||
setTitle(event?.summary || "");
|
||||
setDescription(event?.description || "");
|
||||
setLocation(event?.location || "");
|
||||
setSelectedCalendarUrl(calendarUrl);
|
||||
|
||||
// Reset attendees
|
||||
if (event?.attendees && event.attendees.length > 0) {
|
||||
setAttendees(event.attendees);
|
||||
setShowAttendees(true);
|
||||
} else {
|
||||
setAttendees([]);
|
||||
setShowAttendees(false);
|
||||
}
|
||||
|
||||
// Reset recurrence
|
||||
if (event?.recurrenceRule) {
|
||||
setRecurrence(event.recurrenceRule);
|
||||
setShowRecurrence(true);
|
||||
} else {
|
||||
setRecurrence(undefined);
|
||||
setShowRecurrence(false);
|
||||
}
|
||||
|
||||
// Reset dates
|
||||
// ... existing date reset logic
|
||||
}, [event, calendarUrl]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// ... create icsEvent with recurrence
|
||||
const icsEvent: IcsEvent = {
|
||||
// ... all fields
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
|
||||
await onSave(icsEvent, selectedCalendarUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
{/* Title, Calendar selector, Dates */}
|
||||
|
||||
{/* Feature tags */}
|
||||
<div className="event-modal__features">
|
||||
<button onClick={() => setShowAttendees(!showAttendees)}>
|
||||
<span className="material-icons">group</span>
|
||||
{t('calendar.event.attendees')}
|
||||
</button>
|
||||
|
||||
<button onClick={() => setShowRecurrence(!showRecurrence)}>
|
||||
<span className="material-icons">repeat</span>
|
||||
{t('calendar.recurrence.label')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Location, Description */}
|
||||
|
||||
{/* Attendees section */}
|
||||
{showAttendees && (
|
||||
<AttendeesInput
|
||||
attendees={attendees}
|
||||
onChange={setAttendees}
|
||||
organizerEmail={user?.email}
|
||||
organizer={organizer}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Recurrence section */}
|
||||
{showRecurrence && (
|
||||
<RecurrenceEditor
|
||||
value={recurrence}
|
||||
onChange={setRecurrence}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Save/Cancel buttons */}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Material Icons
|
||||
|
||||
The recurrence button uses the `repeat` Material icon. Make sure Material Icons are loaded:
|
||||
|
||||
```html
|
||||
<!-- In _app.tsx or layout -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
```
|
||||
|
||||
## Testing the Integration
|
||||
|
||||
1. **Create new recurring event:**
|
||||
- Click "Create" in calendar
|
||||
- Click "Repeat" button (🔁 icon)
|
||||
- Select "Weekly", check "Monday" and "Wednesday"
|
||||
- Save
|
||||
|
||||
2. **Edit recurring event:**
|
||||
- Click on a recurring event
|
||||
- Modal should show recurrence with "Repeat" button active
|
||||
- Modify recurrence pattern
|
||||
- Save
|
||||
|
||||
3. **Remove recurrence:**
|
||||
- Open recurring event
|
||||
- Click "Repeat" button to expand
|
||||
- Select "None" from dropdown
|
||||
- Save
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
- ✅ Recurrence button toggles RecurrenceEditor visibility
|
||||
- ✅ Active button shows blue background (like attendees)
|
||||
- ✅ RecurrenceEditor state persists when toggling visibility
|
||||
- ✅ Saving event includes recurrence in IcsEvent
|
||||
- ✅ Opening existing recurring event loads recurrence correctly
|
||||
- ✅ Calendar displays recurring event instances
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Recurrence not saving
|
||||
- Check that `recurrenceRule: recurrence` is in the icsEvent object
|
||||
- Verify ts-ics is correctly serializing the RRULE
|
||||
|
||||
### Recurrence not loading when editing
|
||||
- Check the useEffect includes recurrence reset
|
||||
- Verify event?.recurrenceRule is being passed from EventCalendarAdapter
|
||||
|
||||
### UI not showing properly
|
||||
- Ensure RecurrenceEditor.scss is imported in globals.scss
|
||||
- Check that Material Icons font is loaded
|
||||
|
||||
### Events not appearing as recurring
|
||||
- Verify CalDAV server supports RRULE
|
||||
- Check browser console for errors
|
||||
- Inspect .ics file content in network tab
|
||||
|
||||
## Next Steps
|
||||
|
||||
After integration, consider:
|
||||
1. Adding recurrence summary text (e.g., "Repeats weekly on Monday")
|
||||
2. Handle editing single instance vs series
|
||||
3. Add "Delete series" vs "Delete this occurrence" options
|
||||
4. Show recurrence icon in calendar event display
|
||||
@@ -1,196 +0,0 @@
|
||||
# Plan : Découpage en plusieurs PRs
|
||||
|
||||
## Contexte
|
||||
|
||||
- **24 commits** depuis `main`
|
||||
- **158 fichiers** modifiés (+16,943 / -10,848 lignes)
|
||||
- Travail accumulé sans découpage en PRs
|
||||
|
||||
---
|
||||
|
||||
## Stratégie
|
||||
|
||||
Créer **5-6 branches** depuis `main`, chacune avec des commits logiques,
|
||||
puis créer une PR pour chaque branche.
|
||||
|
||||
**Approche technique :**
|
||||
1. Rester sur `poc/event-calendar` (branche actuelle de travail)
|
||||
2. Pour chaque PR : créer une nouvelle branche depuis `main`, copier les
|
||||
fichiers pertinents depuis `poc/event-calendar`, commiter
|
||||
|
||||
---
|
||||
|
||||
## Découpage proposé (6 PRs)
|
||||
|
||||
### PR 1 : Backend - Invitations CalDAV avec emails
|
||||
**Branche** : `feat/caldav-invitations`
|
||||
|
||||
**Fichiers :**
|
||||
- `docker/sabredav/src/AttendeeNormalizerPlugin.php`
|
||||
- `docker/sabredav/src/HttpCallbackIMipPlugin.php`
|
||||
- `docker/sabredav/server.php`
|
||||
- `docker/sabredav/sql/pgsql.calendars.sql`
|
||||
- `src/backend/core/services/calendar_invitation_service.py`
|
||||
- `src/backend/core/api/viewsets_caldav.py`
|
||||
- `src/backend/core/templates/emails/calendar_invitation*.html/txt`
|
||||
- `src/backend/calendars/settings.py`
|
||||
- `env.d/development/backend.defaults`
|
||||
- `env.d/development/caldav.defaults`
|
||||
- `compose.yaml`
|
||||
|
||||
**Description PR** : Ajout du scheduling CalDAV (iTIP) avec envoi d'emails
|
||||
pour les invitations, mises à jour et annulations.
|
||||
|
||||
---
|
||||
|
||||
### PR 2 : Frontend - Refactoring CalDavService et helpers
|
||||
**Branche** : `refactor/caldav-service`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/services/dav/CalDavService.ts`
|
||||
- `features/calendar/services/dav/EventCalendarAdapter.ts`
|
||||
- `features/calendar/services/dav/caldav-helpers.ts`
|
||||
- `features/calendar/services/dav/helpers/*.ts`
|
||||
- `features/calendar/services/dav/types/*.ts`
|
||||
- `features/calendar/services/dav/constants.ts`
|
||||
- `features/calendar/services/dav/__tests__/*.ts`
|
||||
|
||||
**Description PR** : Refactoring du service CalDAV avec extraction des
|
||||
helpers, meilleure gestion des types et ajout de tests.
|
||||
|
||||
---
|
||||
|
||||
### PR 3 : Frontend - Composant Scheduler (EventModal, handlers)
|
||||
**Branche** : `feat/scheduler-component`
|
||||
|
||||
**Dépend de** : PR 2
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/components/scheduler/*`
|
||||
- `features/calendar/components/RecurrenceEditor.tsx`
|
||||
- `features/calendar/components/RecurrenceEditor.scss`
|
||||
- `features/calendar/components/AttendeesInput.tsx`
|
||||
- `features/calendar/components/AttendeesInput.scss`
|
||||
- `features/calendar/contexts/CalendarContext.tsx`
|
||||
- `pages/calendar.tsx`
|
||||
- `pages/calendar.scss`
|
||||
|
||||
**Description PR** : Nouveau composant Scheduler avec EventModal pour
|
||||
la création/édition d'événements, gestion des récurrences et des invités.
|
||||
|
||||
---
|
||||
|
||||
### PR 4 : Frontend - Refactoring CalendarList modulaire
|
||||
**Branche** : `refactor/calendar-list`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/components/calendar-list/*`
|
||||
- `features/calendar/components/LeftPanel.tsx`
|
||||
- `features/calendar/components/MiniCalendar.tsx`
|
||||
- `features/calendar/components/MiniCalendar.scss`
|
||||
- `features/calendar/components/CreateCalendarModal.tsx`
|
||||
- `features/calendar/components/CalendarList.scss`
|
||||
- `features/calendar/components/index.ts`
|
||||
|
||||
**Description PR** : Refactoring de CalendarList en composants modulaires
|
||||
(CalendarItemMenu, CalendarListItem, CalendarModal, DeleteConfirmModal).
|
||||
|
||||
---
|
||||
|
||||
### PR 5 : Frontend - Support i18n et locales
|
||||
**Branche** : `feat/calendar-i18n`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/hooks/useCalendarLocale.ts`
|
||||
- `features/i18n/*` (si modifié)
|
||||
- `src/frontend/apps/e2e/__tests__/calendar-locale.test.ts`
|
||||
|
||||
**Description PR** : Ajout du support des locales pour le calendrier
|
||||
avec tests e2e.
|
||||
|
||||
---
|
||||
|
||||
### PR 6 : Frontend - Nettoyage code mort
|
||||
**Branche** : `chore/remove-dead-code`
|
||||
|
||||
**Fichiers supprimés :**
|
||||
- `features/ui/components/breadcrumbs/`
|
||||
- `features/ui/components/circular-progress/`
|
||||
- `features/ui/components/infinite-scroll/`
|
||||
- `features/ui/components/info/`
|
||||
- `features/ui/components/responsive/`
|
||||
- `features/forms/components/RhfInput.tsx`
|
||||
- `hooks/useCopyToClipboard.tsx`
|
||||
- `utils/useLayout.tsx`
|
||||
- `features/calendar/components/EventModalDeprecated.tsx`
|
||||
- `features/calendar/components/EventModalAdapter.tsx`
|
||||
- `features/calendar/hooks/useEventModal.tsx`
|
||||
- `features/calendar/hooks/useCreateEventModal.tsx`
|
||||
- `src/frontend/packages/open-calendar/` (package entier)
|
||||
|
||||
**Description PR** : Suppression du code mort et des composants inutilisés.
|
||||
|
||||
---
|
||||
|
||||
## Ordre de merge recommandé
|
||||
|
||||
```
|
||||
1. PR 1 (Backend invitations) - indépendante
|
||||
2. PR 2 (CalDavService) - indépendante
|
||||
3. PR 6 (Dead code) - indépendante
|
||||
4. PR 5 (i18n) - indépendante
|
||||
5. PR 4 (CalendarList) - après PR 6
|
||||
6. PR 3 (Scheduler) - après PR 2, PR 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étapes d'exécution
|
||||
|
||||
Pour chaque PR :
|
||||
|
||||
```bash
|
||||
# 1. Créer la branche depuis main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b <branch-name>
|
||||
|
||||
# 2. Copier les fichiers depuis poc/event-calendar
|
||||
git checkout poc/event-calendar -- <fichiers>
|
||||
|
||||
# 3. Vérifier et commiter
|
||||
git add .
|
||||
git commit -m "..."
|
||||
|
||||
# 4. Pousser et créer la PR
|
||||
git push -u origin <branch-name>
|
||||
gh pr create --title "..." --body "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à exclure des PRs
|
||||
|
||||
- `CLAUDE.md` (fichier local)
|
||||
- `IMPLEMENTATION_CHECKLIST.md`, `README_RECURRENCE.md`, etc.
|
||||
(documentation temporaire à supprimer ou consolider)
|
||||
|
||||
---
|
||||
|
||||
## Vérification
|
||||
|
||||
Avant chaque PR :
|
||||
```bash
|
||||
cd src/frontend/apps/calendars
|
||||
yarn tsc --noEmit # Types OK
|
||||
yarn lint # Lint OK
|
||||
yarn test # Tests OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faisabilité
|
||||
|
||||
**Oui, c'est tout à fait possible.** La stratégie `git checkout <branch> -- <files>`
|
||||
permet de récupérer des fichiers spécifiques d'une branche sans perdre
|
||||
l'historique de travail. Chaque PR sera autonome et reviewable indépendamment.
|
||||
195
docs/invitations.md
Normal file
195
docs/invitations.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Invitations
|
||||
|
||||
How event invitations work end-to-end: creating, sending, responding,
|
||||
updating, and cancelling.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Frontend (EventModal)
|
||||
→ CalDAV proxy (Django)
|
||||
→ SabreDAV (stores event, detects attendees)
|
||||
→ HttpCallbackIMipPlugin (HTTP POST to Django)
|
||||
→ CalendarInvitationService (sends email)
|
||||
→ Attendee receives email
|
||||
→ RSVP link or iTIP client response
|
||||
→ RSVPView (Django) or CalDAV REPLY
|
||||
→ PARTSTAT updated in event
|
||||
→ Organizer notified
|
||||
```
|
||||
|
||||
## Creating an event with attendees
|
||||
|
||||
1. User adds attendees via `AttendeesSection` in EventModal
|
||||
2. `useEventForm.toIcsEvent()` serializes the event with `ATTENDEE`
|
||||
and `ORGANIZER` properties
|
||||
3. `CalDavService.createEvent()` sends a PUT to CalDAV through the
|
||||
Django proxy
|
||||
4. The proxy (`CalDAVProxyView`) injects an
|
||||
`X-CalDAV-Callback-URL` header pointing back to Django
|
||||
|
||||
The resulting `.ics` contains:
|
||||
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123
|
||||
SUMMARY:Team Meeting
|
||||
DTSTART:20260301T140000Z
|
||||
DTEND:20260301T150000Z
|
||||
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:bob@example.com
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
## SabreDAV processing
|
||||
|
||||
When SabreDAV receives the event, three plugins run in order:
|
||||
|
||||
1. **CalendarSanitizerPlugin** (priority 85) — strips inline binary
|
||||
attachments (Outlook signatures), truncates oversized fields,
|
||||
enforces max resource size (1 MB default)
|
||||
2. **AttendeeNormalizerPlugin** (priority 90) — lowercases emails,
|
||||
deduplicates attendees keeping the highest-priority PARTSTAT
|
||||
(ACCEPTED > TENTATIVE > DECLINED > NEEDS-ACTION)
|
||||
3. **iMip scheduling** — detects attendees and creates a REQUEST
|
||||
message for each one
|
||||
|
||||
The scheduling message is routed by **HttpCallbackIMipPlugin**, which
|
||||
POSTs to Django:
|
||||
|
||||
```
|
||||
POST /api/v1.0/caldav-scheduling-callback/
|
||||
X-Api-Key: <shared secret>
|
||||
X-CalDAV-Sender: alice@example.com
|
||||
X-CalDAV-Recipient: bob@example.com
|
||||
X-CalDAV-Method: REQUEST
|
||||
Content-Type: text/calendar
|
||||
|
||||
<serialized VCALENDAR>
|
||||
```
|
||||
|
||||
## Sending invitation emails
|
||||
|
||||
`CalDAVSchedulingCallbackView` receives the callback and delegates to
|
||||
`CalendarInvitationService.send_invitation()`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Parse** — `ICalendarParser.parse()` extracts UID, summary,
|
||||
dates, organizer, attendee, location, description, sequence number
|
||||
2. **Template selection** based on method and sequence:
|
||||
| Method | Sequence | Template |
|
||||
|--------|----------|----------|
|
||||
| REQUEST | 0 | `calendar_invitation.html` |
|
||||
| REQUEST | >0 | `calendar_invitation_update.html` |
|
||||
| CANCEL | any | `calendar_invitation_cancel.html` |
|
||||
| REPLY | any | `calendar_invitation_reply.html` |
|
||||
3. **RSVP tokens** — for REQUEST emails, generates signed URLs:
|
||||
```
|
||||
/rsvp/?token=<signed>&action=accepted
|
||||
/rsvp/?token=<signed>&action=tentative
|
||||
/rsvp/?token=<signed>&action=declined
|
||||
```
|
||||
Tokens are signed with `django.core.signing.Signer(salt="rsvp")`
|
||||
and contain `{uid, email, organizer}`.
|
||||
4. **ICS attachment** — if `CALENDAR_ITIP_ENABLED=True`, the
|
||||
attachment includes `METHOD:REQUEST` for iTIP-aware clients
|
||||
(Outlook, Apple Mail). If False (default), the METHOD is stripped
|
||||
and web RSVP links are used instead.
|
||||
5. **Send** — multipart email with HTML + plain text + ICS attachment.
|
||||
Reply-To is set to the organizer's email.
|
||||
|
||||
## Responding to invitations
|
||||
|
||||
Two paths:
|
||||
|
||||
### Web RSVP (default)
|
||||
|
||||
Attendee clicks Accept / Maybe / Decline link in the email.
|
||||
|
||||
`RSVPView` handles `GET /rsvp/?token=...&action=accepted`:
|
||||
|
||||
1. Unsigns the token (salt="rsvp")
|
||||
2. Finds the event in the organizer's CalDAV calendar by UID
|
||||
3. Checks the event is not in the past (recurring events are never
|
||||
considered past)
|
||||
4. Updates the attendee's `PARTSTAT` to ACCEPTED / TENTATIVE / DECLINED
|
||||
5. PUTs the updated event back to CalDAV
|
||||
6. Renders a confirmation page
|
||||
|
||||
The PUT triggers SabreDAV to generate a REPLY message, which flows
|
||||
back through HttpCallbackIMipPlugin → Django → organizer email.
|
||||
|
||||
### iTIP client response
|
||||
|
||||
When `CALENDAR_ITIP_ENABLED=True`, email clients like Outlook or
|
||||
Apple Calendar show native Accept/Decline buttons. The client sends
|
||||
an iTIP REPLY directly to the CalDAV server, which triggers the same
|
||||
callback flow.
|
||||
|
||||
## Updating an event
|
||||
|
||||
When an event with attendees is modified:
|
||||
|
||||
1. `CalDavService.updateEvent()` increments the `SEQUENCE` number
|
||||
2. SabreDAV detects the change and creates REQUEST messages with the
|
||||
updated sequence
|
||||
3. Attendees receive an update email
|
||||
(`calendar_invitation_update.html`)
|
||||
|
||||
## Cancelling an event
|
||||
|
||||
When an event with attendees is deleted:
|
||||
|
||||
1. SabreDAV creates CANCEL messages for each attendee
|
||||
2. Attendees receive a cancellation email
|
||||
(`calendar_invitation_cancel.html`)
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `CALDAV_URL` | `http://caldav:80` | Internal CalDAV server URL |
|
||||
| `CALDAV_INBOUND_API_KEY` | None | API key for callbacks from CalDAV |
|
||||
| `CALDAV_OUTBOUND_API_KEY` | None | API key for requests to CalDAV |
|
||||
| `CALDAV_CALLBACK_BASE_URL` | None | Internal URL for CalDAV→Django (Docker: `http://backend:8000`) |
|
||||
| `CALENDAR_ITIP_ENABLED` | False | Use iTIP METHOD headers in ICS attachments |
|
||||
| `CALENDAR_INVITATION_FROM_EMAIL` | `DEFAULT_FROM_EMAIL` | Sender address for invitation emails |
|
||||
| `APP_URL` | `""` | Base URL for RSVP links in emails |
|
||||
|
||||
## Key files
|
||||
|
||||
| Area | Path |
|
||||
|------|------|
|
||||
| Attendee UI | `src/frontend/.../event-modal-sections/AttendeesSection.tsx` |
|
||||
| Event form | `src/frontend/.../scheduler/hooks/useEventForm.ts` |
|
||||
| CalDAV client | `src/frontend/.../services/dav/CalDavService.ts` |
|
||||
| CalDAV proxy | `src/backend/core/api/viewsets_caldav.py` |
|
||||
| Scheduling callback | `src/backend/core/api/viewsets_caldav.py` (`CalDAVSchedulingCallbackView`) |
|
||||
| RSVP handler | `src/backend/core/api/viewsets_rsvp.py` |
|
||||
| Email service | `src/backend/core/services/calendar_invitation_service.py` |
|
||||
| ICS parser | `src/backend/core/services/calendar_invitation_service.py` (`ICalendarParser`) |
|
||||
| Email templates | `src/backend/core/templates/emails/calendar_invitation*.html` |
|
||||
| SabreDAV sanitizer | `docker/sabredav/src/CalendarSanitizerPlugin.php` |
|
||||
| SabreDAV attendee dedup | `docker/sabredav/src/AttendeeNormalizerPlugin.php` |
|
||||
| SabreDAV callback plugin | `docker/sabredav/src/HttpCallbackIMipPlugin.php` |
|
||||
|
||||
## Future: Messages mail client integration
|
||||
|
||||
La Suite includes a Messages mail client (based on an open-source
|
||||
webmail). Future integration would allow:
|
||||
|
||||
- **Inline RSVP** — render Accept/Decline buttons directly in the
|
||||
Messages UI when an email contains a `text/calendar` attachment with
|
||||
`METHOD:REQUEST`
|
||||
- **Calendar preview** — show event details (date, time, location)
|
||||
extracted from the ICS attachment without opening the full calendar
|
||||
- **Auto-add to calendar** — accepted events automatically appear in
|
||||
the user's Calendars calendar via a shared CalDAV backend
|
||||
- **Status sync** — PARTSTAT changes in Messages propagate to
|
||||
Calendars and vice versa
|
||||
|
||||
This requires Messages to support iTIP processing
|
||||
(`CALENDAR_ITIP_ENABLED=True`) and share the same CalDAV/auth
|
||||
infrastructure.
|
||||
107
docs/recurrence.md
Normal file
107
docs/recurrence.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Recurring Events
|
||||
|
||||
Recurring events follow the iCalendar RFC 5545 RRULE standard. No backend
|
||||
changes are needed — CalDAV (SabreDAV) handles recurrence natively.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
RecurrenceEditor (React)
|
||||
-> IcsRecurrenceRule (ts-ics)
|
||||
-> RRULE string (RFC 5545)
|
||||
-> .ics file (CalDAV)
|
||||
-> SabreDAV server
|
||||
```
|
||||
|
||||
## RecurrenceEditor Component
|
||||
|
||||
Located at
|
||||
`src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.tsx`
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule>();
|
||||
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
```
|
||||
|
||||
Include in the event object:
|
||||
|
||||
```typescript
|
||||
const event: IcsEvent = {
|
||||
// ...other fields
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
```
|
||||
|
||||
### Supported patterns
|
||||
|
||||
| Pattern | RRULE |
|
||||
|---------|-------|
|
||||
| Every day | `FREQ=DAILY` |
|
||||
| Every 3 days | `FREQ=DAILY;INTERVAL=3` |
|
||||
| Every Monday | `FREQ=WEEKLY;BYDAY=MO` |
|
||||
| Mon/Wed/Fri | `FREQ=WEEKLY;BYDAY=MO,WE,FR` |
|
||||
| Every 2 weeks on Thu | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TH` |
|
||||
| 15th of each month | `FREQ=MONTHLY;BYMONTHDAY=15` |
|
||||
| March 15 yearly | `FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15` |
|
||||
| 10 occurrences | append `;COUNT=10` |
|
||||
| Until a date | append `;UNTIL=20251231T235959Z` |
|
||||
|
||||
### Not yet supported
|
||||
|
||||
- `BYSETPOS` (e.g. "1st Monday of month", "last Friday")
|
||||
- Edit single instance vs series (needs RECURRENCE-ID UI)
|
||||
- Visual preview of recurrence pattern
|
||||
|
||||
### Date validation
|
||||
|
||||
The component warns about edge cases:
|
||||
- Feb 30/31 — "February has at most 29 days"
|
||||
- Feb 29 — "Only exists in leap years"
|
||||
- Day 31 on 30-day months — shown as warning
|
||||
|
||||
### Translations
|
||||
|
||||
Supported: English, French, Dutch. Keys are in
|
||||
`src/frontend/apps/calendars/src/features/i18n/translations.json`
|
||||
under `calendar.recurrence.*`.
|
||||
|
||||
## IcsRecurrenceRule interface (ts-ics)
|
||||
|
||||
```typescript
|
||||
interface IcsRecurrenceRule {
|
||||
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
interval?: number;
|
||||
count?: number;
|
||||
until?: IcsDateObject;
|
||||
byDay?: { day: 'MO'|'TU'|'WE'|'TH'|'FR'|'SA'|'SU' }[];
|
||||
byMonthDay?: number[];
|
||||
byMonth?: number[];
|
||||
}
|
||||
```
|
||||
|
||||
## How CalDAV handles it
|
||||
|
||||
1. RRULE is stored as a property in the VEVENT inside the `.ics` file
|
||||
2. SabreDAV expands recurring instances when clients query date ranges
|
||||
3. Individual instance modifications use RECURRENCE-ID (not yet in UI)
|
||||
|
||||
Example `.ics`:
|
||||
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123
|
||||
SUMMARY:Weekly Team Meeting
|
||||
DTSTART:20260125T140000Z
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20261231T235959Z
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 5545 — iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE spec](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [SabreDAV](https://sabre.io/dav/)
|
||||
Reference in New Issue
Block a user