feat: activity assignment + team view for owner/admin
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
This commit is contained in:
@@ -11,9 +11,9 @@ import { ActivitiesClient } from "@/components/activities/activities-client";
|
|||||||
|
|
||||||
export default async function ActivitiesPage() {
|
export default async function ActivitiesPage() {
|
||||||
const ctx = await requireTenant();
|
const ctx = await requireTenant();
|
||||||
const { tablesDB } = createAdminClient();
|
const { tablesDB, teams, users } = createAdminClient();
|
||||||
|
|
||||||
const [customers, properties, activitiesResult] = await Promise.all([
|
const [customers, properties, activitiesResult, membershipsResult] = await Promise.all([
|
||||||
listCustomers(ctx.tenantId),
|
listCustomers(ctx.tenantId),
|
||||||
listProperties(ctx.tenantId),
|
listProperties(ctx.tenantId),
|
||||||
tablesDB.listRows({
|
tablesDB.listRows({
|
||||||
@@ -25,10 +25,22 @@ export default async function ActivitiesPage() {
|
|||||||
Query.limit(300),
|
Query.limit(300),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
teams.listMemberships(ctx.tenantId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activities = JSON.parse(JSON.stringify(activitiesResult.rows)) as Activity[];
|
const activities = JSON.parse(JSON.stringify(activitiesResult.rows)) as Activity[];
|
||||||
|
|
||||||
|
const members = (
|
||||||
|
await Promise.all(
|
||||||
|
membershipsResult.memberships
|
||||||
|
.filter((m) => m.userId && m.confirm)
|
||||||
|
.map(async (m) => {
|
||||||
|
const u = await users.get(m.userId).catch(() => null);
|
||||||
|
return u ? { id: m.userId, name: u.name } : null;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((m): m is { id: string; name: string } => m !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
|
<div className="flex flex-1 flex-col gap-4 p-4 md:p-6">
|
||||||
<ActivitiesClient
|
<ActivitiesClient
|
||||||
@@ -36,6 +48,8 @@ export default async function ActivitiesPage() {
|
|||||||
customers={customers}
|
customers={customers}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
role={ctx.role}
|
role={ctx.role}
|
||||||
|
members={members}
|
||||||
|
currentUserId={ctx.user.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { DotsThree, Plus, PencilSimple, Trash, CheckCircle, List, CalendarDots } from '@/lib/icons';
|
import { DotsThree, Plus, PencilSimple, Trash, CheckCircle, List, CalendarDots, Users } from '@/lib/icons';
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -19,19 +19,23 @@ import {
|
|||||||
} from "@/lib/appwrite/activity-actions";
|
} from "@/lib/appwrite/activity-actions";
|
||||||
import { ActivityFormSheet } from "./activity-form-sheet";
|
import { ActivityFormSheet } from "./activity-form-sheet";
|
||||||
import { ActivityCalendar } from "./activity-calendar";
|
import { ActivityCalendar } from "./activity-calendar";
|
||||||
|
import { ActivityTeamView } from "./activity-team-view";
|
||||||
import { SendSummaryDialog } from "./send-summary-dialog";
|
import { SendSummaryDialog } from "./send-summary-dialog";
|
||||||
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||||
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
|
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
|
||||||
import { ACTIVITY_TYPE_LABELS } from "@/lib/appwrite/schema";
|
import { ACTIVITY_TYPE_LABELS } from "@/lib/appwrite/schema";
|
||||||
import type { TenantRole } from "@/lib/appwrite/tenant-guard";
|
import type { TenantRole } from "@/lib/appwrite/tenant-guard";
|
||||||
|
import type { TeamMember } from "./activity-form-sheet";
|
||||||
|
|
||||||
type ViewMode = "list" | "calendar";
|
type ViewMode = "list" | "calendar" | "team";
|
||||||
|
|
||||||
interface ActivitiesClientProps {
|
interface ActivitiesClientProps {
|
||||||
initialActivities: Activity[];
|
initialActivities: Activity[];
|
||||||
customers: Customer[];
|
customers: Customer[];
|
||||||
properties: Property[];
|
properties: Property[];
|
||||||
role: TenantRole;
|
role: TenantRole;
|
||||||
|
members: TeamMember[];
|
||||||
|
currentUserId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivitiesClient({
|
export function ActivitiesClient({
|
||||||
@@ -39,6 +43,8 @@ export function ActivitiesClient({
|
|||||||
customers,
|
customers,
|
||||||
properties,
|
properties,
|
||||||
role,
|
role,
|
||||||
|
members,
|
||||||
|
currentUserId,
|
||||||
}: ActivitiesClientProps) {
|
}: ActivitiesClientProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [activities, setActivities] = useState(initialActivities);
|
const [activities, setActivities] = useState(initialActivities);
|
||||||
@@ -126,6 +132,20 @@ export function ActivitiesClient({
|
|||||||
<CalendarDots className="size-3.5" />
|
<CalendarDots className="size-3.5" />
|
||||||
Takvim
|
Takvim
|
||||||
</button>
|
</button>
|
||||||
|
{(role === "owner" || role === "admin") && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode("team")}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
viewMode === "team"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-background text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Users className="size-3.5" />
|
||||||
|
Ekip
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SendSummaryDialog role={role} />
|
<SendSummaryDialog role={role} />
|
||||||
<Button onClick={openCreate} size="sm" data-tour="activities-add">
|
<Button onClick={openCreate} size="sm" data-tour="activities-add">
|
||||||
@@ -146,6 +166,18 @@ export function ActivitiesClient({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Team view — owner/admin only */}
|
||||||
|
{viewMode === "team" && (
|
||||||
|
<ActivityTeamView
|
||||||
|
activities={activities}
|
||||||
|
members={members}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onEdit={openEdit}
|
||||||
|
onComplete={handleComplete}
|
||||||
|
onDelete={setDeleteTarget}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* List view */}
|
{/* List view */}
|
||||||
{viewMode === "list" && (
|
{viewMode === "list" && (
|
||||||
<div data-tour="activities-table" className="rounded-md border">
|
<div data-tour="activities-table" className="rounded-md border">
|
||||||
@@ -230,6 +262,9 @@ export function ActivitiesClient({
|
|||||||
activity={editing}
|
activity={editing}
|
||||||
customers={customers}
|
customers={customers}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
|
members={members}
|
||||||
|
role={role}
|
||||||
|
currentUserId={currentUserId}
|
||||||
onSuccess={() => router.refresh()}
|
onSuccess={() => router.refresh()}
|
||||||
/>
|
/>
|
||||||
<DeleteConfirmDialog
|
<DeleteConfirmDialog
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { ResponsiveSheet, FormWizard } from "@/components/ui/responsive-sheet";
|
import { ResponsiveSheet, FormWizard } from "@/components/ui/responsive-sheet";
|
||||||
import { createActivityAction, updateActivityAction } from "@/lib/appwrite/activity-actions";
|
import { createActivityAction, updateActivityAction } from "@/lib/appwrite/activity-actions";
|
||||||
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
|
import type { Activity, Customer, Property } from "@/lib/appwrite/schema";
|
||||||
|
import type { TenantRole } from "@/lib/appwrite/tenant-guard";
|
||||||
|
|
||||||
|
export type TeamMember = { id: string; name: string };
|
||||||
|
|
||||||
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
type ActionState = { ok: boolean; error?: string; fieldErrors?: Record<string, string[]> };
|
||||||
const INITIAL: ActionState = { ok: false };
|
const INITIAL: ActionState = { ok: false };
|
||||||
@@ -19,10 +22,13 @@ interface ActivityFormSheetProps {
|
|||||||
activity?: Activity | null;
|
activity?: Activity | null;
|
||||||
customers: Customer[];
|
customers: Customer[];
|
||||||
properties: Property[];
|
properties: Property[];
|
||||||
|
members?: TeamMember[];
|
||||||
|
role?: TenantRole;
|
||||||
|
currentUserId?: string;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityFormSheet({ open, onOpenChange, activity, customers, properties, onSuccess }: ActivityFormSheetProps) {
|
export function ActivityFormSheet({ open, onOpenChange, activity, customers, properties, members, role, currentUserId, onSuccess }: ActivityFormSheetProps) {
|
||||||
const action = activity ? updateActivityAction.bind(null, activity.$id) : createActivityAction;
|
const action = activity ? updateActivityAction.bind(null, activity.$id) : createActivityAction;
|
||||||
const [state, formAction, isPending] = useActionState(action, INITIAL);
|
const [state, formAction, isPending] = useActionState(action, INITIAL);
|
||||||
|
|
||||||
@@ -66,6 +72,22 @@ export function ActivityFormSheet({ open, onOpenChange, activity, customers, pro
|
|||||||
<Input id="dueDate" name="dueDate" type="date"
|
<Input id="dueDate" name="dueDate" type="date"
|
||||||
defaultValue={activity?.dueDate ? activity.dueDate.split("T")[0] : ""} />
|
defaultValue={activity?.dueDate ? activity.dueDate.split("T")[0] : ""} />
|
||||||
</div>
|
</div>
|
||||||
|
{(role === "owner" || role === "admin") && members && members.length > 0 && (
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<Label>Atanan Kişi</Label>
|
||||||
|
<select
|
||||||
|
name="assigneeId"
|
||||||
|
defaultValue={activity?.assigneeId ?? currentUserId ?? ""}
|
||||||
|
className="border-input bg-background h-9 rounded-md border px-3 text-sm"
|
||||||
|
>
|
||||||
|
{members.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.id === currentUserId ? `${m.name} (ben)` : m.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CheckCircle, DotsThree, PencilSimple, Trash, User } from "@/lib/icons";
|
||||||
|
import {
|
||||||
|
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import type { Activity } from "@/lib/appwrite/schema";
|
||||||
|
import { ACTIVITY_TYPE_LABELS } from "@/lib/appwrite/schema";
|
||||||
|
import type { TeamMember } from "./activity-form-sheet";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activities: Activity[];
|
||||||
|
members: TeamMember[];
|
||||||
|
currentUserId: string;
|
||||||
|
onEdit: (a: Activity) => void;
|
||||||
|
onComplete: (a: Activity) => void;
|
||||||
|
onDelete: (a: Activity) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivityTeamView({
|
||||||
|
activities,
|
||||||
|
members,
|
||||||
|
currentUserId,
|
||||||
|
onEdit,
|
||||||
|
onComplete,
|
||||||
|
onDelete,
|
||||||
|
}: Props) {
|
||||||
|
const memberIds = new Set(members.map((m) => m.id));
|
||||||
|
|
||||||
|
// Group by effective assignee (assigneeId ?? createdBy)
|
||||||
|
const groups = members
|
||||||
|
.map((member) => ({
|
||||||
|
member,
|
||||||
|
activities: activities.filter((a) => {
|
||||||
|
const effective = a.assigneeId || a.createdBy;
|
||||||
|
return effective === member.id;
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter((g) => g.activities.length > 0);
|
||||||
|
|
||||||
|
// Activities not belonging to any known member
|
||||||
|
const orphaned = activities.filter((a) => {
|
||||||
|
const effective = a.assigneeId || a.createdBy;
|
||||||
|
return !memberIds.has(effective);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (groups.length === 0 && orphaned.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
|
<User className="size-10 text-muted-foreground/30 mb-3" />
|
||||||
|
<p className="text-muted-foreground text-sm">Henüz aktivite bulunmuyor.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{groups.map(({ member, activities: memberActivities }) => (
|
||||||
|
<MemberSection
|
||||||
|
key={member.id}
|
||||||
|
member={member}
|
||||||
|
isCurrentUser={member.id === currentUserId}
|
||||||
|
activities={memberActivities}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{orphaned.length > 0 && (
|
||||||
|
<MemberSection
|
||||||
|
member={{ id: "", name: "Atanmamış" }}
|
||||||
|
isCurrentUser={false}
|
||||||
|
activities={orphaned}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MemberSection({
|
||||||
|
member,
|
||||||
|
isCurrentUser,
|
||||||
|
activities,
|
||||||
|
onEdit,
|
||||||
|
onComplete,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
member: TeamMember;
|
||||||
|
isCurrentUser: boolean;
|
||||||
|
activities: Activity[];
|
||||||
|
onEdit: (a: Activity) => void;
|
||||||
|
onComplete: (a: Activity) => void;
|
||||||
|
onDelete: (a: Activity) => void;
|
||||||
|
}) {
|
||||||
|
const pending = activities.filter((a) => !a.completedAt).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border bg-card">
|
||||||
|
{/* Section header */}
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/40 rounded-t-xl">
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold shrink-0">
|
||||||
|
{member.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="font-semibold text-sm">
|
||||||
|
{member.name}
|
||||||
|
{isCurrentUser && (
|
||||||
|
<span className="text-muted-foreground font-normal ml-1">(ben)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
{pending > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{pending} bekliyor
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{activities.length} toplam
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activities */}
|
||||||
|
<div className="divide-y">
|
||||||
|
{activities.map((a) => (
|
||||||
|
<ActivityRow
|
||||||
|
key={a.$id}
|
||||||
|
activity={a}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivityRow({
|
||||||
|
activity: a,
|
||||||
|
onEdit,
|
||||||
|
onComplete,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
activity: Activity;
|
||||||
|
onEdit: (a: Activity) => void;
|
||||||
|
onComplete: (a: Activity) => void;
|
||||||
|
onDelete: (a: Activity) => void;
|
||||||
|
}) {
|
||||||
|
const isCompleted = !!a.completedAt;
|
||||||
|
const isOverdue =
|
||||||
|
!isCompleted && a.dueDate && new Date(a.dueDate) < new Date(new Date().setHours(0, 0, 0, 0));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-3 px-4 py-3 hover:bg-muted/30 transition-colors group">
|
||||||
|
{/* Completion indicator */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => !isCompleted && onComplete(a)}
|
||||||
|
disabled={isCompleted}
|
||||||
|
title={isCompleted ? "Tamamlandı" : "Tamamla"}
|
||||||
|
className={`mt-0.5 shrink-0 rounded-full transition-colors ${
|
||||||
|
isCompleted
|
||||||
|
? "text-green-500 cursor-default"
|
||||||
|
: "text-muted-foreground/40 hover:text-green-500 cursor-pointer"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckCircle className="size-5" weight={isCompleted ? "fill" : "regular"} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className={`text-sm font-medium leading-snug truncate ${isCompleted ? "line-through text-muted-foreground" : ""}`}>
|
||||||
|
{a.title}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
|
<Badge variant="secondary" className="text-xs py-0">
|
||||||
|
{ACTIVITY_TYPE_LABELS[a.type] ?? a.type}
|
||||||
|
</Badge>
|
||||||
|
{a.dueDate && (
|
||||||
|
<span className={`text-xs ${isOverdue ? "text-destructive font-medium" : "text-muted-foreground"}`}>
|
||||||
|
{new Date(a.dueDate).toLocaleDateString("tr-TR", { day: "numeric", month: "short" })}
|
||||||
|
{isOverdue && " · Gecikmiş"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{a.description && (
|
||||||
|
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||||
|
{a.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
||||||
|
>
|
||||||
|
<DotsThree className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{!isCompleted && (
|
||||||
|
<DropdownMenuItem onClick={() => onComplete(a)}>
|
||||||
|
<CheckCircle className="size-4 mr-2" />
|
||||||
|
Tamamla
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem onClick={() => onEdit(a)}>
|
||||||
|
<PencilSimple className="size-4 mr-2" />
|
||||||
|
Düzenle
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete(a)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash className="size-4 mr-2" />
|
||||||
|
Sil
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ export async function createActivityAction(
|
|||||||
customerId: data.customerId,
|
customerId: data.customerId,
|
||||||
propertyId: data.propertyId,
|
propertyId: data.propertyId,
|
||||||
dueDate: data.dueDate,
|
dueDate: data.dueDate,
|
||||||
|
assigneeId: data.assigneeId || ctx.user.id,
|
||||||
createdBy: ctx.user.id,
|
createdBy: ctx.user.id,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -77,6 +78,7 @@ export async function updateActivityAction(
|
|||||||
customerId: data.customerId,
|
customerId: data.customerId,
|
||||||
propertyId: data.propertyId,
|
propertyId: data.propertyId,
|
||||||
dueDate: data.dueDate,
|
dueDate: data.dueDate,
|
||||||
|
assigneeId: data.assigneeId || null,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return { ok: false, error: "Aktivite güncellenemedi." };
|
return { ok: false, error: "Aktivite güncellenemedi." };
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export async function sendDailySummaryAction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (target === "me") {
|
if (target === "me") {
|
||||||
const mine = todayActivities.filter((a) => a.createdBy === ctx.user.id);
|
const mine = todayActivities.filter(
|
||||||
|
(a) => (a.assigneeId ?? a.createdBy) === ctx.user.id,
|
||||||
|
);
|
||||||
if (mine.length === 0) {
|
if (mine.length === 0) {
|
||||||
return { ok: false, error: "Bugün için planlanmış aktiviteniz bulunmuyor." };
|
return { ok: false, error: "Bugün için planlanmış aktiviteniz bulunmuyor." };
|
||||||
}
|
}
|
||||||
@@ -75,7 +77,9 @@ export async function sendDailySummaryAction(
|
|||||||
for (const m of membershipsResult.memberships) {
|
for (const m of membershipsResult.memberships) {
|
||||||
if (!m.userId || !m.confirm) continue;
|
if (!m.userId || !m.confirm) continue;
|
||||||
|
|
||||||
const memberActivities = todayActivities.filter((a) => a.createdBy === m.userId);
|
const memberActivities = todayActivities.filter(
|
||||||
|
(a) => (a.assigneeId ?? a.createdBy) === m.userId,
|
||||||
|
);
|
||||||
if (memberActivities.length === 0) continue;
|
if (memberActivities.length === 0) continue;
|
||||||
|
|
||||||
const member = await users.get(m.userId).catch(() => null);
|
const member = await users.get(m.userId).catch(() => null);
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export interface Activity extends Row {
|
|||||||
dueDate?: string;
|
dueDate?: string;
|
||||||
completedAt?: string;
|
completedAt?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
assigneeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantSettings extends Row {
|
export interface TenantSettings extends Row {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const activitySchema = z.object({
|
|||||||
customerId: z.string().max(36).optional(),
|
customerId: z.string().max(36).optional(),
|
||||||
propertyId: z.string().max(36).optional(),
|
propertyId: z.string().max(36).optional(),
|
||||||
dueDate: z.string().optional(),
|
dueDate: z.string().optional(),
|
||||||
|
assigneeId: z.string().max(36).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ActivityFormValues = z.infer<typeof activitySchema>;
|
export type ActivityFormValues = z.infer<typeof activitySchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user