bug fixes
This commit is contained in:
48
.env.fcmtest-push
Normal file
48
.env.fcmtest-push
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#** Required
|
||||||
|
DB_PASSWORD=C@nuck2024
|
||||||
|
JWT_SECRET=changemesupersecretjwtkey
|
||||||
|
|
||||||
|
#** App identity
|
||||||
|
PROJECT_NAME=rosterchirp
|
||||||
|
APP_NAME=RosterChirp
|
||||||
|
DEFCHAT_NAME=General Chat
|
||||||
|
ADMIN_NAME=Admin User
|
||||||
|
ADMIN_EMAIL=admin@rosterchirp.local
|
||||||
|
ADMIN_PASS=Admin@1234
|
||||||
|
ADMPW_RESET=false
|
||||||
|
|
||||||
|
#** Database
|
||||||
|
# DB names intentionally kept as 'rosterchirp' — matches the existing live database
|
||||||
|
DB_NAME=rosterchirp
|
||||||
|
DB_USER=rosterchirp
|
||||||
|
# DB_HOST and DB_PORT are set automatically in docker-compose (host=db, port=5432)
|
||||||
|
|
||||||
|
#** Tenancy mode
|
||||||
|
# selfhost = single tenant (RosterChirp-Chat / RosterChirp-Brand / RosterChirp-Team)
|
||||||
|
# host = multi-tenant (RosterChirp-Host only)
|
||||||
|
APP_TYPE=host
|
||||||
|
|
||||||
|
#** RosterChirp-Host only (ignored in selfhost mode)
|
||||||
|
HOST_DOMAIN=rosterchirphost.stretchy.ca
|
||||||
|
HOST_ADMIN_KEY=VBGFHEANTTGRDDWAASJKH
|
||||||
|
|
||||||
|
#** Optional
|
||||||
|
PORT=3144
|
||||||
|
TZ=America/Toronto
|
||||||
|
|
||||||
|
#** Firebase Cloud Messaging (FCM) — Android background push
|
||||||
|
# Web app config — from Firebase Console → Project Settings → General → Your apps
|
||||||
|
FIREBASE_API_KEY=AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA
|
||||||
|
FIREBASE_PROJECT_ID=fcmtest-push
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID=439263996034
|
||||||
|
FIREBASE_APP_ID=1:439263996034:web:62a6a6b0afdbad99fdec9b
|
||||||
|
# VAPID key — from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates
|
||||||
|
FIREBASE_VAPID_KEY=BE6hPKkbf-h0lUQ1tYo249pBOdZFFcWQn9suwg3NDwSE8C_hv8hk1dUY9zxHBQEChO_IAqyFZplF_SUb5c4Ofrw
|
||||||
|
# Service account — from Firebase Console → Project Settings → Service accounts → Generate new private key
|
||||||
|
FIREBASE_SERVICE_ACCOUNT={"type": "service_account", "project_id": "fcmtest-push", "private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n","client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com", "client_id": "103917424542871804597", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token","auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com", "universe_domain": "googleapis.com" }
|
||||||
|
|
||||||
|
RC_VERSION=latest
|
||||||
|
|
||||||
|
VAPID_SUBJECT=mailto:devpush@rosterchirp.com
|
||||||
|
VAPID_PUBLIC=BGYS6vMY7zlx31UKRN9QcaOwfomoDJ50_MtfTcfE84q5bhTLq0rM1zSa6uzBTRBxZuFW1kMQP7ardN_jog3T14Y
|
||||||
|
VAPID_PRIVATE=8SnDSEy_gs2jNwXLtOchZfHW0ppy_RG8wtvjSjYGA48
|
||||||
@@ -199,56 +199,14 @@ router.get('/', authMiddleware, async (req, res) => {
|
|||||||
if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); }
|
if (to) { sql += ` AND start_at <= $${pi++}`; params.push(to); }
|
||||||
sql += ' ORDER BY start_at ASC';
|
sql += ' ORDER BY start_at ASC';
|
||||||
const rawEvents = await query(req.schema, sql, params);
|
const rawEvents = await query(req.schema, sql, params);
|
||||||
|
|
||||||
// Separate master events and exception instances
|
|
||||||
const masterEvents = [];
|
|
||||||
const exceptionInstances = [];
|
|
||||||
for (const e of rawEvents) {
|
|
||||||
if (e.recurring_master_id) {
|
|
||||||
exceptionInstances.push(e);
|
|
||||||
} else {
|
|
||||||
masterEvents.push(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a map of exception dates by master event
|
|
||||||
const exceptionDatesByMaster = new Map();
|
|
||||||
for (const exc of exceptionInstances) {
|
|
||||||
if (!exceptionDatesByMaster.has(exc.recurring_master_id)) {
|
|
||||||
exceptionDatesByMaster.set(exc.recurring_master_id, new Set());
|
|
||||||
}
|
|
||||||
const dateStr = new Date(exc.original_start_at || exc.start_at).toISOString().split('T')[0];
|
|
||||||
exceptionDatesByMaster.get(exc.recurring_master_id).add(dateStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = [];
|
const events = [];
|
||||||
for (const e of masterEvents) {
|
for (const e of rawEvents) {
|
||||||
// Skip master events if all their occurrences in range are replaced by exceptions
|
|
||||||
if (e.recurrence_rule && exceptionDatesByMaster.has(e.id)) {
|
|
||||||
const exceptions = exceptionDatesByMaster.get(e.id);
|
|
||||||
const rule = e.recurrence_rule;
|
|
||||||
|
|
||||||
// Check if this is a simple case where all occurrences are replaced
|
|
||||||
// For now, we'll include the master and let frontend handle filtering
|
|
||||||
// This is safer to avoid missing edge cases
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
|
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
|
||||||
await enrichEvent(req.schema, e);
|
await enrichEvent(req.schema, e);
|
||||||
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
|
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
|
||||||
e.my_response = mine?.response || null;
|
e.my_response = mine?.response || null;
|
||||||
events.push(e);
|
events.push(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add exception instances (they're standalone events)
|
|
||||||
for (const e of exceptionInstances) {
|
|
||||||
if (!(await canViewEvent(req.schema, e, req.user.id, itm))) continue;
|
|
||||||
await enrichEvent(req.schema, e);
|
|
||||||
const mine = await queryOne(req.schema, 'SELECT response FROM event_availability WHERE event_id=$1 AND user_id=$2', [e.id]);
|
|
||||||
e.my_response = mine?.response || null;
|
|
||||||
events.push(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ events });
|
res.json({ events });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|||||||
66
docker-compose-fcmtest-push.yaml
Normal file
66
docker-compose-fcmtest-push.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
rosterchirp:
|
||||||
|
image: rosterchirp-dev:${RC_VERSION:-latest}
|
||||||
|
container_name: ${PROJECT_NAME:-rosterchirp}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- TZ=${TZ:-UTC}
|
||||||
|
- APP_TYPE=${APP_TYPE:-selfhost}
|
||||||
|
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
|
||||||
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@rosterchirp.local}
|
||||||
|
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
|
||||||
|
- ADMPW_RESET=${ADMPW_RESET:-false}
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-changeme_super_secret_jwt_key_2024}
|
||||||
|
- APP_NAME=${APP_NAME:-rosterchirp}
|
||||||
|
- DEFCHAT_NAME=${DEFCHAT_NAME:-General Chat}
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_NAME=${DB_NAME:-rosterchirp}
|
||||||
|
- DB_USER=${DB_USER:-rosterchirp}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
||||||
|
- HOST_DOMAIN=${HOST_DOMAIN:-}
|
||||||
|
- HOST_ADMIN_KEY=${HOST_ADMIN_KEY:-}
|
||||||
|
- FIREBASE_API_KEY=${FIREBASE_API_KEY:-}
|
||||||
|
- FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID:-}
|
||||||
|
- FIREBASE_MESSAGING_SENDER_ID=${FIREBASE_MESSAGING_SENDER_ID:-}
|
||||||
|
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
|
||||||
|
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
|
||||||
|
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
|
||||||
|
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:push@rosterchirp.com}
|
||||||
|
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
|
||||||
|
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
|
||||||
|
volumes:
|
||||||
|
- rosterchirp_uploads:/app/uploads
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ${PROJECT_NAME:-rosterchirp}_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${DB_NAME:-rosterchirp}
|
||||||
|
- POSTGRES_USER=${DB_USER:-rosterchirp}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required}
|
||||||
|
volumes:
|
||||||
|
- rosterchirp_db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-rosterchirp} -d ${DB_NAME:-rosterchirp}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
rosterchirp_db:
|
||||||
|
driver: local
|
||||||
|
rosterchirp_uploads:
|
||||||
|
driver: local
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
rosterchirp:
|
rosterchirp:
|
||||||
image: rosterchirp:${ROSTERCHIRP_VERSION:-latest}
|
image: rosterchirp:${RC_VERSION:-latest}
|
||||||
container_name: ${PROJECT_NAME:-rosterchirp}
|
container_name: ${PROJECT_NAME:-rosterchirp}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -29,6 +29,9 @@ services:
|
|||||||
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
|
- FIREBASE_APP_ID=${FIREBASE_APP_ID:-}
|
||||||
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
|
- FIREBASE_VAPID_KEY=${FIREBASE_VAPID_KEY:-}
|
||||||
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
|
- FIREBASE_SERVICE_ACCOUNT=${FIREBASE_SERVICE_ACCOUNT}
|
||||||
|
- VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:push@rosterchirp.com}
|
||||||
|
- VAPID_PUBLIC=${VAPID_PUBLIC:-CHANGEME}
|
||||||
|
- VAPID_PRIVATE=${VAPID_PRIVATE:-CHANGEME}
|
||||||
volumes:
|
volumes:
|
||||||
- rosterchirp_uploads:/app/uploads
|
- rosterchirp_uploads:/app/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
13
fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json
Normal file
13
fcmtest-push-firebase-adminsdk-fbsvc-ddbf38d0c5.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "fcmtest-push",
|
||||||
|
"private_key_id": "ddbf38d0c5f769b9b8b95000bf05c42b52bb58ad",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDR4jqMb/0UPVpU\nctpVl9UWHY5lePR4hMEoodbRPofNQgtvx5HuFE61cVrquD8mfUmFB9eZc112KPyy\nMuuZHFkJrVT3iEhK8AoeJTNbxh+YiQvwMhyn9/KO4ntr/HIZxqHs62M46rqehZFS\nFR79zG1ptl/hRFkTTwQoQOOAdqP6gJuX+XpKpVeLPNCBVOAhfM8APA4dvjXUqhrk\ns+L5sH8xdttY/XFdjNhiUtv7uHuvww1hBKliUL6dDZZAlm3uwuYAdsvIHkDWOJ+B\nn8EE7n01h5PUpOihQ2poBAFGHvrT9ifk3bzuGviE74ejErCbBEBwJZvvsaebvaG4\n5dI3SQLXAgMBAAECggEAELX0d24LNmtUH9ktLRdzrdkYl1e0D0xynKuWEP7rjRov\nEu1O3yfaxHOMC5gz3vqmueLP9bXLwTauN/n57Cznoe+dDkBZkS3fgFrx5eK2bUys\nGKnEwlLpixrZPNXSt96q0dRECCoYRbrYwTJRT1/RblNI+wSYGwN1j0brVjUcBTvH\nPjpnt9bkIS+Rb1XJg1+TfQFzt1/WvFscpDpc7zUCGczgD7hAXJU2v2NYZyNtjn2g\niFD4r0AODuFk1Z6C8fbUsgcl8AXXQnJSLPTUXnyzifzBVQmGBu3HewLDHI99pTCZ\nT8aOwgaWYUWrjeg0jfyid08j14OfhE58/PuYGwcNsQKBgQDoh1R/OQ147D75BpXP\nEI1TyKTJNZiwnzRnP64cmzAwbIfc6w06hXTJGKIVqMBwM31R8WUfJ5cBxQOb1g3a\nZDgOTz4zO9M0mFyE/L0V3XrTXzCgPN+pCbZjcAw8oizF3u2rupM5lppxuXnkDiSi\nA2GuVdPR6M8pXUmNubs5XNe1jQKBgQDnEb21yF1K2IF9p1T0jcqLkzDLZDHHudVr\ndZSVVEyhpIFmBFgF8EC3C/ehA5i8Ar1K5JvcP5hpAH345QWOUUAKk000A4WpazZx\nfZM5Ema1xju1AFOSMDzwjt4N32Dg1VPqiLDT+CjiQFH67lSretO1IpS//IKT1Y+W\nOv3/ENfm8wKBgHt7moixUJE9zDdMovPCU3sB21iq6Loq4ZZO//RbCV091WyhOnYw\ndxNvzGt6IS+0eEGy0sOXr56V9FOmedbXT9lxhZOJmqCcpM1Otk9NPbPQIi+GBDRt\nXvkxgJ4WdXZi6449139Glh/8ollUlWmgKBh/pawcWR8bVjs4Pc+5mSflAoGAYfMm\nTRmrWl/evGojXCuC8ZmqdH17kKOY8Z19J7P9bAP1Ck7LFXFbrXx4Mxv4MbKjlUzF\nOR8IN3KK8+f5a/PLRvBcKLFZhpC5GnDV6LqBKYrnonmJ841ZN8wIGy9WvNgRY3kg\nJCqtAgOr/MfswmgluEH5dkzO+WXtIQzOwMHeE7sCgYAhrJV+PvnbnaIpfTaOKXGl\nsvzSXTuey5fQQLKMKsp2haKDVZ2hadDejRHsLJKVGb+KwdJ1s5WmBJ4L80/MnOKZ\n+9Yby9DKviVx0TbvMUGuAuWMl9syo4ICMVpp0cbeSOCM5/ulYjKSeU3sFKo7aWa9\nU8Pskm36I88orq90OBpWOg==\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "firebase-adminsdk-fbsvc@fcmtest-push.iam.gserviceaccount.com",
|
||||||
|
"client_id": "103917424542871804597",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fcmtest-push.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
@@ -1125,57 +1125,14 @@ function expandRecurringEvent(ev, rangeStart, rangeEnd) {
|
|||||||
// Expand all recurring events in a list within a date range
|
// Expand all recurring events in a list within a date range
|
||||||
function expandEvents(events, rangeStart, rangeEnd) {
|
function expandEvents(events, rangeStart, rangeEnd) {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
// First, collect all exception instances and their dates by master event
|
|
||||||
const exceptionDatesByMaster = new Map();
|
|
||||||
const masterEvents = [];
|
|
||||||
const standaloneEvents = [];
|
|
||||||
|
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
if (ev.recurring_master_id) {
|
if (ev.recurrence_rule?.freq) {
|
||||||
// This is an exception instance
|
|
||||||
if (!exceptionDatesByMaster.has(ev.recurring_master_id)) {
|
|
||||||
exceptionDatesByMaster.set(ev.recurring_master_id, new Set());
|
|
||||||
}
|
|
||||||
const dateStr = new Date(ev.original_start_at || ev.start_at).toISOString().split('T')[0];
|
|
||||||
exceptionDatesByMaster.get(ev.recurring_master_id).add(dateStr);
|
|
||||||
standaloneEvents.push(ev); // Exception instances are standalone events
|
|
||||||
} else if (ev.recurrence_rule?.freq) {
|
|
||||||
masterEvents.push(ev);
|
|
||||||
} else {
|
|
||||||
standaloneEvents.push(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand master events, skipping dates that have exception instances
|
|
||||||
for (const ev of masterEvents) {
|
|
||||||
const exceptions = exceptionDatesByMaster.get(ev.id);
|
|
||||||
if (exceptions) {
|
|
||||||
// Merge the rule's exceptions with the instance exceptions
|
|
||||||
const rule = ev.recurrence_rule || {};
|
|
||||||
const ruleExceptions = new Set(rule.exceptions || []);
|
|
||||||
for (const date of exceptions) {
|
|
||||||
ruleExceptions.add(date);
|
|
||||||
}
|
|
||||||
// Create a copy of the event with merged exceptions
|
|
||||||
const evWithMergedExceptions = {
|
|
||||||
...ev,
|
|
||||||
recurrence_rule: {
|
|
||||||
...rule,
|
|
||||||
exceptions: Array.from(ruleExceptions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const expanded = expandRecurringEvent(evWithMergedExceptions, rangeStart, rangeEnd);
|
|
||||||
result.push(...expanded);
|
|
||||||
} else {
|
|
||||||
const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd);
|
const expanded = expandRecurringEvent(ev, rangeStart, rangeEnd);
|
||||||
result.push(...expanded);
|
result.push(...expanded);
|
||||||
|
} else {
|
||||||
|
result.push(ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all standalone events (non-recurring + exception instances)
|
|
||||||
result.push(...standaloneEvents);
|
|
||||||
|
|
||||||
// Sort by start_at
|
// Sort by start_at
|
||||||
result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at));
|
result.sort((a,b) => new Date(a.start_at) - new Date(b.start_at));
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user