update: Major Overhaul to all subsystems
This commit is contained in:
579
frontend/src/crm/customers/CustomerForm.jsx
Normal file
579
frontend/src/crm/customers/CustomerForm.jsx
Normal file
@@ -0,0 +1,579 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import api from "../../api/client";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
|
||||
const CONTACT_TYPES = ["email", "phone", "whatsapp", "other"];
|
||||
const LANGUAGES = [
|
||||
{ value: "el", label: "Greek" },
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "de", label: "German" },
|
||||
{ value: "fr", label: "French" },
|
||||
{ value: "it", label: "Italian" },
|
||||
];
|
||||
const TITLES = ["", "Fr.", "Rev.", "Archim.", "Bp.", "Abp.", "Met.", "Mr.", "Mrs.", "Ms.", "Dr.", "Prof."];
|
||||
const PRESET_TAGS = ["church", "monastery", "municipality", "school", "repeat-customer", "vip", "pending", "inactive"];
|
||||
|
||||
const CONTACT_TYPE_ICONS = {
|
||||
email: "📧",
|
||||
phone: "📞",
|
||||
whatsapp: "💬",
|
||||
other: "🔗",
|
||||
};
|
||||
|
||||
const inputClass = "w-full px-3 py-2 text-sm rounded-md border";
|
||||
const inputStyle = {
|
||||
backgroundColor: "var(--bg-input)",
|
||||
borderColor: "var(--border-primary)",
|
||||
color: "var(--text-primary)",
|
||||
};
|
||||
const labelStyle = {
|
||||
display: "block",
|
||||
marginBottom: 4,
|
||||
fontSize: 12,
|
||||
color: "var(--text-secondary)",
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
function Field({ label, children, style }) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<label style={labelStyle}>{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionCard({ title, children }) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border p-5 mb-4"
|
||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||
>
|
||||
<h2 className="text-sm font-semibold mb-4" style={{ color: "var(--text-heading)" }}>{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyContact = () => ({ type: "email", label: "", value: "", primary: false });
|
||||
|
||||
export default function CustomerForm() {
|
||||
const { id } = useParams();
|
||||
const isEdit = Boolean(id);
|
||||
const navigate = useNavigate();
|
||||
const { user, hasPermission } = useAuth();
|
||||
const canEdit = hasPermission("crm", "edit");
|
||||
|
||||
const [form, setForm] = useState({
|
||||
title: "",
|
||||
name: "",
|
||||
surname: "",
|
||||
organization: "",
|
||||
language: "el",
|
||||
tags: [],
|
||||
folder_id: "",
|
||||
location: { city: "", country: "", region: "" },
|
||||
contacts: [],
|
||||
notes: [],
|
||||
});
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [newNoteText, setNewNoteText] = useState("");
|
||||
const [editingNoteIdx, setEditingNoteIdx] = useState(null);
|
||||
const [editingNoteText, setEditingNoteText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) return;
|
||||
api.get(`/crm/customers/${id}`)
|
||||
.then((data) => {
|
||||
setForm({
|
||||
title: data.title || "",
|
||||
name: data.name || "",
|
||||
surname: data.surname || "",
|
||||
organization: data.organization || "",
|
||||
language: data.language || "el",
|
||||
tags: data.tags || [],
|
||||
folder_id: data.folder_id || "",
|
||||
location: {
|
||||
city: data.location?.city || "",
|
||||
country: data.location?.country || "",
|
||||
region: data.location?.region || "",
|
||||
},
|
||||
contacts: data.contacts || [],
|
||||
notes: data.notes || [],
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id, isEdit]);
|
||||
|
||||
const set = (field, value) => setForm((f) => ({ ...f, [field]: value }));
|
||||
const setLoc = (field, value) => setForm((f) => ({ ...f, location: { ...f.location, [field]: value } }));
|
||||
|
||||
// Tags
|
||||
const addTag = (raw) => {
|
||||
const tag = raw.trim();
|
||||
if (tag && !form.tags.includes(tag)) {
|
||||
set("tags", [...form.tags, tag]);
|
||||
}
|
||||
setTagInput("");
|
||||
};
|
||||
const removeTag = (tag) => set("tags", form.tags.filter((t) => t !== tag));
|
||||
|
||||
// Contacts
|
||||
const addContact = () => set("contacts", [...form.contacts, emptyContact()]);
|
||||
const removeContact = (i) => set("contacts", form.contacts.filter((_, idx) => idx !== i));
|
||||
const setContact = (i, field, value) => {
|
||||
const updated = form.contacts.map((c, idx) => idx === i ? { ...c, [field]: value } : c);
|
||||
set("contacts", updated);
|
||||
};
|
||||
const setPrimaryContact = (i) => {
|
||||
const type = form.contacts[i].type;
|
||||
const updated = form.contacts.map((c, idx) => ({
|
||||
...c,
|
||||
primary: c.type === type ? idx === i : c.primary,
|
||||
}));
|
||||
set("contacts", updated);
|
||||
};
|
||||
|
||||
// Notes
|
||||
const addNote = () => {
|
||||
if (!newNoteText.trim()) return;
|
||||
const note = {
|
||||
text: newNoteText.trim(),
|
||||
by: user?.name || "unknown",
|
||||
at: new Date().toISOString(),
|
||||
};
|
||||
set("notes", [...form.notes, note]);
|
||||
setNewNoteText("");
|
||||
};
|
||||
|
||||
const removeNote = (i) => {
|
||||
set("notes", form.notes.filter((_, idx) => idx !== i));
|
||||
if (editingNoteIdx === i) setEditingNoteIdx(null);
|
||||
};
|
||||
|
||||
const startEditNote = (i) => {
|
||||
setEditingNoteIdx(i);
|
||||
setEditingNoteText(form.notes[i].text);
|
||||
};
|
||||
|
||||
const saveEditNote = (i) => {
|
||||
if (!editingNoteText.trim()) return;
|
||||
const updated = form.notes.map((n, idx) =>
|
||||
idx === i ? { ...n, text: editingNoteText.trim(), at: new Date().toISOString() } : n
|
||||
);
|
||||
set("notes", updated);
|
||||
setEditingNoteIdx(null);
|
||||
};
|
||||
|
||||
const buildPayload = () => ({
|
||||
title: form.title.trim() || null,
|
||||
name: form.name.trim(),
|
||||
surname: form.surname.trim() || null,
|
||||
organization: form.organization.trim() || null,
|
||||
language: form.language,
|
||||
tags: form.tags,
|
||||
...(!isEdit && { folder_id: form.folder_id.trim().toLowerCase() }),
|
||||
location: {
|
||||
city: form.location.city.trim(),
|
||||
country: form.location.country.trim(),
|
||||
region: form.location.region.trim(),
|
||||
},
|
||||
contacts: form.contacts.filter((c) => c.value.trim()),
|
||||
notes: form.notes,
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name.trim()) { setError("Customer name is required."); return; }
|
||||
if (!isEdit && !form.folder_id.trim()) { setError("Internal Folder ID is required."); return; }
|
||||
if (!isEdit && !/^[a-z0-9][a-z0-9\-]*[a-z0-9]$/.test(form.folder_id.trim().toLowerCase())) {
|
||||
setError("Internal Folder ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
if (isEdit) {
|
||||
await api.put(`/crm/customers/${id}`, buildPayload());
|
||||
navigate(`/crm/customers/${id}`);
|
||||
} else {
|
||||
const res = await api.post("/crm/customers", buildPayload());
|
||||
navigate(`/crm/customers/${res.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.delete(`/crm/customers/${id}`);
|
||||
navigate("/crm/customers");
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-8" style={{ color: "var(--text-muted)" }}>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 800, margin: "0 auto" }}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)" }}>
|
||||
{isEdit ? "Edit Customer" : "New Customer"}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => navigate(isEdit ? `/crm/customers/${id}` : "/crm/customers")}
|
||||
className="px-4 py-2 text-sm rounded-md border cursor-pointer"
|
||||
style={{ borderColor: "var(--border-primary)", color: "var(--text-secondary)" }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-sm rounded-md hover:opacity-90 transition-colors cursor-pointer"
|
||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)", opacity: saving ? 0.7 : 1 }}
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="text-sm rounded-md p-3 mb-4 border"
|
||||
style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Info */}
|
||||
<SectionCard title="Basic Info">
|
||||
{/* Row 1: Title, Name, Surname */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "140px 1fr 1fr", gap: 16, marginBottom: 16 }}>
|
||||
<Field label="Title">
|
||||
<select className={inputClass} style={inputStyle} value={form.title}
|
||||
onChange={(e) => set("title", e.target.value)}>
|
||||
{TITLES.map((t) => <option key={t} value={t}>{t || "—"}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Name *">
|
||||
<input className={inputClass} style={inputStyle} value={form.name}
|
||||
onChange={(e) => set("name", e.target.value)} placeholder="First name" />
|
||||
</Field>
|
||||
<Field label="Surname">
|
||||
<input className={inputClass} style={inputStyle} value={form.surname}
|
||||
onChange={(e) => set("surname", e.target.value)} placeholder="Last name" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Organization, Language, Folder ID */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16, marginBottom: 16 }}>
|
||||
<Field label="Organization">
|
||||
<input className={inputClass} style={inputStyle} value={form.organization}
|
||||
onChange={(e) => set("organization", e.target.value)} placeholder="Church, organization, etc." />
|
||||
</Field>
|
||||
<Field label="Language">
|
||||
<select className={inputClass} style={inputStyle} value={form.language}
|
||||
onChange={(e) => set("language", e.target.value)}>
|
||||
{LANGUAGES.map((l) => <option key={l.value} value={l.value}>{l.label}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
{!isEdit ? (
|
||||
<Field label="Folder ID *">
|
||||
<input
|
||||
className={inputClass}
|
||||
style={inputStyle}
|
||||
value={form.folder_id}
|
||||
onChange={(e) => set("folder_id", e.target.value.toLowerCase().replace(/[^a-z0-9\-]/g, ""))}
|
||||
placeholder="e.g. saint-john-corfu"
|
||||
/>
|
||||
</Field>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 4 }}>Folder ID</div>
|
||||
<div style={{ fontSize: 13, color: "var(--text-primary)", padding: "6px 0" }}>{form.folder_id || "—"}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEdit && (
|
||||
<p className="text-xs" style={{ color: "var(--text-muted)", marginTop: -8, marginBottom: 16 }}>
|
||||
Lowercase letters, numbers and hyphens only. This becomes the Nextcloud folder name and cannot be changed later.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Row 3: Tags */}
|
||||
<div>
|
||||
<label style={labelStyle}>Tags</label>
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{form.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full cursor-pointer"
|
||||
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}
|
||||
onClick={() => removeTag(tag)}
|
||||
title="Click to remove"
|
||||
>
|
||||
{tag} ×
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{/* Preset quick-add tags */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{PRESET_TAGS.filter((t) => !form.tags.includes(t)).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => addTag(t)}
|
||||
className="px-2 py-0.5 text-xs rounded-full border cursor-pointer hover:opacity-80"
|
||||
style={{ borderColor: "var(--border-primary)", color: "var(--text-muted)" }}
|
||||
>
|
||||
+ {t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
className={inputClass}
|
||||
style={inputStyle}
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
addTag(tagInput);
|
||||
}
|
||||
}}
|
||||
onBlur={() => tagInput.trim() && addTag(tagInput)}
|
||||
placeholder="Type a custom tag and press Enter or comma..."
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{/* Location */}
|
||||
<SectionCard title="Location">
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16 }}>
|
||||
<Field label="City">
|
||||
<input className={inputClass} style={inputStyle} value={form.location.city}
|
||||
onChange={(e) => setLoc("city", e.target.value)} placeholder="City" />
|
||||
</Field>
|
||||
<Field label="Country">
|
||||
<input className={inputClass} style={inputStyle} value={form.location.country}
|
||||
onChange={(e) => setLoc("country", e.target.value)} placeholder="Country" />
|
||||
</Field>
|
||||
<Field label="Region">
|
||||
<input className={inputClass} style={inputStyle} value={form.location.region}
|
||||
onChange={(e) => setLoc("region", e.target.value)} placeholder="Region" />
|
||||
</Field>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{/* Contacts */}
|
||||
<SectionCard title="Contacts">
|
||||
{form.contacts.map((c, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex gap-2 mb-2 items-center"
|
||||
>
|
||||
<span className="text-base w-6 text-center flex-shrink-0">{CONTACT_TYPE_ICONS[c.type] || "🔗"}</span>
|
||||
<select
|
||||
className="px-2 py-2 text-sm rounded-md border w-32 flex-shrink-0"
|
||||
style={inputStyle}
|
||||
value={c.type}
|
||||
onChange={(e) => setContact(i, "type", e.target.value)}
|
||||
>
|
||||
{CONTACT_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
<input
|
||||
className="px-2 py-2 text-sm rounded-md border w-28 flex-shrink-0"
|
||||
style={inputStyle}
|
||||
value={c.label}
|
||||
onChange={(e) => setContact(i, "label", e.target.value)}
|
||||
placeholder="label (e.g. work)"
|
||||
/>
|
||||
<input
|
||||
className={inputClass + " flex-1"}
|
||||
style={inputStyle}
|
||||
value={c.value}
|
||||
onChange={(e) => setContact(i, "value", e.target.value)}
|
||||
placeholder="value"
|
||||
/>
|
||||
<label className="flex items-center gap-1 text-xs flex-shrink-0 cursor-pointer" style={{ color: "var(--text-muted)" }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={`primary-${c.type}`}
|
||||
checked={!!c.primary}
|
||||
onChange={() => setPrimaryContact(i)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
Primary
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeContact(i)}
|
||||
className="text-xs cursor-pointer hover:opacity-70 flex-shrink-0"
|
||||
style={{ color: "var(--danger)" }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addContact}
|
||||
className="mt-2 px-3 py-1.5 text-xs rounded-md border cursor-pointer hover:opacity-80"
|
||||
style={{ borderColor: "var(--border-primary)", color: "var(--text-secondary)" }}
|
||||
>
|
||||
+ Add Contact
|
||||
</button>
|
||||
</SectionCard>
|
||||
|
||||
{/* Notes */}
|
||||
<SectionCard title="Notes">
|
||||
{form.notes.length > 0 && (
|
||||
<div className="mb-4 space-y-2">
|
||||
{form.notes.map((note, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="px-3 py-2 rounded-md text-sm"
|
||||
style={{ backgroundColor: "var(--bg-primary)", color: "var(--text-primary)" }}
|
||||
>
|
||||
{editingNoteIdx === i ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<textarea
|
||||
className={inputClass}
|
||||
style={{ ...inputStyle, resize: "vertical", minHeight: 56 }}
|
||||
value={editingNoteText}
|
||||
onChange={(e) => setEditingNoteText(e.target.value)}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) saveEditNote(i);
|
||||
if (e.key === "Escape") setEditingNoteIdx(null);
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => saveEditNote(i)}
|
||||
disabled={!editingNoteText.trim()}
|
||||
className="px-2 py-1 text-xs rounded-md cursor-pointer hover:opacity-90"
|
||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)", opacity: editingNoteText.trim() ? 1 : 0.5 }}
|
||||
>Save</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingNoteIdx(null)}
|
||||
className="px-2 py-1 text-xs rounded-md cursor-pointer hover:opacity-80"
|
||||
style={{ border: "1px solid var(--border-primary)", color: "var(--text-secondary)" }}
|
||||
>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p>{note.text}</p>
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||
{note.by} · {note.at ? new Date(note.at).toLocaleDateString() : ""}
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startEditNote(i)}
|
||||
className="text-xs cursor-pointer hover:opacity-70"
|
||||
style={{ color: "var(--accent)", background: "none", border: "none", padding: 0 }}
|
||||
>Edit</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeNote(i)}
|
||||
className="text-xs cursor-pointer hover:opacity-70"
|
||||
style={{ color: "var(--danger)", background: "none", border: "none", padding: 0 }}
|
||||
>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
className={inputClass + " flex-1"}
|
||||
style={{ ...inputStyle, resize: "vertical", minHeight: 64 }}
|
||||
value={newNoteText}
|
||||
onChange={(e) => setNewNoteText(e.target.value)}
|
||||
placeholder="Add a note..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addNote}
|
||||
disabled={!newNoteText.trim()}
|
||||
className="px-3 py-2 text-sm rounded-md cursor-pointer hover:opacity-80 self-start"
|
||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)", opacity: newNoteText.trim() ? 1 : 0.5 }}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{/* Delete */}
|
||||
{isEdit && canEdit && (
|
||||
<div
|
||||
className="rounded-lg border p-5"
|
||||
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
||||
>
|
||||
<h2 className="text-sm font-semibold mb-2" style={{ color: "var(--danger)" }}>Danger Zone</h2>
|
||||
{!showDeleteConfirm ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="px-4 py-2 text-sm rounded-md border cursor-pointer hover:opacity-80"
|
||||
style={{ borderColor: "var(--danger)", color: "var(--danger)" }}
|
||||
>
|
||||
Delete Customer
|
||||
</button>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-sm mb-3" style={{ color: "var(--text-secondary)" }}>
|
||||
Are you sure? This cannot be undone.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-sm rounded-md cursor-pointer hover:opacity-80"
|
||||
style={{ backgroundColor: "var(--danger)", color: "#fff", opacity: saving ? 0.7 : 1 }}
|
||||
>
|
||||
{saving ? "Deleting..." : "Yes, Delete"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
className="px-4 py-2 text-sm rounded-md border cursor-pointer"
|
||||
style={{ borderColor: "var(--border-primary)", color: "var(--text-secondary)" }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user