create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { enqueueGoogleSheetSyncJob } from '@/lib/google-sheet-sync-jobs';
|
|
3
|
+
import { QstashAuthError, verifyQstashRequest } from '@/lib/qstash';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Déclenchement planifié de la synchro Google Sheets.
|
|
7
|
+
*
|
|
8
|
+
* - Planification : **Upstash QStash Schedule** en `POST` avec header `upstash-signature` (corps signé).
|
|
9
|
+
* - Optionnel : déclenchement manuel / secours avec `Authorization: Bearer <CRON_SECRET>` (`CRON_SECRET` sur l’hébergeur).
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
const cronSecret = process.env.CRON_SECRET;
|
|
14
|
+
const authHeader = request.headers.get('authorization');
|
|
15
|
+
const expectedBearer = cronSecret ? `Bearer ${cronSecret}` : null;
|
|
16
|
+
const isVercelCron =
|
|
17
|
+
Boolean(expectedBearer && authHeader === expectedBearer);
|
|
18
|
+
|
|
19
|
+
if (!isVercelCron) {
|
|
20
|
+
const signature = request.headers.get('upstash-signature');
|
|
21
|
+
if (signature) {
|
|
22
|
+
const body = await request.text();
|
|
23
|
+
await verifyQstashRequest(signature, body);
|
|
24
|
+
} else {
|
|
25
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const job = await enqueueGoogleSheetSyncJob({
|
|
30
|
+
requestedByUserId: null,
|
|
31
|
+
configId: null,
|
|
32
|
+
triggerType: 'SCHEDULED',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{
|
|
37
|
+
ok: true,
|
|
38
|
+
jobId: job.id,
|
|
39
|
+
status: job.status,
|
|
40
|
+
},
|
|
41
|
+
{ status: 202 },
|
|
42
|
+
);
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
if (error instanceof QstashAuthError) {
|
|
45
|
+
return NextResponse.json({ error: 'Signature QStash invalide' }, { status: 401 });
|
|
46
|
+
}
|
|
47
|
+
console.error('Erreur schedule Google Sheet:', error);
|
|
48
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma, Prisma } from '@/lib/prisma';
|
|
4
|
+
import {
|
|
5
|
+
REMINDERS_CLEAR_ALL_ID,
|
|
6
|
+
REMINDERS_CLEAR_UNDO_WINDOW_MS,
|
|
7
|
+
} from '@/lib/reminder-state';
|
|
8
|
+
import {
|
|
9
|
+
computeExpiresAtForUpsert,
|
|
10
|
+
getRequestId,
|
|
11
|
+
isMissingReminderStateTableError,
|
|
12
|
+
logReminderEvent,
|
|
13
|
+
} from '@/lib/reminder-state-server';
|
|
14
|
+
|
|
15
|
+
export async function POST(request: NextRequest) {
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
const requestId = getRequestId(request);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
21
|
+
if (!session) {
|
|
22
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const undoUntil = new Date(now.getTime() + REMINDERS_CLEAR_UNDO_WINDOW_MS);
|
|
27
|
+
const expiresAt = computeExpiresAtForUpsert(REMINDERS_CLEAR_ALL_ID, 'CLEARED', now);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const existing = await prisma.userReminderState.findUnique({
|
|
31
|
+
where: {
|
|
32
|
+
userId_reminderId: {
|
|
33
|
+
userId: session.user.id,
|
|
34
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
status: true,
|
|
39
|
+
clearedCutoffAt: true,
|
|
40
|
+
updatedAt: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const previousClearedAt =
|
|
45
|
+
existing?.status === 'CLEARED'
|
|
46
|
+
? (existing.clearedCutoffAt ?? existing.updatedAt).toISOString()
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
const metadata: Prisma.InputJsonValue = {
|
|
50
|
+
undoUntil: undoUntil.toISOString(),
|
|
51
|
+
previousClearedAt,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const clearState = await prisma.userReminderState.upsert({
|
|
55
|
+
where: {
|
|
56
|
+
userId_reminderId: {
|
|
57
|
+
userId: session.user.id,
|
|
58
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
update: {
|
|
62
|
+
status: 'CLEARED',
|
|
63
|
+
clearedCutoffAt: now,
|
|
64
|
+
metadata,
|
|
65
|
+
expiresAt,
|
|
66
|
+
},
|
|
67
|
+
create: {
|
|
68
|
+
userId: session.user.id,
|
|
69
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
70
|
+
status: 'CLEARED',
|
|
71
|
+
clearedCutoffAt: now,
|
|
72
|
+
metadata,
|
|
73
|
+
expiresAt,
|
|
74
|
+
},
|
|
75
|
+
select: {
|
|
76
|
+
clearedCutoffAt: true,
|
|
77
|
+
updatedAt: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const clearedAt = (clearState.clearedCutoffAt ?? clearState.updatedAt).toISOString();
|
|
82
|
+
|
|
83
|
+
logReminderEvent({
|
|
84
|
+
event: 'reminders_clear_all',
|
|
85
|
+
requestId,
|
|
86
|
+
userId: session.user.id,
|
|
87
|
+
degraded: false,
|
|
88
|
+
durationMs: Date.now() - started,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return NextResponse.json({
|
|
92
|
+
success: true,
|
|
93
|
+
clearedAt,
|
|
94
|
+
undoUntil: undoUntil.toISOString(),
|
|
95
|
+
degraded: false,
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (isMissingReminderStateTableError(err)) {
|
|
99
|
+
logReminderEvent({
|
|
100
|
+
event: 'reminders_fallback_missing_table',
|
|
101
|
+
requestId,
|
|
102
|
+
userId: session.user.id,
|
|
103
|
+
degraded: true,
|
|
104
|
+
durationMs: Date.now() - started,
|
|
105
|
+
});
|
|
106
|
+
return NextResponse.json({
|
|
107
|
+
success: true,
|
|
108
|
+
clearedAt: now.toISOString(),
|
|
109
|
+
undoUntil: undoUntil.toISOString(),
|
|
110
|
+
degraded: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
const msg = error instanceof Error ? error.message : 'Erreur serveur';
|
|
117
|
+
console.error('Erreur lors du vidage des rappels:', error);
|
|
118
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma, Prisma } from '@/lib/prisma';
|
|
4
|
+
import { REMINDERS_CLEAR_ALL_ID } from '@/lib/reminder-state';
|
|
5
|
+
import {
|
|
6
|
+
computeExpiresAtForUpsert,
|
|
7
|
+
getRequestId,
|
|
8
|
+
isMissingReminderStateTableError,
|
|
9
|
+
logReminderEvent,
|
|
10
|
+
parseClearMetadata,
|
|
11
|
+
} from '@/lib/reminder-state-server';
|
|
12
|
+
|
|
13
|
+
export async function POST(request: NextRequest) {
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const requestId = getRequestId(request);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
19
|
+
if (!session) {
|
|
20
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const now = new Date();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const row = await prisma.userReminderState.findUnique({
|
|
27
|
+
where: {
|
|
28
|
+
userId_reminderId: {
|
|
29
|
+
userId: session.user.id,
|
|
30
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
select: {
|
|
34
|
+
status: true,
|
|
35
|
+
reminderId: true,
|
|
36
|
+
metadata: true,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!row || row.status !== 'CLEARED') {
|
|
41
|
+
return NextResponse.json({ error: 'Rien à annuler.' }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const meta = parseClearMetadata(row.metadata);
|
|
45
|
+
if (!meta || now.getTime() > new Date(meta.undoUntil).getTime()) {
|
|
46
|
+
logReminderEvent({
|
|
47
|
+
event: 'reminders_clear_undo',
|
|
48
|
+
requestId,
|
|
49
|
+
userId: session.user.id,
|
|
50
|
+
degraded: false,
|
|
51
|
+
durationMs: Date.now() - started,
|
|
52
|
+
extra: { rejected: true, reason: 'undo_expired' },
|
|
53
|
+
});
|
|
54
|
+
return NextResponse.json({ error: "Délai d'annulation dépassé." }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (meta.previousClearedAt === null) {
|
|
58
|
+
await prisma.userReminderState.delete({
|
|
59
|
+
where: {
|
|
60
|
+
userId_reminderId: {
|
|
61
|
+
userId: session.user.id,
|
|
62
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
const previous = new Date(meta.previousClearedAt);
|
|
68
|
+
const expiresAt = computeExpiresAtForUpsert(REMINDERS_CLEAR_ALL_ID, 'CLEARED', now);
|
|
69
|
+
await prisma.userReminderState.update({
|
|
70
|
+
where: {
|
|
71
|
+
userId_reminderId: {
|
|
72
|
+
userId: session.user.id,
|
|
73
|
+
reminderId: REMINDERS_CLEAR_ALL_ID,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
data: {
|
|
77
|
+
clearedCutoffAt: previous,
|
|
78
|
+
metadata: Prisma.JsonNull,
|
|
79
|
+
expiresAt,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logReminderEvent({
|
|
85
|
+
event: 'reminders_clear_undo',
|
|
86
|
+
requestId,
|
|
87
|
+
userId: session.user.id,
|
|
88
|
+
degraded: false,
|
|
89
|
+
durationMs: Date.now() - started,
|
|
90
|
+
extra: { restoredPrevious: meta.previousClearedAt != null },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return NextResponse.json({ success: true, degraded: false });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (isMissingReminderStateTableError(err)) {
|
|
96
|
+
logReminderEvent({
|
|
97
|
+
event: 'reminders_fallback_missing_table',
|
|
98
|
+
requestId,
|
|
99
|
+
userId: session.user.id,
|
|
100
|
+
degraded: true,
|
|
101
|
+
durationMs: Date.now() - started,
|
|
102
|
+
});
|
|
103
|
+
return NextResponse.json({ success: true, degraded: true });
|
|
104
|
+
}
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
} catch (error: unknown) {
|
|
108
|
+
const msg = error instanceof Error ? error.message : 'Erreur serveur';
|
|
109
|
+
console.error('Erreur lors de l’annulation du vidage des rappels:', error);
|
|
110
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { REMINDERS_CLEAR_ALL_ID, REMINDER_READ_STATUSES } from '@/lib/reminder-state';
|
|
5
|
+
import {
|
|
6
|
+
getRequestId,
|
|
7
|
+
isMissingReminderStateTableError,
|
|
8
|
+
logReminderEvent,
|
|
9
|
+
reminderStateNotExpired,
|
|
10
|
+
} from '@/lib/reminder-state-server';
|
|
4
11
|
|
|
5
12
|
// GET /api/reminders - Récupérer tous les rappels de l'utilisateur
|
|
6
13
|
export async function GET(request: NextRequest) {
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const requestId = getRequestId(request);
|
|
16
|
+
let degraded = false;
|
|
17
|
+
|
|
7
18
|
try {
|
|
8
19
|
const session = await auth.api.getSession({
|
|
9
20
|
headers: request.headers,
|
|
@@ -14,20 +25,19 @@ export async function GET(request: NextRequest) {
|
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
const now = new Date();
|
|
17
|
-
const
|
|
18
|
-
|
|
28
|
+
const start = new Date(now);
|
|
29
|
+
start.setDate(start.getDate() - 7);
|
|
30
|
+
const end = new Date(now);
|
|
31
|
+
end.setDate(end.getDate() + 1);
|
|
19
32
|
|
|
20
|
-
// Récupérer
|
|
33
|
+
// Récupérer les tâches non complétées de l'utilisateur dans une fenêtre proche
|
|
21
34
|
const tasks = await prisma.task.findMany({
|
|
22
35
|
where: {
|
|
23
36
|
assignedUserId: session.user.id,
|
|
24
37
|
completed: false,
|
|
25
38
|
scheduledAt: {
|
|
26
|
-
gte:
|
|
27
|
-
lte:
|
|
28
|
-
},
|
|
29
|
-
reminderMinutesBefore: {
|
|
30
|
-
not: null,
|
|
39
|
+
gte: start,
|
|
40
|
+
lte: end,
|
|
31
41
|
},
|
|
32
42
|
},
|
|
33
43
|
include: {
|
|
@@ -54,45 +64,160 @@ export async function GET(request: NextRequest) {
|
|
|
54
64
|
take: 100,
|
|
55
65
|
});
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.filter((task) => {
|
|
60
|
-
if (!task.reminderMinutesBefore) return false;
|
|
67
|
+
const reminderCandidates = tasks
|
|
68
|
+
.flatMap((task) => {
|
|
61
69
|
const scheduled = new Date(task.scheduledAt);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const entries: Array<{
|
|
71
|
+
id: string;
|
|
72
|
+
taskId: string;
|
|
73
|
+
kind: 'due' | 'reminder';
|
|
74
|
+
type: string;
|
|
75
|
+
title: string | null;
|
|
76
|
+
description: string;
|
|
77
|
+
priority: string;
|
|
78
|
+
scheduledAt: Date;
|
|
79
|
+
reminderTime: string;
|
|
80
|
+
reminderMinutesBefore: number | null;
|
|
81
|
+
contact: typeof task.contact;
|
|
82
|
+
assignedUser: typeof task.assignedUser;
|
|
83
|
+
}> = [];
|
|
84
|
+
|
|
85
|
+
const diffMs = now.getTime() - scheduled.getTime();
|
|
86
|
+
if (diffMs >= 0) {
|
|
87
|
+
entries.push({
|
|
88
|
+
id: `${task.id}-due`,
|
|
89
|
+
taskId: task.id,
|
|
90
|
+
kind: 'due',
|
|
91
|
+
type: task.type,
|
|
92
|
+
title: task.title,
|
|
93
|
+
description: task.description,
|
|
94
|
+
priority: task.priority,
|
|
95
|
+
scheduledAt: task.scheduledAt,
|
|
96
|
+
reminderTime: scheduled.toISOString(),
|
|
97
|
+
reminderMinutesBefore: task.reminderMinutesBefore,
|
|
98
|
+
contact: task.contact,
|
|
99
|
+
assignedUser: task.assignedUser,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (task.reminderMinutesBefore != null && task.reminderMinutesBefore > 0) {
|
|
104
|
+
const reminderTime = new Date(scheduled.getTime() - task.reminderMinutesBefore * 60 * 1000);
|
|
105
|
+
const diffReminderMs = now.getTime() - reminderTime.getTime();
|
|
106
|
+
if (diffReminderMs >= 0 && now < scheduled) {
|
|
107
|
+
entries.push({
|
|
108
|
+
id: `${task.id}-reminder`,
|
|
109
|
+
taskId: task.id,
|
|
110
|
+
kind: 'reminder',
|
|
111
|
+
type: task.type,
|
|
112
|
+
title: task.title,
|
|
113
|
+
description: task.description,
|
|
114
|
+
priority: task.priority,
|
|
115
|
+
scheduledAt: task.scheduledAt,
|
|
116
|
+
reminderTime: reminderTime.toISOString(),
|
|
117
|
+
reminderMinutesBefore: task.reminderMinutesBefore,
|
|
118
|
+
contact: task.contact,
|
|
119
|
+
assignedUser: task.assignedUser,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return entries;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const reminderIds = reminderCandidates.map((reminder) => reminder.id);
|
|
128
|
+
const countCandidates = reminderCandidates.length;
|
|
129
|
+
|
|
130
|
+
let reminderStates: Array<{
|
|
131
|
+
reminderId: string;
|
|
132
|
+
status: string;
|
|
133
|
+
updatedAt: Date;
|
|
134
|
+
clearedCutoffAt: Date | null;
|
|
135
|
+
}> = [];
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
reminderStates = await prisma.userReminderState.findMany({
|
|
139
|
+
where: {
|
|
140
|
+
userId: session.user.id,
|
|
141
|
+
AND: [
|
|
142
|
+
{
|
|
143
|
+
OR: [{ reminderId: { in: reminderIds } }, { reminderId: REMINDERS_CLEAR_ALL_ID }],
|
|
144
|
+
},
|
|
145
|
+
reminderStateNotExpired(now),
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
select: {
|
|
149
|
+
reminderId: true,
|
|
150
|
+
status: true,
|
|
151
|
+
updatedAt: true,
|
|
152
|
+
clearedCutoffAt: true,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (isMissingReminderStateTableError(err)) {
|
|
157
|
+
degraded = true;
|
|
158
|
+
logReminderEvent({
|
|
159
|
+
event: 'reminders_fallback_missing_table',
|
|
160
|
+
requestId,
|
|
161
|
+
userId: session.user.id,
|
|
162
|
+
degraded: true,
|
|
163
|
+
durationMs: Date.now() - started,
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const clearState = reminderStates.find(
|
|
171
|
+
(state) => state.reminderId === REMINDERS_CLEAR_ALL_ID && state.status === 'CLEARED',
|
|
172
|
+
);
|
|
173
|
+
const clearedAt = clearState
|
|
174
|
+
? (clearState.clearedCutoffAt ?? clearState.updatedAt)
|
|
175
|
+
: null;
|
|
176
|
+
const stateByReminderId = new Map(
|
|
177
|
+
reminderStates
|
|
178
|
+
.filter((state) => state.reminderId !== REMINDERS_CLEAR_ALL_ID)
|
|
179
|
+
.map((state) => [state.reminderId, state]),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const reminders = reminderCandidates
|
|
183
|
+
.filter((reminder) => {
|
|
184
|
+
if (!clearedAt) return true;
|
|
185
|
+
return new Date(reminder.reminderTime).getTime() > clearedAt.getTime();
|
|
70
186
|
})
|
|
71
|
-
.map((
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
187
|
+
.map((reminder) => {
|
|
188
|
+
const state = stateByReminderId.get(reminder.id);
|
|
189
|
+
const isRead = Boolean(state && REMINDER_READ_STATUSES.has(state.status as 'READ' | 'DISMISSED'));
|
|
190
|
+
const isDismissed = state?.status === 'DISMISSED';
|
|
191
|
+
const isClearedByCutoff = Boolean(
|
|
192
|
+
clearedAt && new Date(reminder.reminderTime).getTime() <= clearedAt.getTime(),
|
|
75
193
|
);
|
|
76
194
|
|
|
77
195
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
description: task.description,
|
|
83
|
-
priority: task.priority,
|
|
84
|
-
scheduledAt: task.scheduledAt,
|
|
85
|
-
reminderTime: reminderTime.toISOString(),
|
|
86
|
-
reminderMinutesBefore: task.reminderMinutesBefore,
|
|
87
|
-
contact: task.contact,
|
|
88
|
-
assignedUser: task.assignedUser,
|
|
196
|
+
...reminder,
|
|
197
|
+
isRead,
|
|
198
|
+
isDismissed,
|
|
199
|
+
isClearedByCutoff,
|
|
89
200
|
};
|
|
90
201
|
})
|
|
91
|
-
.sort((a, b) => new Date(
|
|
202
|
+
.sort((a, b) => new Date(b.reminderTime).getTime() - new Date(a.reminderTime).getTime());
|
|
203
|
+
|
|
204
|
+
logReminderEvent({
|
|
205
|
+
event: 'reminders_fetch',
|
|
206
|
+
requestId,
|
|
207
|
+
userId: session.user.id,
|
|
208
|
+
countCandidates,
|
|
209
|
+
countReturned: reminders.length,
|
|
210
|
+
degraded,
|
|
211
|
+
durationMs: Date.now() - started,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const headers: Record<string, string> = {};
|
|
215
|
+
if (degraded) headers['x-reminders-degraded'] = '1';
|
|
92
216
|
|
|
93
|
-
return NextResponse.json(reminders);
|
|
94
|
-
} catch (error:
|
|
217
|
+
return NextResponse.json(reminders, { headers });
|
|
218
|
+
} catch (error: unknown) {
|
|
219
|
+
const msg = error instanceof Error ? error.message : 'Erreur serveur';
|
|
95
220
|
console.error('Erreur lors de la récupération des rappels:', error);
|
|
96
|
-
return NextResponse.json({ error:
|
|
221
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
97
222
|
}
|
|
98
223
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { REMINDERS_CLEAR_ALL_ID } from '@/lib/reminder-state';
|
|
5
|
+
import {
|
|
6
|
+
computeExpiresAtForUpsert,
|
|
7
|
+
getRequestId,
|
|
8
|
+
isMissingReminderStateTableError,
|
|
9
|
+
logReminderEvent,
|
|
10
|
+
reminderStateNotExpired,
|
|
11
|
+
} from '@/lib/reminder-state-server';
|
|
12
|
+
|
|
13
|
+
export async function GET(request: NextRequest) {
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const requestId = getRequestId(request);
|
|
16
|
+
let degraded = false;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
20
|
+
if (!session) {
|
|
21
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const now = new Date();
|
|
25
|
+
let states: Array<{ reminderId: string; status: string; updatedAt: Date }> = [];
|
|
26
|
+
let clearAllState: { status: string; updatedAt: Date; clearedCutoffAt: Date | null } | undefined;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const rows = await prisma.userReminderState.findMany({
|
|
30
|
+
where: {
|
|
31
|
+
userId: session.user.id,
|
|
32
|
+
AND: [reminderStateNotExpired(now)],
|
|
33
|
+
},
|
|
34
|
+
select: {
|
|
35
|
+
reminderId: true,
|
|
36
|
+
status: true,
|
|
37
|
+
updatedAt: true,
|
|
38
|
+
clearedCutoffAt: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
states = rows;
|
|
42
|
+
clearAllState = rows.find((state) => state.reminderId === REMINDERS_CLEAR_ALL_ID);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isMissingReminderStateTableError(err)) {
|
|
45
|
+
degraded = true;
|
|
46
|
+
logReminderEvent({
|
|
47
|
+
event: 'reminders_fallback_missing_table',
|
|
48
|
+
requestId,
|
|
49
|
+
userId: session.user.id,
|
|
50
|
+
degraded: true,
|
|
51
|
+
durationMs: Date.now() - started,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const clearedAtIso =
|
|
59
|
+
clearAllState?.status === 'CLEARED'
|
|
60
|
+
? (clearAllState.clearedCutoffAt ?? clearAllState.updatedAt).toISOString()
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
logReminderEvent({
|
|
64
|
+
event: 'reminder_state_read',
|
|
65
|
+
requestId,
|
|
66
|
+
userId: session.user.id,
|
|
67
|
+
countReturned: states.filter((s) => s.reminderId !== REMINDERS_CLEAR_ALL_ID).length,
|
|
68
|
+
degraded,
|
|
69
|
+
durationMs: Date.now() - started,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
states: states.filter((state) => state.reminderId !== REMINDERS_CLEAR_ALL_ID),
|
|
74
|
+
clearedAt: clearedAtIso,
|
|
75
|
+
degraded,
|
|
76
|
+
});
|
|
77
|
+
} catch (error: unknown) {
|
|
78
|
+
const msg = error instanceof Error ? error.message : 'Erreur serveur';
|
|
79
|
+
console.error("Erreur lors de la récupération de l'état des rappels:", error);
|
|
80
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function POST(request: NextRequest) {
|
|
85
|
+
const started = Date.now();
|
|
86
|
+
const requestId = getRequestId(request);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
90
|
+
if (!session) {
|
|
91
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const body = await request.json().catch(() => ({}));
|
|
95
|
+
const reminderId = typeof body.reminderId === 'string' ? body.reminderId.trim() : '';
|
|
96
|
+
const status = typeof body.status === 'string' ? body.status.trim().toUpperCase() : '';
|
|
97
|
+
if (!reminderId) {
|
|
98
|
+
return NextResponse.json({ error: 'reminderId requis.' }, { status: 400 });
|
|
99
|
+
}
|
|
100
|
+
if (!['READ', 'DISMISSED'].includes(status)) {
|
|
101
|
+
return NextResponse.json({ error: 'status invalide.' }, { status: 400 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const expiresAt = computeExpiresAtForUpsert(reminderId, status as 'READ' | 'DISMISSED', now);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const state = await prisma.userReminderState.upsert({
|
|
109
|
+
where: {
|
|
110
|
+
userId_reminderId: {
|
|
111
|
+
userId: session.user.id,
|
|
112
|
+
reminderId,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
update: {
|
|
116
|
+
status: status as 'READ' | 'DISMISSED',
|
|
117
|
+
expiresAt,
|
|
118
|
+
},
|
|
119
|
+
create: {
|
|
120
|
+
userId: session.user.id,
|
|
121
|
+
reminderId,
|
|
122
|
+
status: status as 'READ' | 'DISMISSED',
|
|
123
|
+
expiresAt,
|
|
124
|
+
},
|
|
125
|
+
select: {
|
|
126
|
+
reminderId: true,
|
|
127
|
+
status: true,
|
|
128
|
+
updatedAt: true,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
logReminderEvent({
|
|
133
|
+
event: 'reminder_state_upsert',
|
|
134
|
+
requestId,
|
|
135
|
+
userId: session.user.id,
|
|
136
|
+
degraded: false,
|
|
137
|
+
durationMs: Date.now() - started,
|
|
138
|
+
extra: { reminderId, status },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return NextResponse.json({ success: true, state, degraded: false });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (isMissingReminderStateTableError(err)) {
|
|
144
|
+
logReminderEvent({
|
|
145
|
+
event: 'reminders_fallback_missing_table',
|
|
146
|
+
requestId,
|
|
147
|
+
userId: session.user.id,
|
|
148
|
+
degraded: true,
|
|
149
|
+
durationMs: Date.now() - started,
|
|
150
|
+
});
|
|
151
|
+
return NextResponse.json({
|
|
152
|
+
success: true,
|
|
153
|
+
state: { reminderId, status, updatedAt: now.toISOString() },
|
|
154
|
+
degraded: true,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
} catch (error: unknown) {
|
|
160
|
+
const msg = error instanceof Error ? error.message : 'Erreur serveur';
|
|
161
|
+
console.error("Erreur lors de la mise à jour de l'état rappel:", error);
|
|
162
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Générer un code à 6 chiffres
|
|
34
|
-
const code =
|
|
34
|
+
const code = String(crypto.getRandomValues(new Uint32Array(1))[0] % 900000 + 100000);
|
|
35
35
|
|
|
36
36
|
// Créer ou mettre à jour le token de vérification
|
|
37
37
|
const expiresAt = new Date();
|