- Added table-fixed layout for consistent column widths - Set specific column widths: Behandlung (2/5), Kategorie (1/6), Dauer (1/12), Preis (1/12), Aktionen (1/6) - Truncate long descriptions to 50 characters with tooltip - Added truncate class to prevent text overflow - Ensures all columns are always visible without horizontal scrolling
274 lines
9.7 KiB
TypeScript
274 lines
9.7 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { queryClient } from "@/client/rpc-client";
|
|
|
|
export function AdminTreatments() {
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingTreatment, setEditingTreatment] = useState<any>(null);
|
|
const [formData, setFormData] = useState({
|
|
name: "",
|
|
description: "",
|
|
duration: 60,
|
|
price: 5000, // 50,00 € in Cent
|
|
category: "Maniküre",
|
|
});
|
|
|
|
const { data: treatments } = useQuery(
|
|
queryClient.treatments.live.list.experimental_liveOptions()
|
|
);
|
|
|
|
const { mutate: createTreatment, isPending: isCreating } = useMutation(
|
|
queryClient.treatments.create.mutationOptions()
|
|
);
|
|
|
|
const { mutate: updateTreatment, isPending: isUpdating } = useMutation(
|
|
queryClient.treatments.update.mutationOptions()
|
|
);
|
|
|
|
const { mutate: deleteTreatment } = useMutation(
|
|
queryClient.treatments.remove.mutationOptions()
|
|
);
|
|
|
|
const categories = ["Maniküre", "Pediküre", "Nageldesign", "Verlängerungen", "Sonstiges"];
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (editingTreatment) {
|
|
updateTreatment({
|
|
id: editingTreatment.id,
|
|
...formData,
|
|
}, {
|
|
onSuccess: () => {
|
|
setEditingTreatment(null);
|
|
setShowForm(false);
|
|
resetForm();
|
|
}
|
|
});
|
|
} else {
|
|
createTreatment(formData, {
|
|
onSuccess: () => {
|
|
setShowForm(false);
|
|
resetForm();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
name: "",
|
|
description: "",
|
|
duration: 60,
|
|
price: 5000,
|
|
category: "Maniküre",
|
|
});
|
|
};
|
|
|
|
const handleEdit = (treatment: any) => {
|
|
setEditingTreatment(treatment);
|
|
setFormData({
|
|
name: treatment.name,
|
|
description: treatment.description,
|
|
duration: treatment.duration,
|
|
price: treatment.price,
|
|
category: treatment.category,
|
|
});
|
|
setShowForm(true);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setShowForm(false);
|
|
setEditingTreatment(null);
|
|
resetForm();
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-900">Behandlungen verwalten</h2>
|
|
<button
|
|
onClick={() => setShowForm(true)}
|
|
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium"
|
|
>
|
|
Behandlung hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
{showForm && (
|
|
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
|
<h3 className="text-lg font-semibold mb-4">
|
|
{editingTreatment ? "Behandlung bearbeiten" : "Neue Behandlung hinzufügen"}
|
|
</h3>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Behandlungsname *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Kategorie *
|
|
</label>
|
|
<select
|
|
value={formData.category}
|
|
onChange={(e) => setFormData({...formData, category: e.target.value})}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
|
required
|
|
>
|
|
{categories.map(cat => (
|
|
<option key={cat} value={cat}>{cat}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Beschreibung *
|
|
</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
|
rows={3}
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Dauer (Minuten) *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.duration}
|
|
onChange={(e) => setFormData({...formData, duration: parseInt(e.target.value)})}
|
|
min="15"
|
|
max="480"
|
|
step="15"
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Preis (€) *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.price / 100}
|
|
onChange={(e) => setFormData({...formData, price: Math.round(parseFloat(e.target.value) * 100)})}
|
|
min="0"
|
|
step="0.01"
|
|
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex space-x-4">
|
|
<button
|
|
type="submit"
|
|
disabled={isCreating || isUpdating}
|
|
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 disabled:opacity-50 font-medium"
|
|
>
|
|
{isCreating || isUpdating ? "Speichern..." : (editingTreatment ? "Aktualisieren" : "Erstellen")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleCancel}
|
|
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white rounded-lg shadow-lg overflow-x-auto">
|
|
<table className="w-full table-fixed">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="w-2/5 px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Behandlung
|
|
</th>
|
|
<th className="w-1/6 px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Kategorie
|
|
</th>
|
|
<th className="w-1/12 px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Dauer
|
|
</th>
|
|
<th className="w-1/12 px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Preis
|
|
</th>
|
|
<th className="w-1/6 px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Aktionen
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{treatments?.map((treatment) => (
|
|
<tr key={treatment.id}>
|
|
<td className="px-6 py-4">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-900 truncate">{treatment.name}</div>
|
|
<div className="text-sm text-gray-500 truncate" title={treatment.description}>
|
|
{treatment.description.length > 50
|
|
? `${treatment.description.substring(0, 50)}...`
|
|
: treatment.description}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-900 truncate">
|
|
{treatment.category}
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-900">
|
|
{treatment.duration} Min
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-gray-900">
|
|
{(treatment.price / 100).toFixed(2)} €
|
|
</td>
|
|
<td className="px-6 py-4 text-sm font-medium space-x-2">
|
|
<button
|
|
onClick={() => handleEdit(treatment)}
|
|
className="text-pink-600 hover:text-pink-900"
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm("Bist du sicher, dass du diese Behandlung löschen möchtest?")) {
|
|
deleteTreatment(treatment.id);
|
|
}
|
|
}}
|
|
className="text-red-600 hover:text-red-900"
|
|
>
|
|
Löschen
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
{!treatments?.length && (
|
|
<div className="text-center py-8 text-gray-500">
|
|
Keine Behandlungen verfügbar. Füge deine erste Behandlung hinzu, um zu starten.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |