[Calendar] Introduce permissions for editing events
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import server from "/@server/server.js"
|
import server from "/calendar/@server/calendar.js"
|
||||||
import calendarUtil from "../calendarUtil.js"
|
import calendarUtil from "../calendarUtil.js"
|
||||||
import "./EventForm.js"
|
import "./EventForm.js"
|
||||||
import "../../components/BottomSheet.js"
|
import "../../components/BottomSheet.js"
|
||||||
@@ -39,10 +39,11 @@ class EventDetails extends Shadow {
|
|||||||
render() {
|
render() {
|
||||||
this.editSheet = BottomSheet(100) // separate sheet for the edit form, layered above this one
|
this.editSheet = BottomSheet(100) // separate sheet for the edit form, layered above this one
|
||||||
|
|
||||||
const isOwner = this.event?.creator_id === global.profile?.id;
|
// Editing is currently owner-only: this flag gates the mobile Edit/Delete actions.
|
||||||
|
const canEdit = global.currentNetwork.permissions.includes("events.edit")
|
||||||
|
|
||||||
VStack(() => {
|
VStack(() => {
|
||||||
this.renderHeader(isOwner)
|
this.renderHeader(canEdit)
|
||||||
|
|
||||||
// ── Error toast ───────────────────────────────────────
|
// ── Error toast ───────────────────────────────────────
|
||||||
VStack(() => {
|
VStack(() => {
|
||||||
@@ -276,11 +277,12 @@ class EventDetails extends Shadow {
|
|||||||
.height(100, pct)
|
.height(100, pct)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeader(isOwner) {
|
renderHeader(canEdit) {
|
||||||
VStack(() => {
|
VStack(() => {
|
||||||
HStack(() => {
|
HStack(() => {
|
||||||
BackButton(false, true, () => $("bottomsheet-").toggle())
|
BackButton(false, true, () => $("bottomsheet-").toggle())
|
||||||
if (isOwner) {
|
// Non-owners never see the controls that open the edit sheet or delete flow.
|
||||||
|
if (canEdit) {
|
||||||
HStack(() => {
|
HStack(() => {
|
||||||
// ── Delete button ─────────────────────────────────
|
// ── Delete button ─────────────────────────────────
|
||||||
button("Delete")
|
button("Delete")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import server from "/@server/server.js"
|
import server from "/calendar/@server/calendar.js"
|
||||||
import calendarUtil from "../../calendarUtil.js"
|
import calendarUtil from "../../calendarUtil.js"
|
||||||
import "../../../components/Avatar.js"
|
import "../../../components/Avatar.js"
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ class DesktopEventDetails extends Shadow {
|
|||||||
if (!this.event) return
|
if (!this.event) return
|
||||||
|
|
||||||
const eventCals = this.calendars.filter(c => this.event.calendars?.includes(c.id))
|
const eventCals = this.calendars.filter(c => this.event.calendars?.includes(c.id))
|
||||||
const isOwner = this.event.creator_id === global.profile?.id
|
const canEdit = global.currentNetwork.permissions.includes("events.edit")
|
||||||
|
|
||||||
VStack(() => {
|
VStack(() => {
|
||||||
this.renderHeader(isOwner)
|
this.renderHeader(canEdit)
|
||||||
this.renderBody(eventCals)
|
this.renderBody(eventCals)
|
||||||
HStack(() => {
|
HStack(() => {
|
||||||
const members = global.currentNetwork.data?.members || []
|
const members = global.currentNetwork.data?.members || []
|
||||||
@@ -53,7 +53,7 @@ class DesktopEventDetails extends Shadow {
|
|||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────────────
|
// ── Header ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
renderHeader(isOwner) {
|
renderHeader(canEdit) {
|
||||||
HStack(() => {
|
HStack(() => {
|
||||||
VStack(() => {
|
VStack(() => {
|
||||||
h2(this.event.title || "Untitled")
|
h2(this.event.title || "Untitled")
|
||||||
@@ -69,7 +69,8 @@ class DesktopEventDetails extends Shadow {
|
|||||||
.paddingBottom(0.5, em)
|
.paddingBottom(0.5, em)
|
||||||
.justifyContent("center")
|
.justifyContent("center")
|
||||||
|
|
||||||
if (isOwner) {
|
// Non-owners never see the controls that open the edit form or delete flow.
|
||||||
|
if (canEdit) {
|
||||||
button("Delete")
|
button("Delete")
|
||||||
.paddingVertical(0.34, em)
|
.paddingVertical(0.34, em)
|
||||||
.paddingHorizontal(0.85, em)
|
.paddingHorizontal(0.85, em)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
export async function setEventTime(eventId, startHour, startMin, endHour, endMin, month, day, year = 2026) {
|
export async function setEventTime(eventId, startHour, startMin, endHour, endMin, month, day, year = 2026) {
|
||||||
const start = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}:00-05:00`)
|
const start = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}:00-05:00`)
|
||||||
const end = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}:00-05:00`)
|
const end = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}:00-05:00`)
|
||||||
|
|
||||||
console.log(start, end)
|
await context.sql`
|
||||||
await this.sql`
|
|
||||||
UPDATE events.events
|
UPDATE events.events
|
||||||
SET time_start = ${start}, time_end = ${end}
|
SET time_start = ${start}, time_end = ${end}
|
||||||
WHERE id = ${eventId}
|
WHERE id = ${eventId}
|
||||||
@@ -22,7 +23,7 @@ export async function getEventsByNetwork(networkId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns each event with its linked calendar IDs, file attachments, and recurrence rule
|
// Returns each event with its linked calendar IDs, file attachments, and recurrence rule
|
||||||
return await this.sql`
|
return await context.sql`
|
||||||
SELECT e.*,
|
SELECT e.*,
|
||||||
COALESCE(jsonb_agg(DISTINCT ec.calendar_id) FILTER (WHERE ec.calendar_id IS NOT NULL), '[]') AS calendars,
|
COALESCE(jsonb_agg(DISTINCT ec.calendar_id) FILTER (WHERE ec.calendar_id IS NOT NULL), '[]') AS calendars,
|
||||||
COALESCE(jsonb_agg(DISTINCT jsonb_build_object('id', f.id, 'name', f.name, 'type', f.type, 'original_name', f.original_name, 'size_bytes', f.size_bytes)) FILTER (WHERE f.id IS NOT NULL), '[]') AS attachments,
|
COALESCE(jsonb_agg(DISTINCT jsonb_build_object('id', f.id, 'name', f.name, 'type', f.type, 'original_name', f.original_name, 'size_bytes', f.size_bytes)) FILTER (WHERE f.id IS NOT NULL), '[]') AS attachments,
|
||||||
@@ -42,22 +43,23 @@ export async function getCalendarsByNetwork(networkId) {
|
|||||||
throw new global.ServerError(400, "Invalid network ID!");
|
throw new global.ServerError(400, "Invalid network ID!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.sql`
|
return await context.sql`
|
||||||
SELECT c.* FROM events.calendars c
|
SELECT c.* FROM events.calendars c
|
||||||
WHERE c.network_id = ${networkId}
|
WHERE c.network_id = ${networkId}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addCalendar(userId, newCalendar, networkId) {
|
export async function addCalendar(newCalendar, networkId) {
|
||||||
|
//@ should have scoping here
|
||||||
const { name, description, color } = newCalendar;
|
const { name, description, color } = newCalendar;
|
||||||
try {
|
try {
|
||||||
let insertedCalendar;
|
let insertedCalendar;
|
||||||
|
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
const [calendar] = await tx`
|
const [calendar] = await tx`
|
||||||
INSERT INTO events.calendars ${tx({
|
INSERT INTO events.calendars ${tx({
|
||||||
network_id: networkId,
|
network_id: networkId,
|
||||||
owner_id: userId,
|
owner_id: context.userId,
|
||||||
name: name,
|
name: name,
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
color: color
|
color: color
|
||||||
@@ -74,10 +76,10 @@ export async function addCalendar(userId, newCalendar, networkId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
export async function editCalendar(id, updatedCalendar, networkId) {
|
||||||
const { name, description, color } = updatedCalendar;
|
const { name, description, color } = updatedCalendar;
|
||||||
try {
|
try {
|
||||||
const [calendar] = await this.sql`
|
const [calendar] = await context.sql`
|
||||||
UPDATE events.calendars
|
UPDATE events.calendars
|
||||||
SET
|
SET
|
||||||
name = ${name},
|
name = ${name},
|
||||||
@@ -87,7 +89,8 @@ export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
|||||||
WHERE
|
WHERE
|
||||||
id = ${id}
|
id = ${id}
|
||||||
AND network_id = ${networkId}
|
AND network_id = ${networkId}
|
||||||
AND owner_id = ${userId}
|
--@ Calendar editing is owner-only on the backend.
|
||||||
|
AND owner_id = ${context.userId}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -99,11 +102,12 @@ export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCalendar(userId, id, networkId) {
|
export async function deleteCalendar(id, networkId) {
|
||||||
try {
|
try {
|
||||||
const [calendar] = await this.sql`
|
//@ Calendar deletion is owner-only on the backend.
|
||||||
|
const [calendar] = await context.sql`
|
||||||
DELETE FROM events.calendars
|
DELETE FROM events.calendars
|
||||||
WHERE id = ${id} AND network_id = ${networkId} AND owner_id = ${userId}
|
WHERE id = ${id} AND network_id = ${networkId} AND owner_id = ${context.userId}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
if (!calendar) return { status: 403, error: "Calendar not found or not authorized" };
|
if (!calendar) return { status: 403, error: "Calendar not found or not authorized" };
|
||||||
@@ -114,12 +118,12 @@ export async function deleteCalendar(userId, id, networkId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addEvent(userId, newEvent, networkId) {
|
export async function addEvent(newEvent, networkId) {
|
||||||
const { title, description, location, time_start, time_end, all_day, calendars, recurrence } = newEvent;
|
const { title, description, location, time_start, time_end, all_day, calendars, recurrence } = newEvent;
|
||||||
try {
|
try {
|
||||||
let insertedEvent;
|
let insertedEvent;
|
||||||
|
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
let recurrenceId = null;
|
let recurrenceId = null;
|
||||||
if (recurrence) {
|
if (recurrence) {
|
||||||
const [rule] = await tx`
|
const [rule] = await tx`
|
||||||
@@ -137,7 +141,7 @@ export async function addEvent(userId, newEvent, networkId) {
|
|||||||
const [event] = await tx`
|
const [event] = await tx`
|
||||||
INSERT INTO events.events ${tx({
|
INSERT INTO events.events ${tx({
|
||||||
network_id: networkId,
|
network_id: networkId,
|
||||||
creator_id: userId,
|
creator_id: context.userId,
|
||||||
title,
|
title,
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
location: location ?? null,
|
location: location ?? null,
|
||||||
@@ -162,18 +166,30 @@ export async function addEvent(userId, newEvent, networkId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function networkFromEvent(eventId) {
|
||||||
|
return await context.sql`
|
||||||
|
SELECT c.network_id
|
||||||
|
FROM events.event_calendars ec
|
||||||
|
JOIN events.calendars c ON c.id = ec.calendar_id
|
||||||
|
WHERE ec.event_id = ${eventId};
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export async function addAttachments(userId, eventId, files) {
|
export async function addAttachments(userId, eventId, files) {
|
||||||
const [event] = await this.sql`
|
let results = await networkFromEvent(eventId)
|
||||||
|
if(!(await global.permissions.canDo("events.edit", userId, results[0].network_id))) return { status: 403, error: "Forbidden" };
|
||||||
|
const [event] = await context.sql`
|
||||||
SELECT id FROM events.events
|
SELECT id FROM events.events
|
||||||
WHERE id = ${eventId} AND creator_id = ${userId}
|
WHERE id = ${eventId} AND creator_id = ${userId}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!event) throw new global.ServerError(403, "Not authorized to add attachments to this event");
|
if (!event) throw new global.ServerError(403, "Not authorized to add attachments to this event");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let insertedFiles = [];
|
let insertedFiles = [];
|
||||||
|
|
||||||
// Will rollback changes if it encounters an error
|
// Will rollback changes if it encounters an error
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
insertedFiles = await Promise.all(
|
insertedFiles = await Promise.all(
|
||||||
files.map(file =>
|
files.map(file =>
|
||||||
tx`
|
tx`
|
||||||
@@ -207,13 +223,16 @@ export async function addAttachments(userId, eventId, files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAttachment(userId, fileId, eventId) {
|
export async function deleteAttachment(userId, fileId, eventId) {
|
||||||
const [event] = await this.sql`
|
let results = await networkFromEvent(eventId)
|
||||||
|
if(!(await global.permissions.canDo("events.edit", userId, results[0].network_id))) return { status: 403, error: "Forbidden" };
|
||||||
|
|
||||||
|
const [event] = await context.sql`
|
||||||
SELECT id FROM events.events
|
SELECT id FROM events.events
|
||||||
WHERE id = ${eventId} AND creator_id = ${userId}
|
WHERE id = ${eventId} AND creator_id = ${userId}
|
||||||
`;
|
`;
|
||||||
if (!event) throw new global.ServerError(403, "Not authorized to modify this event");
|
if (!event) throw new global.ServerError(403, "Not authorized to modify this event");
|
||||||
|
|
||||||
const [file] = await this.sql`
|
const [file] = await context.sql`
|
||||||
SELECT f.* FROM public.files f
|
SELECT f.* FROM public.files f
|
||||||
JOIN public.file_to_event fte ON fte.file_id = f.id
|
JOIN public.file_to_event fte ON fte.file_id = f.id
|
||||||
WHERE f.id = ${fileId} AND fte.event_id = ${eventId}
|
WHERE f.id = ${fileId} AND fte.event_id = ${eventId}
|
||||||
@@ -222,7 +241,7 @@ export async function deleteAttachment(userId, fileId, eventId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let deletedFile = null;
|
let deletedFile = null;
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
await tx`DELETE FROM public.file_to_event WHERE file_id = ${fileId} AND event_id = ${eventId}`;
|
await tx`DELETE FROM public.file_to_event WHERE file_id = ${fileId} AND event_id = ${eventId}`;
|
||||||
// Only remove the file record (and trigger disk unlink) if no other event still references it
|
// Only remove the file record (and trigger disk unlink) if no other event still references it
|
||||||
const [stillReferenced] = await tx`SELECT 1 FROM public.file_to_event WHERE file_id = ${fileId} LIMIT 1`;
|
const [stillReferenced] = await tx`SELECT 1 FROM public.file_to_event WHERE file_id = ${fileId} LIMIT 1`;
|
||||||
@@ -238,16 +257,17 @@ export async function deleteAttachment(userId, fileId, eventId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editEvent(userId, id, updatedEvent, networkId) {
|
export async function editEvent(id, updatedEvent, networkId) {
|
||||||
|
if(!(await global.permissions.canDo("events.edit", context.userId, networkId))) return { status: 403, error: "Forbidden" };
|
||||||
const { title, description, location, time_start, time_end, all_day, calendars, recurrence, scope, exception_date } = updatedEvent;
|
const { title, description, location, time_start, time_end, all_day, calendars, recurrence, scope, exception_date } = updatedEvent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let editedEvent;
|
let editedEvent;
|
||||||
|
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
const [current] = await tx`
|
const [current] = await tx`
|
||||||
SELECT * FROM events.events
|
SELECT * FROM events.events
|
||||||
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${userId}
|
WHERE id = ${id} AND network_id = ${networkId}
|
||||||
`;
|
`;
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
|
|
||||||
@@ -299,7 +319,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
|||||||
all_day = ${all_day},
|
all_day = ${all_day},
|
||||||
recurrence_id = ${newRecurrenceId},
|
recurrence_id = ${newRecurrenceId},
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${userId}
|
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${context.userId}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
@@ -321,7 +341,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
|||||||
const [override] = await tx`
|
const [override] = await tx`
|
||||||
INSERT INTO events.events ${tx({
|
INSERT INTO events.events ${tx({
|
||||||
network_id: networkId,
|
network_id: networkId,
|
||||||
creator_id: userId,
|
creator_id: context.userId,
|
||||||
title,
|
title,
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
location: location ?? null,
|
location: location ?? null,
|
||||||
@@ -410,7 +430,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
|||||||
const [newEvent] = await tx`
|
const [newEvent] = await tx`
|
||||||
INSERT INTO events.events ${tx({
|
INSERT INTO events.events ${tx({
|
||||||
network_id: networkId,
|
network_id: networkId,
|
||||||
creator_id: userId,
|
creator_id: context.userId,
|
||||||
title,
|
title,
|
||||||
description: description ?? null,
|
description: description ?? null,
|
||||||
location: location ?? null,
|
location: location ?? null,
|
||||||
@@ -453,11 +473,12 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEvent(userId, eventId, networkId, scope, exception_date) {
|
export async function deleteEvent(eventId, networkId, scope, exception_date) {
|
||||||
|
if(!global.permissions.canDo("events.delete", context.userId, networkId)) return { status: 403, error: "Forbidden" };
|
||||||
try {
|
try {
|
||||||
const [event] = await this.sql`
|
const [event] = await context.sql`
|
||||||
SELECT * FROM events.events
|
SELECT * FROM events.events
|
||||||
WHERE id = ${eventId} AND network_id = ${networkId} AND creator_id = ${userId}
|
WHERE id = ${eventId} AND network_id = ${networkId}
|
||||||
`;
|
`;
|
||||||
if (!event) return { status: 403, error: "Event not found or not authorized" };
|
if (!event) return { status: 403, error: "Event not found or not authorized" };
|
||||||
|
|
||||||
@@ -466,16 +487,16 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
|||||||
|
|
||||||
if (isOverride) {
|
if (isOverride) {
|
||||||
// Already a concrete row — just mark it cancelled
|
// Already a concrete row — just mark it cancelled
|
||||||
await this.sql`UPDATE events.events SET is_cancelled = true WHERE id = ${eventId}`;
|
await context.sql`UPDATE events.events SET is_cancelled = true WHERE id = ${eventId}`;
|
||||||
return { status: 200 };
|
return { status: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRecurring && scope === 'single') {
|
if (isRecurring && scope === 'single') {
|
||||||
// Virtual occurrence — insert a cancelled placeholder so it gets skipped
|
// Virtual occurrence — insert a cancelled placeholder so it gets skipped
|
||||||
await this.sql`
|
await context.sql`
|
||||||
INSERT INTO events.events ${this.sql({
|
INSERT INTO events.events ${context.sql({
|
||||||
network_id: networkId,
|
network_id: networkId,
|
||||||
creator_id: userId,
|
creator_id: context.userId,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
time_start: exception_date,
|
time_start: exception_date,
|
||||||
time_end: exception_date,
|
time_end: exception_date,
|
||||||
@@ -490,16 +511,16 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
|||||||
|
|
||||||
if (isRecurring && scope === 'future' && new Date(exception_date) > new Date(event.time_start)) {
|
if (isRecurring && scope === 'future' && new Date(exception_date) > new Date(event.time_start)) {
|
||||||
// Capture current end_date before modifying — marks the boundary of independent later splits
|
// Capture current end_date before modifying — marks the boundary of independent later splits
|
||||||
const [existingRule] = await this.sql`SELECT end_date FROM events.event_recurrence WHERE id = ${event.recurrence_id}`;
|
const [existingRule] = await context.sql`SELECT end_date FROM events.event_recurrence WHERE id = ${event.recurrence_id}`;
|
||||||
const oldEndDate = existingRule?.end_date ?? null;
|
const oldEndDate = existingRule?.end_date ?? null;
|
||||||
|
|
||||||
// Cap the series at this date
|
// Cap the series at this date
|
||||||
await this.sql`
|
await context.sql`
|
||||||
UPDATE events.event_recurrence SET end_date = ${exception_date}, updated_at = NOW()
|
UPDATE events.event_recurrence SET end_date = ${exception_date}, updated_at = NOW()
|
||||||
WHERE id = ${event.recurrence_id}
|
WHERE id = ${event.recurrence_id}
|
||||||
`;
|
`;
|
||||||
// Promote future non-cancelled overrides (single-event edits) to standalone events
|
// Promote future non-cancelled overrides (single-event edits) to standalone events
|
||||||
await this.sql`
|
await context.sql`
|
||||||
UPDATE events.events
|
UPDATE events.events
|
||||||
SET recurrence_parent_id = NULL, recurrence_exception_date = NULL, updated_at = NOW()
|
SET recurrence_parent_id = NULL, recurrence_exception_date = NULL, updated_at = NOW()
|
||||||
WHERE recurrence_parent_id = ${eventId}
|
WHERE recurrence_parent_id = ${eventId}
|
||||||
@@ -507,24 +528,24 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
|||||||
AND (is_cancelled IS NULL OR is_cancelled = false)
|
AND (is_cancelled IS NULL OR is_cancelled = false)
|
||||||
`;
|
`;
|
||||||
// Drop future cancelled placeholders — they are meaningless without the series
|
// Drop future cancelled placeholders — they are meaningless without the series
|
||||||
await this.sql`
|
await context.sql`
|
||||||
DELETE FROM events.events
|
DELETE FROM events.events
|
||||||
WHERE recurrence_parent_id = ${eventId} AND recurrence_exception_date >= ${exception_date}
|
WHERE recurrence_parent_id = ${eventId} AND recurrence_exception_date >= ${exception_date}
|
||||||
AND is_cancelled = true
|
AND is_cancelled = true
|
||||||
`;
|
`;
|
||||||
// Remove intermediate descendant templates in [exception_date, oldEndDate); independent splits at/beyond oldEndDate are preserved
|
// Remove intermediate descendant templates in [exception_date, oldEndDate); independent splits at/beyond oldEndDate are preserved
|
||||||
const descendants = await this.sql`
|
const descendants = await context.sql`
|
||||||
SELECT id, recurrence_id FROM events.events
|
SELECT id, recurrence_id FROM events.events
|
||||||
WHERE parent_template_id = ${eventId}
|
WHERE parent_template_id = ${eventId}
|
||||||
AND time_start >= ${exception_date}
|
AND time_start >= ${exception_date}
|
||||||
${oldEndDate ? this.sql`AND time_start < ${oldEndDate}` : this.sql``}
|
${oldEndDate ? context.sql`AND time_start < ${oldEndDate}` : context.sql``}
|
||||||
`;
|
`;
|
||||||
if (descendants.length > 0) {
|
if (descendants.length > 0) {
|
||||||
const descIds = descendants.map(d => d.id);
|
const descIds = descendants.map(d => d.id);
|
||||||
const descRuleIds = descendants.map(d => d.recurrence_id).filter(Boolean);
|
const descRuleIds = descendants.map(d => d.recurrence_id).filter(Boolean);
|
||||||
await this.sql`DELETE FROM events.events WHERE id = ANY(${descIds})`;
|
await context.sql`DELETE FROM events.events WHERE id = ANY(${descIds})`;
|
||||||
if (descRuleIds.length > 0) {
|
if (descRuleIds.length > 0) {
|
||||||
await this.sql`DELETE FROM events.event_recurrence WHERE id = ANY(${descRuleIds})`;
|
await context.sql`DELETE FROM events.event_recurrence WHERE id = ANY(${descRuleIds})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { status: 200 };
|
return { status: 200 };
|
||||||
@@ -533,7 +554,7 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
|||||||
// Delete the whole event/series — collect files from the template AND all overrides,
|
// Delete the whole event/series — collect files from the template AND all overrides,
|
||||||
// but only those not shared with surviving events (e.g. a 'this and future' split
|
// but only those not shared with surviving events (e.g. a 'this and future' split
|
||||||
// inherits the same file_ids; deleting the physical file would orphan that series)
|
// inherits the same file_ids; deleting the physical file would orphan that series)
|
||||||
const files = await this.sql`
|
const files = await context.sql`
|
||||||
SELECT DISTINCT f.* FROM public.files f
|
SELECT DISTINCT f.* FROM public.files f
|
||||||
JOIN public.file_to_event fte ON fte.file_id = f.id
|
JOIN public.file_to_event fte ON fte.file_id = f.id
|
||||||
JOIN events.events e ON e.id = fte.event_id
|
JOIN events.events e ON e.id = fte.event_id
|
||||||
@@ -548,7 +569,7 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
|||||||
`;
|
`;
|
||||||
const recurrenceId = event.recurrence_id;
|
const recurrenceId = event.recurrence_id;
|
||||||
|
|
||||||
await this.sql.begin(async tx => {
|
await context.sql.begin(async tx => {
|
||||||
// Promote non-cancelled overrides (single-event edits) to standalone events before deleting the template
|
// Promote non-cancelled overrides (single-event edits) to standalone events before deleting the template
|
||||||
await tx`
|
await tx`
|
||||||
UPDATE events.events
|
UPDATE events.events
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export async function getMoneyData(networkId) {
|
export async function getMoneyData(networkId) {
|
||||||
const purchases = await this.sql`
|
const purchases = await context.sql`
|
||||||
SELECT * FROM purchases
|
SELECT * FROM purchases
|
||||||
WHERE network_id = ${networkId}
|
WHERE network_id = ${networkId}
|
||||||
ORDER BY created DESC
|
ORDER BY created DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const subscriptions = await this.sql`
|
const subscriptions = await context.sql`
|
||||||
SELECT mn.id, mn.created, mn.active, mn.network_plan_id,
|
SELECT mn.id, mn.created, mn.active, mn.network_plan_id,
|
||||||
np.name AS plan_name, np.price AS plan_price,
|
np.name AS plan_name, np.price AS plan_price,
|
||||||
m.first_name, m.last_name, m.email
|
m.first_name, m.last_name, m.email
|
||||||
|
|||||||
4
notes.txt
Normal file
4
notes.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
reset permissions
|
||||||
|
|
||||||
|
sudo chown -R test:staff /Users/Shared/apps
|
||||||
|
chmod -R ug+w /Users/Shared/apps
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export async function saveMemberNote(email, text) {
|
export async function saveMemberNote(email, text) {
|
||||||
console.log("saving note: ", email, text)
|
console.log("saving note: ", email, text)
|
||||||
try {
|
try {
|
||||||
await this.sql`
|
await context.sql`
|
||||||
UPDATE members
|
UPDATE members
|
||||||
SET notes = ${text}
|
SET notes = ${text}
|
||||||
WHERE email = ${email}
|
WHERE email = ${email}
|
||||||
@@ -14,7 +14,7 @@ export async function saveMemberNote(email, text) {
|
|||||||
|
|
||||||
export async function getPeople(networkId) {
|
export async function getPeople(networkId) {
|
||||||
try {
|
try {
|
||||||
const people = await this.sql`
|
const people = await context.sql`
|
||||||
SELECT
|
SELECT
|
||||||
m.*,
|
m.*,
|
||||||
mn.created AS joined_network,
|
mn.created AS joined_network,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export async function getTasks(networkId) {
|
export async function getTasks(networkId) {
|
||||||
const tasks = await this.sql`
|
const tasks = await context.sql`
|
||||||
SELECT * FROM tasks.tasks
|
SELECT * FROM tasks.tasks
|
||||||
WHERE network_id = ${networkId}
|
WHERE network_id = ${networkId}
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
@@ -9,7 +9,7 @@ export async function getTasks(networkId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addTask(networkId, title) {
|
export async function addTask(networkId, title) {
|
||||||
const [task] = await this.sql`
|
const [task] = await context.sql`
|
||||||
INSERT INTO tasks.tasks (title, network_id)
|
INSERT INTO tasks.tasks (title, network_id)
|
||||||
VALUES (${title}, ${networkId})
|
VALUES (${title}, ${networkId})
|
||||||
RETURNING *
|
RETURNING *
|
||||||
@@ -18,7 +18,7 @@ export async function addTask(networkId, title) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTaskDone(taskId, done) {
|
export async function updateTaskDone(taskId, done) {
|
||||||
const [task] = await this.sql`
|
const [task] = await context.sql`
|
||||||
UPDATE tasks.tasks
|
UPDATE tasks.tasks
|
||||||
SET done = ${done}
|
SET done = ${done}
|
||||||
WHERE id = ${taskId}
|
WHERE id = ${taskId}
|
||||||
@@ -28,7 +28,7 @@ export async function updateTaskDone(taskId, done) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function editTaskTitle(taskId, title) {
|
export async function editTaskTitle(taskId, title) {
|
||||||
const [task] = await this.sql`
|
const [task] = await context.sql`
|
||||||
UPDATE tasks.tasks
|
UPDATE tasks.tasks
|
||||||
SET title = ${title}
|
SET title = ${title}
|
||||||
WHERE id = ${taskId}
|
WHERE id = ${taskId}
|
||||||
@@ -38,7 +38,7 @@ export async function editTaskTitle(taskId, title) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTask(taskId) {
|
export async function deleteTask(taskId) {
|
||||||
await this.sql`
|
await context.sql`
|
||||||
UPDATE tasks.tasks
|
UPDATE tasks.tasks
|
||||||
SET is_active = false
|
SET is_active = false
|
||||||
WHERE id = ${taskId}
|
WHERE id = ${taskId}
|
||||||
|
|||||||
Reference in New Issue
Block a user