first commit

This commit is contained in:
Your NamebaishaliHolocron
2026-06-15 12:57:03 +05:30
commit b9ac5ae0b2
398 changed files with 49583 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
"use client";
import { useState, useEffect } from "react";
import {
Button,
Badge,
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
Separator,
Skeleton,
} from "ikoncomponents";
import MeetingDetailsForm from "./components/meetingForm/MeetingDetailsForm";
import { CalendarDays, MapPin, Clock, Users, ListChecks, Zap, Search } from "lucide-react";
import { createMeetingApi, meetingsApi, updateMeetingApi } from "@/app/utils/api/meetingApi";
interface MeetingResponse {
id: string;
meeting: {
title: string;
projectName: string;
place: string;
date: string;
time: string;
duration: string;
calledBy: string;
};
attendees: {
attendees: { name: string; role: string; contact: string }[];
};
agenda: {
agenda: { item: string; owner: string }[];
};
decisions: {
decisions: string[];
};
actions: {
actions: { item: string; responsible: string; dueDate: string }[];
};
others: {
notes: string;
};
projectIdentifier: string;
status: "Draft" | "Completed";
createdAt: string;
updatedAt: string;
}
interface MomTabProps {
projectIdentifier: string;
projectName?: string;
}
export default function MomTab({ projectIdentifier, projectName = "" }: MomTabProps) {
const [open, setOpen] = useState(false);
const [editMeeting, setEditMeeting] = useState<MeetingResponse | null>(null);
const [meetings, setMeetings] = useState<MeetingResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeStatus, setActiveStatus] = useState<"Draft" | "Completed">("Draft");
const [search, setSearch] = useState("");
const fetchMeetings = async () => {
setLoading(true);
setError(null);
try {
const response = await meetingsApi(projectIdentifier);
if (response && Array.isArray(response)) {
setMeetings(response);
} else {
setMeetings([]);
}
} catch (err) {
setError("Failed to load meetings.");
} finally {
setLoading(false);
}
};
const handleSave = async (data): Promise<void> => {
if (editMeeting) {
await updateMeetingApi(editMeeting.id, data);
} else {
await createMeetingApi(data);
}
setOpen(false);
setEditMeeting(null);
fetchMeetings();
};
const handleEditClick = (meeting: MeetingResponse) => {
// Always create a new object so initialData reference changes
// even if the user clicks the same meeting twice
setEditMeeting({ ...meeting });
setOpen(true);
};
useEffect(() => {
fetchMeetings();
}, [projectIdentifier]);
const filtered = meetings
.filter((m) => m.status === activeStatus)
.filter((m) =>
search.trim() === "" ||
m.meeting.title?.toLowerCase().includes(search.toLowerCase()) ||
m.meeting.place?.toLowerCase().includes(search.toLowerCase())
);
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" });
};
const formatTime = (timeStr: string) => {
if (!timeStr || !timeStr.includes(":")) return "—";
const [h, m] = timeStr.split(":");
const hour = parseInt(h, 10);
const ampm = hour >= 12 ? "PM" : "AM";
const displayHour = hour % 12 || 12;
return `${displayHour}:${m} ${ampm}`;
};
return (
<>
<div className="p-6 space-y-4">
{/* Header Row — toggle + search + button all in one line */}
<div className="flex items-center gap-3 flex-wrap">
{/* Draft / Completed Toggle */}
<div className="flex items-center bg-muted rounded-lg p-1 gap-1 shrink-0">
<Button
variant={activeStatus === "Draft" ? "secondary" : "ghost"}
size="sm"
onClick={() => setActiveStatus("Draft")}
className="rounded-md px-4"
>
Draft
</Button>
<Button
variant={activeStatus === "Completed" ? "secondary" : "ghost"}
size="sm"
onClick={() => setActiveStatus("Completed")}
className="rounded-md px-4"
>
Completed
</Button>
</div>
{/* Search */}
<div className="relative flex-1 min-w-[160px]">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search meetings..."
className="w-full h-8 pl-8 pr-3 text-xs rounded-md border border-input bg-background focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
{/* New Meeting */}
<Button
size="sm"
className="shrink-0"
onClick={() => { setEditMeeting(null); setOpen(true); }}
>
+ New Meeting
</Button>
</div>
{/* Loading */}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{[1, 2, 3].map((i) => (
<Card key={i} className="overflow-hidden">
<CardHeader className="space-y-2 pb-3">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-2/3" />
</CardContent>
</Card>
))}
</div>
) : error ? (
<div className="flex items-center justify-center py-16 text-destructive text-sm">
{error}
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 gap-3">
<CalendarDays className="w-12 h-12 text-muted-foreground opacity-40" />
<p className="text-sm text-muted-foreground font-medium">
{search ? "No meetings match your search" : `No ${activeStatus} meetings found`}
</p>
{!search && (
<Button variant="outline" size="sm" onClick={() => { setEditMeeting(null); setOpen(true); }}>
Create one
</Button>
)}
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-2 gap-3">
{filtered.map((m) => (
<MeetingCard
key={m.id}
meeting={m}
formatDate={formatDate}
formatTime={formatTime}
onEdit={handleEditClick}
/>
))}
</div>
)}
</div>
<MeetingDetailsForm
open={open}
setOpen={(val) => {
setOpen(val);
if (!val) setEditMeeting(null);
}}
projectIdentifier={projectIdentifier}
projectName={projectName}
initialData={editMeeting}
onSubmit={(data) => handleSave(data)}
/>
</>
);
}
// ─── Meeting Card ──────────────────────────────────────────────────────────────
interface MeetingCardProps {
meeting: MeetingResponse;
formatDate: (d: string) => string;
formatTime: (t: string) => string;
onEdit: (meeting: MeetingResponse) => void;
}
function MeetingCard({ meeting, formatDate, formatTime, onEdit }: MeetingCardProps) {
const { meeting: m, attendees, agenda, decisions, actions, status } = meeting;
const isCompleted = status === "Completed";
return (
<Card className="flex flex-col overflow-hidden hover:shadow-md transition-shadow duration-200">
{/* Accent bar */}
<div className={`h-1 w-full ${isCompleted ? "bg-emerald-400" : "bg-amber-400"}`} />
<CardHeader className="pb-1 pt-2 px-3">
<div className="flex items-start justify-between gap-1">
<CardTitle className="text-[15px] font-semibold line-clamp-1 flex-1">
{m.title}
</CardTitle>
<Badge
variant="outline"
className={`text-[13px] px-1 py-0 shrink-0 ${
isCompleted
? "bg-emerald-50 text-emerald-600 border-emerald-200"
: "bg-amber-50 text-amber-600 border-amber-200"
}`}
>
{status}
</Badge>
</div>
{/* Meta — single line */}
<div className="flex items-center gap-1 text-[10px] text-muted-foreground flex-wrap pt-0.5">
<CalendarDays className="w-2.5 h-2.5 shrink-0" />
<span>{formatDate(m.date)}</span>
<span className="opacity-40">·</span>
<Clock className="w-2.5 h-2.5 shrink-0" />
<span>{formatTime(m.time)}</span>
<span className="opacity-40">·</span>
<span>{m.duration} min</span>
<span className="opacity-40">·</span>
<MapPin className="w-2.5 h-2.5 shrink-0" />
<span className="line-clamp-1">{m.place}</span>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-1 flex-1 pt-0 px-3 pb-2">
<Separator />
{/* Stats row — compact */}
<div className="grid grid-cols-3 gap-1 text-center">
<StatPill
icon={<Users className="w-2.5 h-2.5 mx-auto mb-0.5 text-muted-foreground" />}
label="Attendees"
value={attendees.attendees.length}
/>
<StatPill
icon={<ListChecks className="w-2.5 h-2.5 mx-auto mb-0.5 text-muted-foreground" />}
label="Agenda"
value={agenda.agenda.length}
/>
<StatPill
icon={<Zap className="w-2.5 h-2.5 mx-auto mb-0.5 text-muted-foreground" />}
label="Actions"
value={actions.actions.length}
/>
</div>
{/* Completed → Key Decisions */}
{isCompleted && decisions.decisions.length > 0 && (
<div className="space-y-0.5">
<p className="text-[9px] font-semibold text-muted-foreground uppercase tracking-wide">
Key Decisions
</p>
<li className="flex items-start gap-1 text-[10px] text-foreground list-none">
<span className="mt-1 w-1 h-1 rounded-full bg-emerald-400 shrink-0" />
<span className="line-clamp-1">{decisions.decisions[0]}</span>
</li>
{decisions.decisions.length > 1 && (
<p className="text-[9px] text-muted-foreground pl-2">
+{decisions.decisions.length - 1} more
</p>
)}
</div>
)}
{/* Draft → Agenda Preview */}
{!isCompleted && agenda.agenda.length > 0 && (
<div className="space-y-1 ">
<p className="text-[9px] mt-1 font-semibold text-muted-foreground uppercase tracking-wide">
Agenda
</p>
<li className="flex items-start gap-1 text-[11px] text-foreground list-none">
<span className="mt-1 w-1 h-1 rounded-full bg-amber-400 shrink-0" />
<span className="line-clamp-1">{agenda.agenda[0].item}</span>
</li>
{agenda.agenda.length > 1 && (
<p className="text-[10px] text-muted-foreground pl-2">
+{agenda.agenda.length - 1} more
</p>
)}
</div>
)}
</CardContent>
<CardFooter className="border-t bg-muted/40 px-3 py-1.5 flex items-center justify-between">
<span className="text-[9px] text-muted-foreground">
{formatDate(meeting.createdAt)}
</span>
{!isCompleted && (
<Button
size="sm"
variant="outline"
className="h-5 text-[12px] px-1.5 border p-3"
onClick={() => onEdit(meeting)}
>
Continue Editing
</Button>
)}
</CardFooter>
</Card>
);
}
// ─── Stat Pill ─────────────────────────────────────────────────────────────────
function StatPill({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: number;
}) {
return (
<div className="bg-muted rounded-md py-1.5 px-1 flex flex-col items-center">
{icon}
<p className="text-xs font-semibold text-foreground">{value}</p>
<p className="text-[9px] text-muted-foreground">{label}</p>
</div>
);
}