7c23a2b4ae
db: activities.assigneeId column (string, optional) activity-actions: assigneeId saved on create (default: self), cleared on update validation: assigneeId added to activitySchema schema: assigneeId field on Activity type activity-form-sheet: Atanan Kişi dropdown for owner/admin with member list activity-team-view: new component — activities grouped by assignee, completion/edit/delete actions, overdue indicator, member avatars activities-client: Ekip tab (owner/admin only), members + currentUserId props activities page: fetches team memberships + user details, passes to client activity-email-actions: filter by assigneeId ?? createdBy for both me/team modes
171 lines
5.7 KiB
TypeScript
171 lines
5.7 KiB
TypeScript
"use server";
|
||
|
||
import { ID } from "node-appwrite";
|
||
import { Query } from "node-appwrite";
|
||
|
||
import { createAdminClient } from "./server";
|
||
import { requireTenant } from "./tenant-guard";
|
||
import { DATABASE_ID, TABLES, ACTIVITY_TYPE_LABELS, type Activity } from "./schema";
|
||
import type { AuthState } from "./auth-types";
|
||
|
||
export type SendTarget = "me" | "team";
|
||
|
||
export type SendSummaryState = AuthState & { sent?: number };
|
||
|
||
export async function sendDailySummaryAction(
|
||
_prev: SendSummaryState,
|
||
formData: FormData,
|
||
): Promise<SendSummaryState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
} catch {
|
||
return { ok: false, error: "Oturum geçersiz." };
|
||
}
|
||
|
||
const target = (formData.get("target") ?? "me") as SendTarget;
|
||
|
||
if (target === "team" && !["owner", "admin"].includes(ctx.role)) {
|
||
return { ok: false, error: "Bu işlem için yetkiniz yok." };
|
||
}
|
||
|
||
const { tablesDB, teams, users, messaging } = createAdminClient();
|
||
|
||
const todayStr = new Date().toISOString().split("T")[0]; // "YYYY-MM-DD"
|
||
|
||
const activitiesResult = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.activities,
|
||
queries: [
|
||
Query.equal("tenantId", ctx.tenantId),
|
||
Query.limit(500),
|
||
],
|
||
});
|
||
|
||
const allActivities = JSON.parse(JSON.stringify(activitiesResult.rows)) as Activity[];
|
||
const todayActivities = allActivities.filter(
|
||
(a) => a.dueDate?.startsWith(todayStr),
|
||
);
|
||
|
||
const dateLabel = new Date().toLocaleDateString("tr-TR", {
|
||
day: "numeric",
|
||
month: "long",
|
||
});
|
||
|
||
if (target === "me") {
|
||
const mine = todayActivities.filter(
|
||
(a) => (a.assigneeId ?? a.createdBy) === ctx.user.id,
|
||
);
|
||
if (mine.length === 0) {
|
||
return { ok: false, error: "Bugün için planlanmış aktiviteniz bulunmuyor." };
|
||
}
|
||
await messaging.createEmail(
|
||
ID.unique(),
|
||
`Bugünün Aktiviteleri — ${dateLabel}`,
|
||
buildEmailHtml(mine, ctx.user.name),
|
||
[],
|
||
[ctx.user.id],
|
||
[],
|
||
);
|
||
return { ok: true, sent: 1 };
|
||
}
|
||
|
||
// send_to_team: her üyeye kendi aktiviteleri
|
||
const membershipsResult = await teams.listMemberships(ctx.tenantId);
|
||
let sentCount = 0;
|
||
|
||
for (const m of membershipsResult.memberships) {
|
||
if (!m.userId || !m.confirm) continue;
|
||
|
||
const memberActivities = todayActivities.filter(
|
||
(a) => (a.assigneeId ?? a.createdBy) === m.userId,
|
||
);
|
||
if (memberActivities.length === 0) continue;
|
||
|
||
const member = await users.get(m.userId).catch(() => null);
|
||
if (!member) continue;
|
||
|
||
await messaging.createEmail(
|
||
ID.unique(),
|
||
`Bugünün Aktiviteleri — ${dateLabel}`,
|
||
buildEmailHtml(memberActivities, member.name),
|
||
[],
|
||
[m.userId],
|
||
[],
|
||
);
|
||
sentCount++;
|
||
}
|
||
|
||
if (sentCount === 0) {
|
||
return {
|
||
ok: false,
|
||
error: "Ekip üyelerinin bugün planlanmış aktivitesi bulunmuyor.",
|
||
};
|
||
}
|
||
|
||
return { ok: true, sent: sentCount };
|
||
}
|
||
|
||
function buildEmailHtml(activities: Activity[], name: string): string {
|
||
const today = new Date().toLocaleDateString("tr-TR", {
|
||
weekday: "long",
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric",
|
||
});
|
||
|
||
const rows = activities
|
||
.map((a) => {
|
||
const done = a.completedAt
|
||
? `<span style="color:#16a34a;font-weight:600;">✓ Tamamlandı</span>`
|
||
: `<span style="color:#94a3b8;">○ Bekliyor</span>`;
|
||
const type = ACTIVITY_TYPE_LABELS[a.type] ?? a.type;
|
||
const desc = a.description
|
||
? `<div style="color:#64748b;font-size:13px;margin-top:2px;">${a.description}</div>`
|
||
: "";
|
||
return `
|
||
<tr style="border-bottom:1px solid #f1f5f9;">
|
||
<td style="padding:12px 16px;">
|
||
<div style="font-weight:500;color:#1e293b;">${a.title}</div>
|
||
${desc}
|
||
</td>
|
||
<td style="padding:12px 16px;white-space:nowrap;">
|
||
<span style="background:#f1f5f9;color:#475569;font-size:12px;padding:2px 8px;border-radius:9999px;">${type}</span>
|
||
</td>
|
||
<td style="padding:12px 16px;white-space:nowrap;font-size:13px;">${done}</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
|
||
return `<!DOCTYPE html>
|
||
<html><body style="margin:0;padding:0;background:#f8fafc;font-family:sans-serif;">
|
||
<div style="max-width:600px;margin:32px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1);">
|
||
<div style="background:linear-gradient(135deg,#1e3a5f,#2563eb);padding:32px 32px 24px;">
|
||
<p style="color:#93c5fd;font-size:13px;margin:0 0 6px;">Emlak CRM · Günlük Özet</p>
|
||
<h1 style="color:#fff;font-size:22px;margin:0;">Merhaba, ${name}</h1>
|
||
<p style="color:#bfdbfe;font-size:14px;margin:8px 0 0;">${today}</p>
|
||
</div>
|
||
<div style="padding:24px 32px 8px;">
|
||
<p style="color:#475569;font-size:14px;margin:0 0 16px;">
|
||
Bugün için <strong>${activities.length} aktivite</strong> planlanmış:
|
||
</p>
|
||
<table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;">
|
||
<thead>
|
||
<tr style="background:#f8fafc;">
|
||
<th style="padding:10px 16px;text-align:left;color:#64748b;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;">Aktivite</th>
|
||
<th style="padding:10px 16px;text-align:left;color:#64748b;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;">Tür</th>
|
||
<th style="padding:10px 16px;text-align:left;color:#64748b;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;">Durum</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="padding:24px 32px;">
|
||
<p style="color:#94a3b8;font-size:12px;margin:0;">
|
||
Bu e-posta Emlak CRM tarafından otomatik olarak gönderilmiştir.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body></html>`;
|
||
}
|