diff --git a/client/src/app.tsx b/client/src/app.tsx index ba9a5a5..9733584 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -4,6 +4,8 @@ import { Layout } from "./components/Layout"; import { ReloadPrompt } from "./components/ReloadPrompt"; import { ConnectWhatsApp } from "./pages/ConnectWhatsApp"; import { CreateSchedule } from "./pages/CreateSchedule"; +import { Contacts } from "./pages/Contacts"; +import { Broadcasts } from "./pages/Broadcasts"; import { Feedbacks } from "./pages/Feedbacks"; import { Schedules } from "./pages/Schedules"; import { Settings } from "./pages/Settings"; @@ -50,6 +52,8 @@ export function App() { + + diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index e0cf19c..4e8743f 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,4 +1,11 @@ -import { Calendar, MessageSquare, Plus, Settings } from "lucide-preact"; +import { + Calendar, + MessageSquare, + Megaphone, + Plus, + Settings, + Users, +} from "lucide-preact"; import { Link, useLocation } from "wouter-preact"; export function Layout({ children }: { children: any }) { @@ -7,6 +14,8 @@ export function Layout({ children }: { children: any }) { const navItems = [ { href: "/", icon: Calendar, label: "Schedules" }, { href: "/create", icon: Plus, label: "Create" }, + { href: "/contacts", icon: Users, label: "Contacts" }, + { href: "/broadcasts", icon: Megaphone, label: "Broadcasts" }, { href: "/feedbacks", icon: MessageSquare, label: "Feedbacks" }, { href: "/settings", icon: Settings, label: "Settings" }, ]; diff --git a/client/src/pages/Broadcasts.tsx b/client/src/pages/Broadcasts.tsx new file mode 100644 index 0000000..9b55ab2 --- /dev/null +++ b/client/src/pages/Broadcasts.tsx @@ -0,0 +1,327 @@ +import type { Broadcast, Contact, CreateBroadcastDto } from "@shared/types"; +import { Megaphone, Plus, Trash2 } from "lucide-preact"; +import { useEffect, useState } from "preact/hooks"; +import { api } from "../services/api"; +import "./common.css"; + +export function Broadcasts() { + const [broadcasts, setBroadcasts] = useState([]); + const [contacts, setContacts] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreate, setShowCreate] = useState(false); + + const [formData, setFormData] = useState({ + name: "", + message: "", + contactIds: [], + scheduledTime: undefined, + type: "instant", + intervalValue: 1, + intervalUnit: "day", + }); + const [attachment, setAttachment] = useState(null); + + const loadData = async () => { + try { + const [bData, cData] = await Promise.all([ + api.getBroadcasts(), + api.getContacts(), + ]); + setBroadcasts(bData); + setContacts(cData); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, []); + + const handleDelete = async (id: number) => { + if (confirm("Delete this broadcast?")) { + await api.deleteBroadcast(id); + loadData(); + } + }; + + const handleCreate = async (e: Event) => { + e.preventDefault(); + try { + if (formData.contactIds.length === 0) { + alert("Please select at least one contact"); + return; + } + + const form = new FormData(); + form.append("name", formData.name); + form.append("message", formData.message); + form.append("contactIds", JSON.stringify(formData.contactIds)); + form.append("type", formData.type || "instant"); + + if (formData.type !== "instant" && formData.scheduledTime) { + form.append("scheduledTime", formData.scheduledTime); + } + + if (formData.type === "recurring") { + if (formData.intervalValue) + form.append("intervalValue", formData.intervalValue.toString()); + if (formData.intervalUnit) + form.append("intervalUnit", formData.intervalUnit); + } + + if (attachment) { + form.append("attachment", attachment); + } + + await api.createBroadcast(form); + setShowCreate(false); + setAttachment(null); + setFormData({ + name: "", + message: "", + contactIds: [], + scheduledTime: undefined, + type: "instant", + intervalValue: 1, + intervalUnit: "day", + }); + loadData(); + } catch (err: any) { + alert(err.message); + } + }; + + const toggleContact = (id: number) => { + const current = formData.contactIds; + if (current.includes(id)) { + setFormData({ + ...formData, + contactIds: current.filter((cid) => cid !== id), + }); + } else { + setFormData({ ...formData, contactIds: [...current, id] }); + } + }; + + const toggleSelectAll = () => { + if (formData.contactIds.length === contacts.length) { + setFormData({ ...formData, contactIds: [] }); + } else { + setFormData({ ...formData, contactIds: contacts.map((c) => c.id) }); + } + }; + + if (loading) return
Loading...
; + + return ( +
+ + + {showCreate && ( +
+

New Broadcast

+
+ + + setFormData({ ...formData, name: e.currentTarget.value }) + } + required + /> +
+ +
+ +
+ {(["instant", "once", "recurring"] as const).map((t) => ( + + ))} +
+
+ + {formData.type !== "instant" && ( +
+ + + setFormData({ + ...formData, + scheduledTime: e.currentTarget.value, + }) + } + required + /> +
+ )} + + {formData.type === "recurring" && ( +
+
+ + + setFormData({ + ...formData, + intervalValue: parseInt(e.currentTarget.value), + }) + } + required + /> +
+
+ + +
+
+ )} + +
+ +