Add activity detail, Jalali pickers, and customer/user profiles.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ActivityPriority" AS ENUM ('low', 'medium', 'high');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Activity" ADD COLUMN "priority" "ActivityPriority" NOT NULL DEFAULT 'medium';
|
||||
ALTER TABLE "Activity" ADD COLUMN "createdById" TEXT;
|
||||
|
||||
UPDATE "Activity" SET "createdById" = "assigneeId" WHERE "createdById" IS NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Activity_organizationId_createdById_idx" ON "Activity"("organizationId", "createdById");
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ActivityPriority" ADD VALUE 'very_low';
|
||||
ALTER TYPE "ActivityPriority" ADD VALUE 'very_high';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Activity" ADD COLUMN "durationMinutes" INTEGER;
|
||||
ALTER TABLE "Activity" ADD COLUMN "reminderAt" TIMESTAMP(3);
|
||||
@@ -33,6 +33,14 @@ enum ActivityStatus {
|
||||
canceled
|
||||
}
|
||||
|
||||
enum ActivityPriority {
|
||||
low
|
||||
medium
|
||||
high
|
||||
very_low
|
||||
very_high
|
||||
}
|
||||
|
||||
enum CustomFieldEntity {
|
||||
person
|
||||
company
|
||||
@@ -165,6 +173,7 @@ model User {
|
||||
ownedCompanies Company[] @relation("CompanyOwner")
|
||||
ownedProjectCards ProjectCard[] @relation("ProjectCardOwner")
|
||||
activities Activity[] @relation("ActivityAssignee")
|
||||
createdActivities Activity[] @relation("ActivityCreator")
|
||||
notes Note[] @relation("NoteAuthor")
|
||||
noteAttachments NoteAttachment[] @relation("NoteAttachmentUploader")
|
||||
notifications Notification[]
|
||||
@@ -333,10 +342,13 @@ model Activity {
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
type ActivityType @default(task)
|
||||
status ActivityStatus @default(open)
|
||||
title String
|
||||
description String?
|
||||
dueAt DateTime?
|
||||
completedAt DateTime?
|
||||
priority ActivityPriority @default(medium)
|
||||
title String
|
||||
description String?
|
||||
dueAt DateTime?
|
||||
durationMinutes Int?
|
||||
reminderAt DateTime?
|
||||
completedAt DateTime?
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
|
||||
companyId String?
|
||||
@@ -345,11 +357,14 @@ model Activity {
|
||||
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
|
||||
assigneeId String?
|
||||
assignee User? @relation("ActivityAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
|
||||
createdById String?
|
||||
createdBy User? @relation("ActivityCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId, status, dueAt])
|
||||
@@index([organizationId, assigneeId])
|
||||
@@index([organizationId, createdById])
|
||||
@@index([personId])
|
||||
@@index([companyId])
|
||||
@@index([dealId])
|
||||
|
||||
@@ -22,9 +22,12 @@ type ActivityRow = {
|
||||
id: string;
|
||||
type: ActivityDto['type'];
|
||||
status: ActivityDto['status'];
|
||||
priority: ActivityDto['priority'];
|
||||
title: string;
|
||||
description: string | null;
|
||||
dueAt: Date | null;
|
||||
durationMinutes: number | null;
|
||||
reminderAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -32,6 +35,7 @@ type ActivityRow = {
|
||||
company: { id: string; name: string } | null;
|
||||
deal: { id: string; title: string } | null;
|
||||
assignee: { id: string; fullName: string } | null;
|
||||
createdBy: { id: string; fullName: string } | null;
|
||||
};
|
||||
|
||||
const include = {
|
||||
@@ -39,6 +43,7 @@ const include = {
|
||||
company: { select: { id: true, name: true } },
|
||||
deal: { select: { id: true, title: true } },
|
||||
assignee: { select: { id: true, fullName: true } },
|
||||
createdBy: { select: { id: true, fullName: true } },
|
||||
} satisfies Prisma.ActivityInclude;
|
||||
|
||||
@Injectable()
|
||||
@@ -173,12 +178,16 @@ export class ActivitiesService {
|
||||
organizationId,
|
||||
title: dto.title.trim(),
|
||||
type: dto.type ?? 'task',
|
||||
priority: dto.priority ?? 'medium',
|
||||
description: emptyToNull(dto.description),
|
||||
dueAt: dto.dueAt ? new Date(dto.dueAt) : null,
|
||||
durationMinutes: dto.durationMinutes ?? null,
|
||||
reminderAt: dto.reminderAt ? new Date(dto.reminderAt) : null,
|
||||
personId: emptyToNull(dto.personId ?? null),
|
||||
companyId: emptyToNull(dto.companyId ?? null),
|
||||
dealId: emptyToNull(dto.dealId ?? null),
|
||||
assigneeId,
|
||||
createdById: userId,
|
||||
},
|
||||
include,
|
||||
});
|
||||
@@ -211,8 +220,11 @@ export class ActivitiesService {
|
||||
const data: Prisma.ActivityUncheckedUpdateInput = {};
|
||||
if (dto.title !== undefined) data.title = dto.title.trim();
|
||||
if (dto.type !== undefined) data.type = dto.type;
|
||||
if (dto.priority !== undefined) data.priority = dto.priority;
|
||||
if ('description' in dto) data.description = emptyToNull(dto.description);
|
||||
if ('dueAt' in dto) data.dueAt = dto.dueAt ? new Date(dto.dueAt) : null;
|
||||
if ('durationMinutes' in dto) data.durationMinutes = dto.durationMinutes ?? null;
|
||||
if ('reminderAt' in dto) data.reminderAt = dto.reminderAt ? new Date(dto.reminderAt) : null;
|
||||
if ('personId' in dto) data.personId = emptyToNull(dto.personId ?? null);
|
||||
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
|
||||
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
|
||||
@@ -394,9 +406,12 @@ export class ActivitiesService {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
priority: row.priority,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
dueAt: row.dueAt?.toISOString() ?? null,
|
||||
durationMinutes: row.durationMinutes,
|
||||
reminderAt: row.reminderAt?.toISOString() ?? null,
|
||||
completedAt: row.completedAt?.toISOString() ?? null,
|
||||
person: row.person
|
||||
? {
|
||||
@@ -407,6 +422,7 @@ export class ActivitiesService {
|
||||
company: row.company,
|
||||
deal: row.deal,
|
||||
assignee: row.assignee,
|
||||
createdBy: row.createdBy,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
const ACTIVITY_TYPES = ['call', 'meeting', 'task', 'note'] as const;
|
||||
const ACTIVITY_STATUSES = ['open', 'done', 'canceled'] as const;
|
||||
const ACTIVITY_PRIORITIES = ['very_low', 'low', 'medium', 'high', 'very_high'] as const;
|
||||
const ACTIVITY_FILTERS = ['today', 'overdue', 'upcoming', 'done', 'all'] as const;
|
||||
|
||||
export class ListActivitiesQueryDto {
|
||||
@@ -71,6 +72,10 @@ export class CreateActivityDto {
|
||||
@IsIn(ACTIVITY_TYPES)
|
||||
type?: (typeof ACTIVITY_TYPES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACTIVITY_PRIORITIES)
|
||||
priority?: (typeof ACTIVITY_PRIORITIES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
@@ -81,6 +86,18 @@ export class CreateActivityDto {
|
||||
@IsDateString()
|
||||
dueAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(24 * 60)
|
||||
durationMinutes?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsDateString()
|
||||
reminderAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsString()
|
||||
@@ -117,6 +134,10 @@ export class UpdateActivityDto {
|
||||
@IsIn(ACTIVITY_STATUSES)
|
||||
status?: (typeof ACTIVITY_STATUSES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(ACTIVITY_PRIORITIES)
|
||||
priority?: (typeof ACTIVITY_PRIORITIES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
@@ -127,6 +148,18 @@ export class UpdateActivityDto {
|
||||
@IsDateString()
|
||||
dueAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(24 * 60)
|
||||
durationMinutes?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsDateString()
|
||||
reminderAt?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsString()
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
||||
Reference in New Issue
Block a user