feat: implement portal shell layout with user authentication, sidebar navigation, and progress tracking
This commit is contained in:
@@ -121,3 +121,23 @@ export async function archiveClientRecord(formData: FormData) {
|
||||
|
||||
revalidatePath("/clients");
|
||||
}
|
||||
|
||||
export async function updateClientPipelineStage(id: string, stage: string) {
|
||||
const { supabase, userId } = await getCurrentUserId();
|
||||
|
||||
if (!id || !stage) {
|
||||
throw new Error("Eksik bilgi.");
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("clients")
|
||||
.update({ pipeline_stage: stage })
|
||||
.eq("id", id)
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Aşama güncellenemedi: ${error.message}`);
|
||||
}
|
||||
|
||||
revalidatePath("/clients");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
archiveClientRecord,
|
||||
createClientRecord,
|
||||
updateClientRecord,
|
||||
updateClientPipelineStage,
|
||||
} from "@/app/(dashboard)/clients/actions";
|
||||
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
|
||||
import {
|
||||
@@ -43,6 +44,21 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { format, isPast, isToday } from "date-fns";
|
||||
import { tr } from "date-fns/locale";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCorners,
|
||||
useDroppable,
|
||||
useDraggable,
|
||||
} from "@dnd-kit/core";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type ClientListItem = {
|
||||
id: string;
|
||||
@@ -101,8 +117,52 @@ export function ClientsClient({
|
||||
const [query, setQuery] = useState("");
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
const [activeDragClient, setActiveDragClient] = useState<ClientListItem | null>(null);
|
||||
const [localClients, setLocalClients] = useState(clients);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalClients(clients);
|
||||
}, [clients]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
function handleDragStart(event: DragStartEvent) {
|
||||
const { active } = event;
|
||||
const client = localClients.find(c => c.id === active.id);
|
||||
if (client) setActiveDragClient(client);
|
||||
}
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
setActiveDragClient(null);
|
||||
|
||||
if (!over) return;
|
||||
|
||||
const clientId = active.id as string;
|
||||
const newStage = over.id as string;
|
||||
|
||||
const client = localClients.find(c => c.id === clientId);
|
||||
if (!client || client.pipeline_stage === newStage) return;
|
||||
|
||||
setLocalClients(prev =>
|
||||
prev.map(c => c.id === clientId ? { ...c, pipeline_stage: newStage as any } : c)
|
||||
);
|
||||
|
||||
try {
|
||||
await updateClientPipelineStage(clientId, newStage);
|
||||
} catch (error) {
|
||||
setLocalClients(clients);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredClients = normalizedQuery
|
||||
? clients.filter((client) =>
|
||||
? localClients.filter((client) =>
|
||||
[
|
||||
client.name,
|
||||
client.company_name,
|
||||
@@ -114,7 +174,7 @@ export function ClientsClient({
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(normalizedQuery)),
|
||||
)
|
||||
: clients;
|
||||
: localClients;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
@@ -181,83 +241,133 @@ export function ClientsClient({
|
||||
</div>
|
||||
|
||||
<TabsContent value="pipeline" className="mt-0">
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 snap-x">
|
||||
{pipelineStages.map(stage => {
|
||||
const stageClients = filteredClients.filter(c => c.pipeline_stage === stage.id && c.status !== 'archived');
|
||||
return (
|
||||
<div key={stage.id} className="flex-shrink-0 w-80 bg-muted/30 rounded-lg border border-border p-3 snap-start flex flex-col h-[calc(100vh-320px)] min-h-[500px]">
|
||||
<div className="flex items-center justify-between mb-3 px-1">
|
||||
<h3 className="font-semibold text-sm text-foreground flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${stage.color.split(' ')[1]}`}></span>
|
||||
{stage.label}
|
||||
</h3>
|
||||
<Badge variant="secondary" className="text-xs">{stageClients.length}</Badge>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-1 tiny-scrollbar">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 snap-x">
|
||||
{pipelineStages.map(stage => {
|
||||
const stageClients = filteredClients.filter(c => c.pipeline_stage === stage.id && c.status !== 'archived');
|
||||
return (
|
||||
<DroppableColumn
|
||||
key={stage.id}
|
||||
id={stage.id}
|
||||
title={stage.label}
|
||||
count={stageClients.length}
|
||||
color={stage.color.split(' ')[1]}
|
||||
>
|
||||
{stageClients.map(client => (
|
||||
<Card key={client.id} className="cursor-pointer hover:border-primary/50 transition-colors">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<Link href={`/clients/${client.id}`} className="font-medium text-foreground hover:underline">
|
||||
{client.name}
|
||||
</Link>
|
||||
<ClientDialog mode="edit" client={client} trigger={<Button variant="ghost" className="h-6 w-6 p-0"><Pencil className="h-3 w-3" /></Button>} />
|
||||
</div>
|
||||
{client.company_name && <p className="text-xs text-muted-foreground mb-2">{client.company_name}</p>}
|
||||
|
||||
{client.next_follow_up_date && (
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs">
|
||||
<Clock className={`h-3 w-3 ${isPast(new Date(client.next_follow_up_date)) ? 'text-rose-500' : 'text-muted-foreground'}`} />
|
||||
<span className={isPast(new Date(client.next_follow_up_date)) ? 'text-rose-500 font-medium' : 'text-muted-foreground'}>
|
||||
{format(new Date(client.next_follow_up_date), 'd MMM yyyy', { locale: tr })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DraggableClientCard key={client.id} client={client} />
|
||||
))}
|
||||
{stageClients.length === 0 && (
|
||||
<div className="h-24 flex items-center justify-center border-2 border-dashed border-border rounded-md text-xs text-muted-foreground">
|
||||
Boş
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DroppableColumn>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<DragOverlay>
|
||||
{activeDragClient ? (
|
||||
<DraggableClientCard client={activeDragClient} isOverlay />
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="list" className="mt-0">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{filteredClients.length > 0 ? (
|
||||
<div className="overflow-hidden rounded-sm border border-border">
|
||||
<div className="hidden grid-cols-[1.5fr_1fr_1fr_1fr_0.8fr_0.8fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid">
|
||||
<span>Müşteri</span>
|
||||
<span>İletişim</span>
|
||||
<span>Aşama</span>
|
||||
<span>Follow-up</span>
|
||||
<span>Projeler</span>
|
||||
<span className="text-right">İşlem</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{filteredClients.map((client) => (
|
||||
<ClientRow key={client.id} client={client} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState hasQuery={Boolean(normalizedQuery)} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{filteredClients.length > 0 ? (
|
||||
<div className="overflow-hidden rounded-sm border border-border">
|
||||
<div className="hidden grid-cols-[1.5fr_1fr_1fr_1fr_0.8fr_0.8fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid">
|
||||
<span>Müşteri</span>
|
||||
<span>İletişim</span>
|
||||
<span>Aşama</span>
|
||||
<span>Follow-up</span>
|
||||
<span>Projeler</span>
|
||||
<span className="text-right">İşlem</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{filteredClients.map((client) => (
|
||||
<ClientRow key={client.id} client={client} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState hasQuery={Boolean(normalizedQuery)} />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DroppableColumn({ id, title, count, color, children }: { id: string, title: string, count: number, color: string, children: React.ReactNode }) {
|
||||
const { isOver, setNodeRef } = useDroppable({ id });
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"flex-shrink-0 w-[340px] px-2 flex flex-col h-[calc(100vh-320px)] min-h-[500px] transition-colors rounded-lg",
|
||||
isOver ? "bg-muted/30" : ""
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3 px-1">
|
||||
<h3 className="font-semibold text-sm text-foreground flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${color}`}></span>
|
||||
{title}
|
||||
</h3>
|
||||
<Badge variant="secondary" className="text-xs">{count}</Badge>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-1 tiny-scrollbar">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DraggableClientCard({ client, isOverlay }: { client: ClientListItem, isOverlay?: boolean }) {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||
id: client.id,
|
||||
data: client,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
opacity: isDragging && !isOverlay ? 0.3 : 1,
|
||||
zIndex: isDragging ? 999 : "auto",
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes} className={cn("touch-none cursor-grab active:cursor-grabbing", isOverlay && "rotate-2 scale-105 shadow-xl")}>
|
||||
<Card className="hover:border-primary/50 transition-colors">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<Link href={`/clients/${client.id}`} className="font-medium text-foreground hover:underline" onPointerDown={(e) => e.stopPropagation()}>
|
||||
{client.name}
|
||||
</Link>
|
||||
<div onPointerDown={(e) => e.stopPropagation()}>
|
||||
<ClientDialog mode="edit" client={client} trigger={<Button variant="ghost" className="h-6 w-6 p-0"><Pencil className="h-3 w-3" /></Button>} />
|
||||
</div>
|
||||
</div>
|
||||
{client.company_name && <p className="text-xs text-muted-foreground mb-2 pointer-events-none">{client.company_name}</p>}
|
||||
|
||||
{client.next_follow_up_date && (
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs pointer-events-none">
|
||||
<Clock className={`h-3 w-3 ${isPast(new Date(client.next_follow_up_date)) ? 'text-rose-500' : 'text-muted-foreground'}`} />
|
||||
<span className={isPast(new Date(client.next_follow_up_date)) ? 'text-rose-500 font-medium' : 'text-muted-foreground'}>
|
||||
{format(new Date(client.next_follow_up_date), 'd MMM yyyy', { locale: tr })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientRow({ client }: { client: ClientListItem }) {
|
||||
const isFollowUpOverdue = client.next_follow_up_date && (isPast(new Date(client.next_follow_up_date)) || isToday(new Date(client.next_follow_up_date)));
|
||||
const stage = pipelineStages.find(s => s.id === client.pipeline_stage) || pipelineStages[0];
|
||||
|
||||
@@ -20,6 +20,8 @@ type ProjectRow = {
|
||||
budget_amount: number | string | null;
|
||||
currency: string;
|
||||
progress: number;
|
||||
progress_type: "manual" | "auto" | null;
|
||||
revision_quota: number | null;
|
||||
cover_image_path: string | null;
|
||||
cover_image_alt: string | null;
|
||||
clients: { name: string } | { name: string }[] | null;
|
||||
@@ -66,7 +68,7 @@ export default async function ProjectDetailPage({
|
||||
supabase
|
||||
.from("projects")
|
||||
.select(
|
||||
"id, client_id, name, type, description, status, start_date, due_date, budget_amount, currency, progress, cover_image_path, cover_image_alt, clients(name)",
|
||||
"id, client_id, name, type, description, status, start_date, due_date, budget_amount, currency, progress, progress_type, revision_quota, cover_image_path, cover_image_alt, clients(name)",
|
||||
)
|
||||
.eq("id", id)
|
||||
.eq("user_id", user.id)
|
||||
@@ -120,6 +122,8 @@ export default async function ProjectDetailPage({
|
||||
projectData.budget_amount === null ? null : Number(projectData.budget_amount),
|
||||
currency: projectData.currency,
|
||||
progress: Number(projectData.progress || 0),
|
||||
progress_type: projectData.progress_type === "auto" ? "auto" : "manual",
|
||||
revision_quota: Number(projectData.revision_quota || 0),
|
||||
cover_image_alt: projectData.cover_image_alt,
|
||||
coverImageUrl,
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
Settings2,
|
||||
Target,
|
||||
Trash2,
|
||||
Wallet,
|
||||
@@ -58,6 +59,8 @@ export type ProjectDetail = {
|
||||
budget_amount: number | null;
|
||||
currency: string;
|
||||
progress: number;
|
||||
progress_type: "manual" | "auto";
|
||||
revision_quota: number;
|
||||
cover_image_alt: string | null;
|
||||
coverImageUrl: string | null;
|
||||
};
|
||||
@@ -215,6 +218,7 @@ export function ProjectDetailClient({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ProjectSettingsDialog project={project} />
|
||||
<SectionDialog projectId={project.id} mode="create" defaultCategory="overview" />
|
||||
{project.status !== "completed" ? (
|
||||
<form action={completeProjectRecord}>
|
||||
@@ -806,6 +810,101 @@ function ProjectTaskKanban({
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectSettingsDialog({ project }: { project: ProjectDetail }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [progressType, setProgressType] = useState<"manual" | "auto">(project.progress_type);
|
||||
const [progress, setProgress] = useState(project.progress);
|
||||
const [revisionQuota, setRevisionQuota] = useState(project.revision_quota);
|
||||
|
||||
async function handleSubmit() {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const { updateProjectSettings } = await import("@/app/(dashboard)/projects/actions");
|
||||
await updateProjectSettings(project.id, progressType, progress, revisionQuota);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="h-9 gap-2 px-3">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Ayarlar</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<form action={handleSubmit} className="space-y-5">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Proje ayarları</DialogTitle>
|
||||
<DialogDescription>
|
||||
İlerleme hesaplama yöntemi ve revizyon kotasını belirle.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>İlerleme Hesaplama</Label>
|
||||
<Select value={progressType} onValueChange={(val: "manual" | "auto") => setProgressType(val)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="manual">Manuel (Elle girilir)</SelectItem>
|
||||
<SelectItem value="auto">Otomatik (Görevlere göre)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{progressType === "manual" && (
|
||||
<div className="grid gap-2">
|
||||
<Label>İlerleme Durumu (%)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={progress}
|
||||
onChange={(e) => setProgress(Number(e.target.value))}
|
||||
className="flex-1 accent-primary"
|
||||
/>
|
||||
<span className="w-12 text-right text-sm font-medium">{progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{progressType === "auto" && (
|
||||
<p className="text-xs text-muted-foreground">İlerleme yüzdesi "Görevler" sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.</p>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Müşteri Revizyon Kotası</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={revisionQuota}
|
||||
onChange={(e) => setRevisionQuota(Number(e.target.value))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Müşterinin portal üzerinden talep edebileceği toplam revizyon hakkı.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ProjectTaskDialog({
|
||||
projectId,
|
||||
clientId,
|
||||
|
||||
@@ -336,3 +336,28 @@ export async function updateRevisionStatus(id: string, projectId: string, status
|
||||
|
||||
revalidatePath(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
export async function updateProjectSettings(projectId: string, progressType: "manual" | "auto", progress: number, revisionQuota: number) {
|
||||
const { supabase, userId } = await getCurrentUserId();
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error("Proje ID zorunludur.");
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("projects")
|
||||
.update({
|
||||
progress_type: progressType,
|
||||
progress: progress,
|
||||
revision_quota: revisionQuota
|
||||
})
|
||||
.eq("id", projectId)
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Ayarlar güncellenemedi: ${error.message}`);
|
||||
}
|
||||
|
||||
revalidatePath("/projects");
|
||||
revalidatePath(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
+1
-17
@@ -67,23 +67,7 @@ export default async function LoginPage({
|
||||
</Button>
|
||||
</form>
|
||||
}
|
||||
secondaryAction={
|
||||
<div className="space-y-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">veya</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="button" variant="outline" className="h-11 w-full gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
Google ile devam et
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
secondaryAction={null}
|
||||
footer={
|
||||
<div className="text-center text-sm">
|
||||
Hesabın yok mu?{" "}
|
||||
|
||||
@@ -39,6 +39,24 @@ export default async function PortalLayout({
|
||||
.join("")
|
||||
.slice(0, 2) || "MS";
|
||||
|
||||
const { data: clientData } = await supabase
|
||||
.from("clients")
|
||||
.select("id")
|
||||
.eq("client_auth_id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
let avgProgress = 0;
|
||||
if (clientData) {
|
||||
const { data: projectsData } = await supabase
|
||||
.from("projects")
|
||||
.select("progress")
|
||||
.eq("client_id", clientData.id)
|
||||
.eq("status", "active");
|
||||
if (projectsData && projectsData.length > 0) {
|
||||
avgProgress = Math.round(projectsData.reduce((sum, p) => sum + p.progress, 0) / projectsData.length);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalShell
|
||||
user={{
|
||||
@@ -47,6 +65,7 @@ export default async function PortalLayout({
|
||||
shortName,
|
||||
avatarUrl: profile?.avatar_url || null,
|
||||
}}
|
||||
progress={avgProgress}
|
||||
>
|
||||
{children}
|
||||
</PortalShell>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export default async function PortalDashboardPage() {
|
||||
const completedProjects = projects.filter(p => p.status === 'completed');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Hoş Geldiniz, {clientData.name}</h1>
|
||||
<p className="text-muted-foreground">İş süreçlerinizi ve aktif projelerinizi buradan takip edebilirsiniz.</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function PortalProjectClient({ project, sections, tasks, revisions, clien
|
||||
const pendingRevisions = revisions.filter((r: any) => r.status === 'pending' || r.status === 'in_progress').length;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-5xl flex-col gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{/* Header Info */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MessageSquareDiff } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function PortalRevisionsPage() {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Revizyon Taleplerim</h1>
|
||||
<p className="text-muted-foreground">İlettiğiniz tüm revizyon taleplerinin durumunu buradan takip edebilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-72 flex-col items-center justify-center rounded-sm border border-dashed border-border bg-muted/20 p-8 text-center">
|
||||
<MessageSquareDiff className="h-10 w-10 text-muted-foreground" />
|
||||
<h3 className="mt-4 text-lg font-semibold text-foreground">
|
||||
Revizyon modülü yapım aşamasında
|
||||
</h3>
|
||||
<p className="mt-2 max-w-md text-sm text-muted-foreground">
|
||||
Yakında tüm revizyon taleplerinizi buradan yönetebileceksiniz. Şimdilik proje detay sayfasından revizyon talep edebilirsiniz.
|
||||
</p>
|
||||
<Link href="/portal" className="mt-4 text-primary hover:underline text-sm font-medium">
|
||||
Dashboard'a dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FolderKanban } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function PortalTasksPage() {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Görevlerim</h1>
|
||||
<p className="text-muted-foreground">Size atanan ve herkese açık olan proje görevleri.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-72 flex-col items-center justify-center rounded-sm border border-dashed border-border bg-muted/20 p-8 text-center">
|
||||
<FolderKanban className="h-10 w-10 text-muted-foreground" />
|
||||
<h3 className="mt-4 text-lg font-semibold text-foreground">
|
||||
Görev modülü yapım aşamasında
|
||||
</h3>
|
||||
<p className="mt-2 max-w-md text-sm text-muted-foreground">
|
||||
Yakında tüm görevlerinizi buradan takip edebileceksiniz. Şimdilik proje detay sayfasından görevlere ulaşabilirsiniz.
|
||||
</p>
|
||||
<Link href="/portal" className="mt-4 text-primary hover:underline text-sm font-medium">
|
||||
Dashboard'a dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-17
@@ -59,23 +59,7 @@ export default async function RegisterPage({
|
||||
</Button>
|
||||
</form>
|
||||
}
|
||||
secondaryAction={
|
||||
<div className="space-y-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">veya</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="button" variant="outline" className="h-11 w-full gap-2">
|
||||
<Search className="h-4 w-4" />
|
||||
Google ile devam et
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
secondaryAction={null}
|
||||
footer={
|
||||
<div className="text-center text-sm">
|
||||
Zaten hesabın var mı?{" "}
|
||||
|
||||
Reference in New Issue
Block a user