149 lines
5.7 KiB
React
Executable File
149 lines
5.7 KiB
React
Executable File
import { useState } from 'react';
|
|
import { FolderCog, Pencil, Plus, Trash2 } from 'lucide-react';
|
|
import Modal from './Modal.jsx';
|
|
|
|
const PRESET_COLORS = ['#7C3AED', '#2563EB', '#D97706', '#059669', '#DC2626', '#0F766E', '#9333EA', '#475569'];
|
|
|
|
function CategoryForm({ initial, suggestedSortOrder, onSave, onClose }) {
|
|
const [name, setName] = useState(initial?.name ?? '');
|
|
const [color, setColor] = useState(initial?.color ?? PRESET_COLORS[0]);
|
|
const [sortOrder, setSortOrder] = useState(initial?.sort_order ?? suggestedSortOrder ?? 1);
|
|
|
|
function submit(e) {
|
|
e.preventDefault();
|
|
if (!name.trim()) return;
|
|
|
|
onSave({
|
|
name: name.trim(),
|
|
color,
|
|
sort_order: parseInt(sortOrder) || suggestedSortOrder || 1,
|
|
});
|
|
onClose();
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} className="space-y-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Namn</label>
|
|
<input
|
|
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
placeholder="t.ex. Försäkringar"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">Färg</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{PRESET_COLORS.map(option => (
|
|
<button
|
|
key={option}
|
|
type="button"
|
|
onClick={() => setColor(option)}
|
|
className={`w-8 h-8 rounded-full border-2 ${color === option ? 'border-slate-800' : 'border-transparent'}`}
|
|
style={{ backgroundColor: option }}
|
|
aria-label={`Välj färg ${option}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Sorteringsordning</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
value={sortOrder}
|
|
onChange={e => setSortOrder(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-1">
|
|
<button type="button" onClick={onClose} className="flex-1 py-2.5 rounded-xl border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50">
|
|
Avbryt
|
|
</button>
|
|
<button type="submit" className="flex-1 py-2.5 rounded-xl bg-blue-600 text-white text-sm font-medium hover:bg-blue-700">
|
|
Spara
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export default function CategorySettingsSection({ categories, onAdd, onUpdate, onDelete, deleteError }) {
|
|
const [modal, setModal] = useState(null);
|
|
const suggestedSortOrder = (categories.at(-1)?.sort_order ?? 0) + 1;
|
|
|
|
return (
|
|
<section className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden mb-3">
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
|
|
<div className="flex items-center gap-2">
|
|
<FolderCog size={16} className="text-emerald-600" />
|
|
<span className="font-semibold text-slate-700 text-sm uppercase tracking-wide">Kategorier</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setModal('add')}
|
|
className="flex items-center gap-1 text-blue-600 text-xs font-medium hover:text-blue-700"
|
|
>
|
|
<Plus size={14} /> Lägg till
|
|
</button>
|
|
</div>
|
|
|
|
<div className="px-4 py-3 border-b border-slate-100 bg-emerald-50/70 text-sm text-slate-600">
|
|
Här skapar och ändrar du kategorierna som används i budgeten, transaktionerna och AI-förslagen. Byter du namn eller färg här följer det med i hela appen.
|
|
</div>
|
|
|
|
{deleteError && (
|
|
<div className="mx-4 mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
|
{deleteError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="divide-y divide-slate-50">
|
|
{categories.map(category => (
|
|
<div key={category.id} className="flex items-center gap-3 px-4 py-3 group">
|
|
<span className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: category.color }} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-slate-800">{category.name}</div>
|
|
<div className="text-xs text-slate-500">Ordning {category.sort_order}</div>
|
|
</div>
|
|
<div className="flex gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity shrink-0">
|
|
<button onClick={() => setModal({ edit: category })} className="p-1 rounded hover:bg-slate-100 text-slate-400">
|
|
<Pencil size={13} />
|
|
</button>
|
|
<button onClick={() => onDelete(category.id)} className="p-1 rounded hover:bg-red-50 text-slate-400 hover:text-red-500">
|
|
<Trash2 size={13} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{modal === 'add' && (
|
|
<Modal title="Lägg till kategori" onClose={() => setModal(null)}>
|
|
<CategoryForm
|
|
suggestedSortOrder={suggestedSortOrder}
|
|
onSave={onAdd}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
</Modal>
|
|
)}
|
|
|
|
{modal?.edit && (
|
|
<Modal title="Redigera kategori" onClose={() => setModal(null)}>
|
|
<CategoryForm
|
|
initial={modal.edit}
|
|
suggestedSortOrder={modal.edit.sort_order}
|
|
onSave={data => onUpdate(modal.edit.id, data)}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
</Modal>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|