Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Manage is an open-source project management platform. With its intuitive interfa
- [x] Activity logs
- [x] Search
- [x] Permissions
- [ ] Notifications
- [x] Notifications
- [ ] Posts & files

## Development
Expand Down
54 changes: 26 additions & 28 deletions app/(api)/api/jobs/account/mark-for-deletion/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { clerkClient } from "@clerk/nextjs/server";
import { serve } from "@upstash/workflow/nextjs";
import { and, eq, isNull, lte } from "drizzle-orm";
import { Resend } from "resend";
import {
DeletionNoticePlainText,
OrgDeletionNotice,
} from "@/components/emails/org-deletion-notice";
import {
SevenDayWarning,
sevenDayWarningPlainText,
} from "@/components/emails/seven-day-warning";
import {
ThirtyDayDeletionNotice,
thirtyDayDeletionNoticePlainText,
} from "@/components/emails/thirty-day-deletion-notice";
import { opsOrganization } from "@/ops/drizzle/schema";
import { getOpsDatabase } from "@/ops/useOps";
import { clerkClient } from "@clerk/nextjs/server";
import { serve } from "@upstash/workflow/nextjs";
import { and, eq, isNull, lte } from "drizzle-orm";
import { Resend } from "resend";

type ClerkOrgData = {
createdBy?: string;
Expand Down Expand Up @@ -66,20 +66,20 @@ export const { POST } = serve(async (context) => {
new Date().toISOString(),
);
const db = await getOpsDatabase();
const thirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60);
console.log(
"[OrgDeletion] Looking for orgs inactive since:",
thirtyDaysAgo.toISOString(),
sixtyDaysAgo.toISOString(),
);

// Step 1: Mark organizations for deletion (30 days inactive)
// Step 1: Mark organizations for deletion (60 days inactive)
const orgsToMark = await context.run("fetch-orgs-to-mark", async () => {
const orgs = await db
.select()
.from(opsOrganization)
.where(
and(
lte(opsOrganization.lastActiveAt, thirtyDaysAgo),
lte(opsOrganization.lastActiveAt, sixtyDaysAgo),
isNull(opsOrganization.markedForDeletionAt),
),
);
Expand All @@ -104,7 +104,7 @@ export const { POST } = serve(async (context) => {
return orgs;
});

// Step 2: Send 30-day deletion notice and mark organizations
// Step 2: Send 60-day deletion notice and mark organizations
await context.run("mark-orgs-for-deletion", async () => {
if (orgsToMark.length === 0) {
console.log(
Expand All @@ -114,7 +114,7 @@ export const { POST } = serve(async (context) => {
}

console.log(
`[OrgDeletion] Processing ${orgsToMark.length} organizations for 30-day deletion notices`,
`[OrgDeletion] Processing ${orgsToMark.length} organizations for 60-day deletion notices`,
);
for (const org of orgsToMark) {
try {
Expand All @@ -129,20 +129,20 @@ export const { POST } = serve(async (context) => {
JSON.stringify(org.rawData, null, 2),
);

// Send 30-day deletion notice
// Send 60-day deletion notice
console.log(
`[OrgDeletion] Sending 30-day notice email to ${contactEmail} for org ${org.name}`,
`[OrgDeletion] Sending 60-day notice email to ${contactEmail} for org ${org.name}`,
);
const emailResult = await resend.emails.send({
from: "Manage Team <noreply@email.managee.xyz>",
to: contactEmail,
subject: "Organization Deletion Notice - 30 Days",
react: ThirtyDayDeletionNotice({
subject: "Organization Deletion Notice - 60 Days",
react: OrgDeletionNotice({
firstName: firstName,
email: contactEmail,
organizationName: org.name,
}),
text: thirtyDayDeletionNoticePlainText({
text: DeletionNoticePlainText({
firstName: firstName,
email: contactEmail,
organizationName: org.name,
Expand Down Expand Up @@ -171,19 +171,17 @@ export const { POST } = serve(async (context) => {
}
});

// Step 3: Send 7-day warning to organizations marked 23 days ago
// Step 3: Send 7-day warning to organizations marked 53 days ago
const orgsFor7DayWarning = await context.run(
"fetch-orgs-for-7-day-warning",
async () => {
const twentyThreeDaysAgo = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 23,
);
const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53);
const orgs = await db
.select()
.from(opsOrganization)
.where(
and(
lte(opsOrganization.markedForDeletionAt, twentyThreeDaysAgo),
lte(opsOrganization.markedForDeletionAt, fiftyThreeDaysAgo),
isNull(opsOrganization.finalWarningAt),
),
);
Expand Down Expand Up @@ -271,18 +269,18 @@ export const { POST } = serve(async (context) => {
}
});

// Step 4: Trigger deletion for organizations marked 30 days ago
// Step 4: Trigger deletion for organizations marked 60 days ago
const orgsToTriggerDeletion = await context.run(
"fetch-orgs-to-trigger-deletion",
async () => {
const thirtyDaysAgoForDeletion = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 30,
const sixtyDaysAgoForDeletion = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 60,
);
const orgs = await db
.select()
.from(opsOrganization)
.where(
lte(opsOrganization.markedForDeletionAt, thirtyDaysAgoForDeletion),
lte(opsOrganization.markedForDeletionAt, sixtyDaysAgoForDeletion),
);

console.log(
Expand Down
46 changes: 22 additions & 24 deletions app/(api)/api/jobs/user/mark-for-deletion/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { clerkClient } from "@clerk/nextjs/server";
import { serve } from "@upstash/workflow/nextjs";
import { and, eq, isNull, lte } from "drizzle-orm";
import { Resend } from "resend";
import {
DeletionNoticePlainText,
OrgDeletionNotice,
} from "@/components/emails/org-deletion-notice";
import {
SevenDayWarning,
sevenDayWarningPlainText,
} from "@/components/emails/seven-day-warning";
import {
ThirtyDayDeletionNotice,
thirtyDayDeletionNoticePlainText,
} from "@/components/emails/thirty-day-deletion-notice";
import { opsUser } from "@/ops/drizzle/schema";
import { getOpsDatabase } from "@/ops/useOps";

Expand Down Expand Up @@ -67,20 +67,20 @@ export const { POST } = serve(async (context) => {
new Date().toISOString(),
);
const db = await getOpsDatabase();
const thirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
const sixtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 60);
console.log(
"[UserDeletion] Looking for users inactive since:",
thirtyDaysAgo.toISOString(),
sixtyDaysAgo.toISOString(),
);

// Step 1: Mark users for deletion (30 days inactive, not part of any org)
// Step 1: Mark users for deletion (60 days inactive, not part of any org)
const usersToMark = await context.run("fetch-users-to-mark", async () => {
const users = await db
.select()
.from(opsUser)
.where(
and(
lte(opsUser.lastActiveAt, thirtyDaysAgo),
lte(opsUser.lastActiveAt, sixtyDaysAgo),
isNull(opsUser.markedForDeletionAt),
),
);
Expand Down Expand Up @@ -122,7 +122,7 @@ export const { POST } = serve(async (context) => {
return eligibleUsers;
});

// Step 2: Send 30-day deletion notice and mark users
// Step 2: Send 60-day deletion notice and mark users
await context.run("mark-users-for-deletion", async () => {
if (usersToMark.length === 0) {
console.log(
Expand All @@ -132,7 +132,7 @@ export const { POST } = serve(async (context) => {
}

console.log(
`[UserDeletion] Processing ${usersToMark.length} users for 30-day deletion notices`,
`[UserDeletion] Processing ${usersToMark.length} users for 60-day deletion notices`,
);
for (const user of usersToMark) {
try {
Expand All @@ -143,20 +143,20 @@ export const { POST } = serve(async (context) => {
`[UserDeletion] Processing user ${user.id}, contact email: ${contactEmail}`,
);

// Send 30-day deletion notice
// Send 60-day deletion notice
console.log(
`[UserDeletion] Sending 30-day notice email to ${contactEmail} for user ${user.id}`,
`[UserDeletion] Sending 60-day notice email to ${contactEmail} for user ${user.id}`,
);
const emailResult = await resend.emails.send({
from: "Manage Team <noreply@email.managee.xyz>",
to: contactEmail,
subject: "Account Deletion Notice - 30 Days",
react: ThirtyDayDeletionNotice({
subject: "Account Deletion Notice - 60 Days",
react: OrgDeletionNotice({
firstName: firstName,
email: contactEmail,
// organizationName is undefined for user deletion
}),
text: thirtyDayDeletionNoticePlainText({
text: DeletionNoticePlainText({
firstName: firstName,
email: contactEmail,
// organizationName is undefined for user deletion
Expand Down Expand Up @@ -185,19 +185,17 @@ export const { POST } = serve(async (context) => {
}
});

// Step 3: Send 7-day warning to users marked 23 days ago
// Step 3: Send 7-day warning to users marked 53 days ago
const usersFor7DayWarning = await context.run(
"fetch-users-for-7-day-warning",
async () => {
const twentyThreeDaysAgo = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 23,
);
const fiftyThreeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 53);
const users = await db
.select()
.from(opsUser)
.where(
and(
lte(opsUser.markedForDeletionAt, twentyThreeDaysAgo),
lte(opsUser.markedForDeletionAt, fiftyThreeDaysAgo),
isNull(opsUser.finalWarningAt),
),
);
Expand Down Expand Up @@ -283,17 +281,17 @@ export const { POST } = serve(async (context) => {
}
});

// Step 4: Trigger deletion for users marked 30 days ago
// Step 4: Trigger deletion for users marked 60 days ago
const usersToTriggerDeletion = await context.run(
"fetch-users-to-trigger-deletion",
async () => {
const thirtyDaysAgoForDeletion = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 30,
const sixtyDaysAgoForDeletion = new Date(
Date.now() - 1000 * 60 * 60 * 24 * 60,
);
const users = await db
.select()
.from(opsUser)
.where(lte(opsUser.markedForDeletionAt, thirtyDaysAgoForDeletion));
.where(lte(opsUser.markedForDeletionAt, sixtyDaysAgoForDeletion));

console.log(
`[UserDeletion] Found ${users.length} users ready for deletion`,
Expand Down
42 changes: 7 additions & 35 deletions app/(dashboard)/[tenant]/today/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SaveButton } from "@/components/form/button";
import SharedForm from "@/components/form/shared";
import PageTitle from "@/components/layout/page-title";
import { ProjecItem } from "@/components/project/project-item";
import { TaskItem } from "@/components/today/task-item";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { toDateStringWithDay } from "@/lib/utils/date";
Expand Down Expand Up @@ -151,7 +152,9 @@ export default function Today() {
titleClassName="text-red-600 dark:text-red-500"
titleIcon={<AlertTriangleIcon className="w-5 h-5" />}
>
{overDue.map((task) => TaskItem(tenant, task))}
{overDue.map((task) => (
<TaskItem key={task.id} tenant={tenant} task={task} />
))}
</PageSection>
) : null}

Expand All @@ -161,7 +164,9 @@ export default function Today() {
titleClassName="text-orange-600 dark:text-orange-500"
titleIcon={<InfoIcon className="w-5 h-5" />}
>
{dueToday.map((task) => TaskItem(tenant, task))}
{dueToday.map((task) => (
<TaskItem key={task.id} tenant={tenant} task={task} />
))}
</PageSection>
) : null}

Expand Down Expand Up @@ -267,36 +272,3 @@ export default function Today() {
</>
);
}

function TaskItem(
tenant: string,
task: {
name: string;
id: number;
taskList: {
id: number;
name: string;
status: string;
project: { id: number; name: string };
};
},
) {
return (
<Link
href={`/${tenant}/projects/${task.taskList.project.id}/tasklists/${task.taskList.id}`}
key={task.id}
>
<div className="px-4 py-2 hover:bg-muted/50 transition-colors border-none">
<div className="flex items-start justify-between">
<div className="space-y-1">
<h4 className="font-medium">{task.name}</h4>
<div className="text-sm text-muted-foreground">
<span className="text-primary">{task.taskList.project.name}</span>{" "}
• {task.taskList.name}
</div>
</div>
</div>
</div>
</Link>
);
}
Loading