mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
fix(pipelines): size columns to fill, batch stage saves, inline delete confirm
Sizing: - Board columns grow to fill wide viewports (flex-1 basis-[260px]) instead of being capped at 300px; still overflow-scroll on narrow screens. - Analytics grid: 2 cols mobile → 3 cols sm → 6 cols xl (was lg, which cramped labels on 1024-1280px widths). Senior-review pass: - pipeline-settings handleSave: replace N sequential UPDATEs in a for-loop with a single upsert (onConflict: "id"). Previously latency scaled linearly with stage count; now constant. - deal-form delete: swap window.confirm for an inline confirm row that matches the app's pattern (pipeline-settings uses the same inline confirmation for pipeline deletion). - pipeline-board: add KeyboardSensor alongside PointerSensor so deal cards are draggable via keyboard for a11y (Space to pick up, arrows to move). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4229c94ec5
commit
03fbead9c8
@ -69,10 +69,12 @@ export function DealForm({
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [statusAction, setStatusAction] = useState<DealStatus | null>(null);
|
const [statusAction, setStatusAction] = useState<DealStatus | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
// Reset when opening
|
// Reset when opening
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
setConfirmDelete(false);
|
||||||
if (deal) {
|
if (deal) {
|
||||||
setTitle(deal.title);
|
setTitle(deal.title);
|
||||||
setValue(String(deal.value ?? ""));
|
setValue(String(deal.value ?? ""));
|
||||||
@ -211,10 +213,6 @@ export function DealForm({
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!deal) return;
|
if (!deal) return;
|
||||||
const confirmed = window.confirm(
|
|
||||||
"Delete this deal? This cannot be undone.",
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
const { error } = await supabase.from("deals").delete().eq("id", deal.id);
|
const { error } = await supabase.from("deals").delete().eq("id", deal.id);
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
@ -223,6 +221,7 @@ export function DealForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success("Deal deleted");
|
toast.success("Deal deleted");
|
||||||
|
setConfirmDelete(false);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSaved();
|
onSaved();
|
||||||
}
|
}
|
||||||
@ -426,17 +425,39 @@ export function DealForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deal && (
|
{deal &&
|
||||||
<button
|
(confirmDelete ? (
|
||||||
type="button"
|
<div className="mt-3 flex items-center justify-between gap-2 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs">
|
||||||
onClick={handleDelete}
|
<span className="text-red-300">Delete this deal?</span>
|
||||||
disabled={deleting}
|
<div className="flex gap-1">
|
||||||
className="mt-3 flex w-full items-center justify-center gap-1 text-xs text-red-400 hover:text-red-300"
|
<button
|
||||||
>
|
type="button"
|
||||||
<Trash2 className="h-3 w-3" />
|
onClick={() => setConfirmDelete(false)}
|
||||||
{deleting ? "Deleting..." : "Delete Deal"}
|
disabled={deleting}
|
||||||
</button>
|
className="rounded px-2 py-1 text-slate-300 hover:bg-slate-800"
|
||||||
)}
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
className="rounded bg-red-600 px-2 py-1 font-medium text-white hover:bg-red-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{deleting ? "Deleting..." : "Confirm"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setConfirmDelete(true)}
|
||||||
|
className="mt-3 flex w-full items-center justify-center gap-1 text-xs text-red-400 hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
Delete Deal
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-800 bg-slate-900/60 p-4 sm:grid-cols-3 lg:grid-cols-6">
|
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-800 bg-slate-900/60 p-4 sm:grid-cols-3 xl:grid-cols-6">
|
||||||
<Metric
|
<Metric
|
||||||
icon={<BarChart3 className="h-4 w-4 text-slate-400" />}
|
icon={<BarChart3 className="h-4 w-4 text-slate-400" />}
|
||||||
label="Total Deals"
|
label="Total Deals"
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
|
KeyboardSensor,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@ -62,6 +63,9 @@ export function PipelineBoard({
|
|||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
// 5px activation distance avoids clicks being interpreted as drags.
|
// 5px activation distance avoids clicks being interpreted as drags.
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
// Keyboard drag support: focus a card, Space to pick up, arrows to move,
|
||||||
|
// Space to drop, Escape to cancel.
|
||||||
|
useSensor(KeyboardSensor),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeDeal = activeDealId
|
const activeDeal = activeDealId
|
||||||
@ -172,7 +176,7 @@ function StageColumn({
|
|||||||
const { setNodeRef, isOver } = useDroppable({ id: stage.id });
|
const { setNodeRef, isOver } = useDroppable({ id: stage.id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-[280px] max-w-[300px] shrink-0 flex-col rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
<div className="flex min-w-[260px] flex-1 basis-[260px] flex-col rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
||||||
{/* 3px colored top border — sits above the column's padding */}
|
{/* 3px colored top border — sits above the column's padding */}
|
||||||
<div
|
<div
|
||||||
className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
|
className="-mx-4 -mt-4 h-[3px] rounded-t-xl"
|
||||||
|
|||||||
@ -100,26 +100,33 @@ export function PipelineSettings({
|
|||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const { error: renameErr } = await supabase
|
|
||||||
.from("pipelines")
|
// One upsert for all stages — batches N stage writes into a single
|
||||||
.update({ name: name.trim() })
|
// round-trip. Previous implementation did N sequential UPDATEs which
|
||||||
.eq("id", pipeline.id);
|
// latency-scaled linearly with stage count.
|
||||||
if (renameErr) {
|
const stageRows = localStages.map((s, i) => ({
|
||||||
toast.error("Failed to rename pipeline");
|
id: s.id,
|
||||||
setSaving(false);
|
pipeline_id: s.pipeline_id,
|
||||||
|
name: s.name,
|
||||||
|
color: s.color,
|
||||||
|
position: i,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [renameRes, stagesRes] = await Promise.all([
|
||||||
|
supabase
|
||||||
|
.from("pipelines")
|
||||||
|
.update({ name: name.trim() })
|
||||||
|
.eq("id", pipeline.id),
|
||||||
|
supabase.from("pipeline_stages").upsert(stageRows, { onConflict: "id" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSaving(false);
|
||||||
|
|
||||||
|
if (renameRes.error || stagesRes.error) {
|
||||||
|
toast.error("Failed to save pipeline");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist each stage's name/color/position
|
|
||||||
for (let i = 0; i < localStages.length; i++) {
|
|
||||||
const s = localStages[i];
|
|
||||||
await supabase
|
|
||||||
.from("pipeline_stages")
|
|
||||||
.update({ name: s.name, color: s.color, position: i })
|
|
||||||
.eq("id", s.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(false);
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onPipelinesChanged();
|
onPipelinesChanged();
|
||||||
onStagesChanged();
|
onStagesChanged();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user