📝(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