Files
route-commerce/src/components/admin/StopMessagingForm.tsx
T
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

241 lines
8.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { getStopPendingCustomers, type StopCustomer } from "@/actions/stops/get-stop-customers";
import { sendStopBlast } from "@/actions/communications/stop-blast";
import { AdminButton } from "@/components/admin/design-system";
const quickMessages = [
{ label: "Truck running late", value: "Heads up — the truck is running about 15 minutes behind schedule. Thanks for your patience!" },
{ label: "Stop moved", value: "Attention — today's stop location has changed to a new address. Please check the updated details." },
{ label: "Weather delay", value: "Due to weather conditions, today's stop may be delayed. We'll send updates as the day progresses." },
{ label: "Sold out", value: "This stop has sold out of several items. Check our website or next stop for availability." },
{ label: "Preorder cutoff reminder", value: "Friendly reminder — the preorder cutoff for our next stop is tonight at midnight. Order online to guarantee your pickup!" },
{ label: "Pickup reminder", value: "Reminder — you have an order ready for pickup today. See you soon!" },
];
export default function StopMessagingForm({
stopId,
brandId,
}: {
stopId: string;
brandId: string;
}) {
const [customers, setCustomers] = useState<StopCustomer[]>([]);
const [loaded, setLoaded] = useState(false);
const [loading, setLoading] = useState(false);
const [channel, setChannel] = useState<"sms" | "email" | "both">("sms");
const [message, setMessage] = useState("");
const [customMessage, setCustomMessage] = useState("");
const [sending, setSending] = useState(false);
const [sent, setSent] = useState(0);
const [error, setError] = useState<string | null>(null);
async function loadCustomers() {
setLoading(true);
setError(null);
const result = await getStopPendingCustomers(stopId);
if (!result.success) {
setError(result.error);
setLoading(false);
return;
}
setCustomers(result.customers);
setLoaded(true);
setLoading(false);
}
function applyQuickMessage(msg: string) {
setMessage(msg);
setCustomMessage(msg);
}
async function handleSend() {
if (!message.trim()) return;
setSending(true);
setError(null);
const blast = await sendStopBlast({
stopId,
brandId,
channel,
body: message,
audience: "pending",
});
if (!blast.success) {
setError(blast.error);
setSending(false);
return;
}
setSent(blast.messages_logged);
setSending(false);
}
const recipients = customers.filter((c) => {
if (channel === "sms") return c.customer_phone;
if (channel === "email") return c.customer_email;
return c.customer_phone || c.customer_email;
});
return (
<div className="space-y-6">
<div>
<h2 className="ha-display text-2xl text-[var(--admin-text-primary)]">
Send Message
</h2>
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Notify all customers with pending pickups at this stop.
</p>
</div>
{!loaded ? (
<button type="button"
onClick={loadCustomers}
disabled={loading}
className="w-full rounded-xl border-2 border-dashed border-[var(--admin-border)] px-6 py-4 text-lg font-medium text-[var(--admin-text-secondary)] hover:border-[var(--admin-primary)] hover:text-[var(--admin-primary)] transition-colors disabled:opacity-50"
>
{loading ? "Loading customers..." : "Load Pending Customers"}
</button>
) : (
<>
{customers.length === 0 ? (
<div className="rounded-xl bg-[var(--admin-bg-subtle)] p-6 text-center text-[var(--admin-text-muted)]">
No pending orders for this stop yet.
</div>
) : (
<div
className="rounded-xl p-4 text-sm text-[var(--admin-success)]"
style={{ background: "var(--admin-success-soft)" }}
>
{customers.length} pending order{customers.length !== 1 ? "s" : ""} found
{recipients.length !== customers.length && (
<span> {recipients.length} with contact info</span>
)}
</div>
)}
{/* Channel */}
<div>
<label className="ha-field-label mb-2">
<span>Send via</span>
</label>
<div className="flex gap-2">
{(["sms", "email", "both"] as const).map((ch) => (
<button type="button"
key={ch}
onClick={() => setChannel(ch)}
className={`flex-1 rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
channel === ch
? "bg-[var(--admin-primary)] text-white"
: "bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] hover:bg-[var(--admin-primary-soft)] hover:text-[var(--admin-primary)]"
}`}
>
{ch === "sms" ? "SMS" : ch === "email" ? "Email" : "Both"}
</button>
))}
</div>
</div>
{/* Quick messages */}
<div>
<label className="ha-field-label mb-2">
<span>Quick messages</span>
</label>
<div className="flex flex-wrap gap-2">
{quickMessages.map((qm) => (
<button type="button"
key={qm.label}
onClick={() => applyQuickMessage(qm.value)}
className={`rounded-full px-3 py-1 text-sm font-medium transition-colors ${
message === qm.value
? "bg-[var(--admin-primary)] text-white"
: "bg-[var(--admin-bg-subtle)] text-[var(--admin-text-secondary)] hover:bg-[var(--admin-primary-soft)] hover:text-[var(--admin-primary)]"
}`}
>
{qm.label}
</button>
))}
</div>
</div>
{/* Message preview */}
<div>
<label className="ha-field-label mb-2">
<span>Message</span>
</label>
<textarea aria-label="Type Your Message..."
value={message}
onChange={(e) => {
setMessage(e.target.value);
setCustomMessage(e.target.value);
}}
rows={4}
className="ha-field-textarea"
placeholder="Type your message..."
/>
<p className="mt-1 text-xs text-[var(--admin-text-muted)] tabular-nums">
{message.length} characters
</p>
</div>
{error && (
<div
role="alert"
className="rounded-xl p-4 text-sm text-[var(--admin-danger)]"
style={{ background: "var(--admin-danger-soft)" }}
>
{error}
</div>
)}
{sent > 0 && (
<div
className="rounded-xl p-4 text-sm text-[var(--admin-success)]"
style={{ background: "var(--admin-success-soft)" }}
>
Message sent to {sent} customer{sent !== 1 ? "s" : ""}!
</div>
)}
{/* Recipients */}
{recipients.length > 0 && message && (
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-4">
<p className="mb-3 text-sm font-medium text-[var(--admin-text-secondary)]">
Recipients ({recipients.length}):
</p>
<div className="space-y-2">
{recipients.map((c) => (
<div key={c.id} className="flex justify-between text-sm">
<span className="text-[var(--admin-text-primary)]">{c.customer_name}</span>
<span className="text-[var(--admin-text-muted)] tabular-nums">
{c.customer_phone ?? c.customer_email ?? "no contact"}
</span>
</div>
))}
</div>
</div>
)}
<AdminButton
variant="primary"
size="lg"
onClick={handleSend}
disabled={!message || recipients.length === 0 || sending}
isLoading={sending}
className="w-full"
>
{sending
? "Sending..."
: `Send to ${recipients.length} customer${recipients.length !== 1 ? "s" : ""}`}
</AdminButton>
</>
)}
</div>
);
}