251 lines
8.1 KiB
TypeScript
251 lines
8.1 KiB
TypeScript
"use client"
|
||
|
||
import { create } from "zustand"
|
||
import { persist } from "zustand/middleware"
|
||
import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage"
|
||
import type { Email } from "@/lib/email-data"
|
||
|
||
export type ScheduleSendPayload = {
|
||
sendAtIso: string
|
||
to: { name: string; email: string }[]
|
||
subject: string
|
||
previewText: string
|
||
bodyHtml: string
|
||
}
|
||
|
||
type ScheduledStoreState = {
|
||
scheduledEmails: Email[]
|
||
snoozedEmails: Email[]
|
||
sentPlaceholderEmails: Email[]
|
||
}
|
||
|
||
function rowToSchedulePayload(row: Email): ScheduleSendPayload {
|
||
const email = row.senderEmail?.trim() ?? ""
|
||
const name = row.scheduledToName ?? row.sender
|
||
return {
|
||
sendAtIso: row.scheduledSendAt ?? new Date().toISOString(),
|
||
to: email ? [{ name, email }] : [],
|
||
subject: row.subject,
|
||
previewText: row.preview,
|
||
bodyHtml: row.body ?? `<p></p>`,
|
||
}
|
||
}
|
||
|
||
type ScheduledStoreActions = {
|
||
createScheduledSend: (payload: ScheduleSendPayload) => { id: string }
|
||
deleteScheduledSend: (id: string) => void
|
||
archiveScheduledSend: (id: string) => void
|
||
snoozeScheduledSend: (id: string) => void
|
||
rescheduleScheduledSend: (id: string, sendAtIso: string) => void
|
||
markScheduledReadState: (id: string, read: boolean) => void
|
||
getScheduledEditPayload: (id: string) => ScheduleSendPayload | null
|
||
updateScheduledSend: (id: string, payload: ScheduleSendPayload) => void
|
||
sendScheduledNow: (id: string) => void
|
||
removeScheduledLocal: (id: string) => void
|
||
/** Mettre en attente depuis la boîte (clone id `snz-…` dans En attente ; l’appelant masque l’id source). */
|
||
snoozeMailboxEmail: (row: Email) => void
|
||
/** Quitter « En attente » : réaffiche dans la Boîte (snz-) ou parmi Planifiés (ex-envoi différé snoozé). */
|
||
restoreSnoozedToInbox: (row: Email) => void
|
||
}
|
||
|
||
export const useScheduledStore = create<ScheduledStoreState & ScheduledStoreActions>()(
|
||
persist(
|
||
(set, get) => ({
|
||
scheduledEmails: [],
|
||
snoozedEmails: [],
|
||
sentPlaceholderEmails: [],
|
||
|
||
createScheduledSend: (payload) => {
|
||
const id = `sched-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||
const first = payload.to[0]
|
||
const toName = first?.name?.trim() || first?.email || "Destinataire"
|
||
const row: Email = {
|
||
id,
|
||
sender: toName,
|
||
senderEmail: first?.email,
|
||
subject: payload.subject.trim() || "(Sans objet)",
|
||
preview: payload.previewText.slice(0, 200),
|
||
body: payload.bodyHtml,
|
||
date: "",
|
||
read: true,
|
||
starred: false,
|
||
important: false,
|
||
category: "primary",
|
||
labels: ["scheduled"],
|
||
scheduledSendAt: payload.sendAtIso,
|
||
scheduledToName: toName,
|
||
}
|
||
set((s) => ({
|
||
scheduledEmails: [row, ...s.scheduledEmails.filter((e) => e.id !== id)],
|
||
}))
|
||
return { id }
|
||
},
|
||
|
||
deleteScheduledSend: (id) =>
|
||
set((s) => ({
|
||
scheduledEmails: s.scheduledEmails.filter((e) => e.id !== id),
|
||
})),
|
||
|
||
archiveScheduledSend: (id) =>
|
||
set((s) => ({
|
||
scheduledEmails: s.scheduledEmails.filter((e) => e.id !== id),
|
||
})),
|
||
|
||
snoozeScheduledSend: (id) =>
|
||
set((s) => {
|
||
const row = s.scheduledEmails.find((e) => e.id === id)
|
||
if (!row) return s
|
||
const wake = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||
return {
|
||
scheduledEmails: s.scheduledEmails.filter((e) => e.id !== id),
|
||
snoozedEmails: [
|
||
{
|
||
...row,
|
||
labels: ["snoozed"],
|
||
scheduledSendAt: undefined,
|
||
scheduledToName: undefined,
|
||
snoozeWakeAt: wake.toISOString(),
|
||
sender: row.scheduledToName ?? row.sender,
|
||
date: wake.toLocaleString("fr-FR", { dateStyle: "medium", timeStyle: "short" }),
|
||
read: true,
|
||
},
|
||
...s.snoozedEmails,
|
||
],
|
||
}
|
||
}),
|
||
|
||
rescheduleScheduledSend: (id, sendAtIso) =>
|
||
set((s) => ({
|
||
scheduledEmails: s.scheduledEmails.map((e) =>
|
||
e.id === id ? { ...e, scheduledSendAt: sendAtIso } : e
|
||
),
|
||
})),
|
||
|
||
markScheduledReadState: (id, read) =>
|
||
set((s) => ({
|
||
scheduledEmails: s.scheduledEmails.map((e) =>
|
||
e.id === id ? { ...e, read } : e
|
||
),
|
||
})),
|
||
|
||
getScheduledEditPayload: (id) => {
|
||
const row = get().scheduledEmails.find((e) => e.id === id)
|
||
if (!row) return null
|
||
return rowToSchedulePayload(row)
|
||
},
|
||
|
||
updateScheduledSend: (id, payload) =>
|
||
set((s) => {
|
||
const first = payload.to[0]
|
||
const toName = first?.name?.trim() || first?.email || "Destinataire"
|
||
return {
|
||
scheduledEmails: s.scheduledEmails.map((e) =>
|
||
e.id === id
|
||
? {
|
||
...e,
|
||
sender: toName,
|
||
senderEmail: first?.email,
|
||
subject: payload.subject.trim() || "(Sans objet)",
|
||
preview: payload.previewText.slice(0, 200),
|
||
body: payload.bodyHtml,
|
||
scheduledSendAt: payload.sendAtIso,
|
||
scheduledToName: toName,
|
||
}
|
||
: e
|
||
),
|
||
}
|
||
}),
|
||
|
||
sendScheduledNow: (id) =>
|
||
set((s) => {
|
||
const row = s.scheduledEmails.find((e) => e.id === id)
|
||
if (!row) return s
|
||
const now = new Date()
|
||
return {
|
||
scheduledEmails: s.scheduledEmails.filter((e) => e.id !== id),
|
||
sentPlaceholderEmails: [
|
||
{
|
||
id: `sent-now-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
|
||
sender: row.scheduledToName ?? row.sender,
|
||
senderEmail: row.senderEmail,
|
||
subject: row.subject,
|
||
preview: row.preview,
|
||
body: row.body,
|
||
date: now.toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" }),
|
||
read: true,
|
||
starred: false,
|
||
important: false,
|
||
category: "primary",
|
||
labels: ["sent"],
|
||
},
|
||
...s.sentPlaceholderEmails,
|
||
],
|
||
}
|
||
}),
|
||
|
||
removeScheduledLocal: (id) =>
|
||
set((s) => ({
|
||
scheduledEmails: s.scheduledEmails.filter((e) => e.id !== id),
|
||
})),
|
||
|
||
snoozeMailboxEmail: (row) =>
|
||
set((s) => {
|
||
const persistedId = row.id.startsWith("snz-") ? row.id : `snz-${row.id}`
|
||
const wake = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||
const wakeIso = wake.toISOString()
|
||
const copy: Email = {
|
||
...row,
|
||
id: persistedId,
|
||
labels: ["snoozed"],
|
||
snoozeWakeAt: wakeIso,
|
||
scheduledSendAt: undefined,
|
||
scheduledToName: undefined,
|
||
date: wake.toLocaleString("fr-FR", {
|
||
dateStyle: "medium",
|
||
timeStyle: "short",
|
||
}),
|
||
read: true,
|
||
}
|
||
return {
|
||
snoozedEmails: [
|
||
copy,
|
||
...s.snoozedEmails.filter((e) => e.id !== persistedId),
|
||
],
|
||
}
|
||
}),
|
||
|
||
restoreSnoozedToInbox: (row) =>
|
||
set((s) => {
|
||
const nextSnoozed = s.snoozedEmails.filter((e) => e.id !== row.id)
|
||
if (row.id.startsWith("snz-")) {
|
||
return { snoozedEmails: nextSnoozed }
|
||
}
|
||
const resumeAt =
|
||
row.snoozeWakeAt ??
|
||
new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
||
const back: Email = {
|
||
...row,
|
||
labels: ["scheduled"],
|
||
scheduledSendAt: resumeAt,
|
||
scheduledToName: row.sender,
|
||
snoozeWakeAt: undefined,
|
||
date: "",
|
||
read: true,
|
||
}
|
||
return {
|
||
snoozedEmails: nextSnoozed,
|
||
scheduledEmails: [
|
||
back,
|
||
...s.scheduledEmails.filter((e) => e.id !== row.id),
|
||
],
|
||
}
|
||
}),
|
||
}),
|
||
{
|
||
name: "ultimail-scheduled-state",
|
||
storage: debouncedPersistJSONStorage,
|
||
version: 1,
|
||
}
|
||
)
|
||
)
|