Add event types management functionality, including database schema, UI components, and hooks for CRUD operations. Introduce EventTypeModal for creating and editing event types, and integrate with the navigation bar and event types page for seamless user experience.
This commit is contained in:
parent
00b1ee61a3
commit
27dc530b1c
13 changed files with 1016 additions and 104 deletions
|
|
@ -138,6 +138,36 @@ export type Database = {
|
|||
}
|
||||
Relationships: []
|
||||
}
|
||||
event_types: {
|
||||
Row: {
|
||||
config: Json
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string
|
||||
is_active: boolean
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
events: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
|
|
|
|||
100
sql/18_event_types_table.sql
Normal file
100
sql/18_event_types_table.sql
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
-- Create event_types table to store event type configurations as JSONB
|
||||
CREATE TABLE IF NOT EXISTS event_types (
|
||||
id TEXT PRIMARY KEY DEFAULT generate_random_string(24),
|
||||
user_id UUID NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
|
||||
-- Foreign key constraint to users table (auth.users)
|
||||
CONSTRAINT fk_event_types_user_id
|
||||
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_event_types_user_id ON event_types(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_types_is_active ON event_types(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_types_deleted_at ON event_types(deleted_at);
|
||||
|
||||
-- Create GIN index for JSONB config column for efficient querying
|
||||
CREATE INDEX IF NOT EXISTS idx_event_types_config_gin ON event_types USING GIN (config);
|
||||
|
||||
-- Add constraint to ensure config JSONB has required fields
|
||||
ALTER TABLE event_types ADD CONSTRAINT event_types_config_check
|
||||
CHECK (
|
||||
config ? 'name' AND
|
||||
(config->>'name')::text <> '' AND
|
||||
config ? 'duration' AND
|
||||
(config->>'duration')::int > 0
|
||||
);
|
||||
|
||||
-- Create function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_event_types_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create trigger to automatically update updated_at
|
||||
CREATE TRIGGER update_event_types_updated_at
|
||||
BEFORE UPDATE ON event_types
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_event_types_updated_at();
|
||||
|
||||
-- Enable Row Level Security
|
||||
ALTER TABLE event_types ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy to allow users to view their own event types
|
||||
CREATE POLICY "Users can view their own event types" ON event_types
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to insert their own event types
|
||||
CREATE POLICY "Users can insert their own event types" ON event_types
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to update their own event types
|
||||
CREATE POLICY "Users can update their own event types" ON event_types
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid())
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
-- Policy to allow users to delete their own event types (soft delete)
|
||||
CREATE POLICY "Users can delete their own event types" ON event_types
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (user_id = auth.uid() AND deleted_at IS NULL)
|
||||
WITH CHECK (user_id = auth.uid());
|
||||
|
||||
-- Add helpful comments
|
||||
COMMENT ON TABLE event_types IS
|
||||
'Event type configurations stored as JSONB with Row Level Security';
|
||||
|
||||
COMMENT ON COLUMN event_types.id IS
|
||||
'Primary key: random 24-character alphanumeric string';
|
||||
|
||||
COMMENT ON COLUMN event_types.user_id IS
|
||||
'Foreign key reference to auth.users.id';
|
||||
|
||||
COMMENT ON COLUMN event_types.config IS
|
||||
'JSONB configuration containing: name (required), description (optional), duration (required), bufferTime, maxBookingsPerDay, requiresApproval, price, location, minAdvanceBooking, etc.';
|
||||
|
||||
COMMENT ON COLUMN event_types.is_active IS
|
||||
'Whether this event type is active and available for booking';
|
||||
|
||||
COMMENT ON COLUMN event_types.created_at IS
|
||||
'Timestamp when the event type was created';
|
||||
|
||||
COMMENT ON COLUMN event_types.updated_at IS
|
||||
'Timestamp when the event type was last updated (auto-updated by trigger)';
|
||||
|
||||
COMMENT ON COLUMN event_types.deleted_at IS
|
||||
'Timestamp for soft deletion (NULL means not deleted)';
|
||||
|
|
@ -4,6 +4,7 @@ interface CustomModalProps {
|
|||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
width?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "auto";
|
||||
}
|
||||
|
||||
export function CustomModal({
|
||||
|
|
@ -11,9 +12,31 @@ export function CustomModal({
|
|||
onClose,
|
||||
title,
|
||||
children,
|
||||
width = "md",
|
||||
}: CustomModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getWidthClasses = () => {
|
||||
switch (width) {
|
||||
case "sm":
|
||||
return "w-full max-w-sm";
|
||||
case "md":
|
||||
return "w-full max-w-md";
|
||||
case "lg":
|
||||
return "w-full max-w-lg";
|
||||
case "xl":
|
||||
return "w-full max-w-xl";
|
||||
case "2xl":
|
||||
return "w-full max-w-2xl";
|
||||
case "full":
|
||||
return "w-full max-w-full mx-4";
|
||||
case "auto":
|
||||
return "w-auto min-w-80 max-w-[90vw]";
|
||||
default:
|
||||
return "w-full max-w-md";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
|
|
@ -23,9 +46,11 @@ export function CustomModal({
|
|||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 max-h-[90vh] overflow-hidden">
|
||||
<div
|
||||
className={`relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 ${getWidthClasses()} mx-4 max-h-[90vh] flex flex-col`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</h2>
|
||||
|
|
@ -50,7 +75,7 @@ export function CustomModal({
|
|||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">{children}</div>
|
||||
<div className="p-6 overflow-y-auto flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import { EventAndTablo } from "@ui/types/events.types";
|
||||
import {
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogCloseButton,
|
||||
} from "@ui/ui-library/dialog";
|
||||
import { DialogBody } from "@ui/ui-library/dialog";
|
||||
import { Text, Strong } from "@ui/ui-library/text";
|
||||
import { Button } from "@ui/ui-library/button";
|
||||
import { CalendarIcon, User } from "lucide-react";
|
||||
import { CustomModal } from "./CustomModal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface EventDetailsModalProps {
|
||||
event: EventAndTablo | null;
|
||||
|
|
@ -21,7 +17,6 @@ export const EventDetailsModal = ({
|
|||
event,
|
||||
isOpen,
|
||||
onClose,
|
||||
onEdit,
|
||||
}: EventDetailsModalProps) => {
|
||||
if (!event) return null;
|
||||
|
||||
|
|
@ -41,10 +36,10 @@ export const EventDetailsModal = ({
|
|||
if (event.start_time) {
|
||||
// Remove seconds from time (HH:MM:SS -> HH:MM)
|
||||
const startTime = event.start_time.substring(0, 5);
|
||||
formatted += ` à ${startTime}`;
|
||||
formatted += ` de ${startTime}`;
|
||||
if (event.end_time) {
|
||||
const endTime = event.end_time.substring(0, 5);
|
||||
formatted += ` - ${endTime}`;
|
||||
formatted += ` à ${endTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,99 +78,69 @@ export const EventDetailsModal = ({
|
|||
}
|
||||
};
|
||||
|
||||
console.log(event.tablo_color);
|
||||
return (
|
||||
<CustomModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={event.title || "Événement sans titre"}
|
||||
>
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{event.title || "Événement sans titre"}
|
||||
</h2>
|
||||
<div className="mt-1">{getEventStatusBadge(event)}</div>
|
||||
</div>
|
||||
<DialogBody className="space-y-6">
|
||||
<div className="flex items-start space-x-3">
|
||||
{getEventStatusBadge(event)}
|
||||
</div>
|
||||
{/* Date and Time */}
|
||||
<div className="flex items-start space-x-3">
|
||||
<CalendarIcon className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div>
|
||||
<Strong className="text-sm text-gray-900 dark:text-gray-100">
|
||||
Date et heure
|
||||
</Strong>
|
||||
<Text className="text-gray-600 dark:text-gray-300">
|
||||
{formatEventDateTime(event)}
|
||||
</Text>
|
||||
</div>
|
||||
<DialogCloseButton />
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<DialogBody className="space-y-6">
|
||||
{/* Date and Time */}
|
||||
{/* Tablo */}
|
||||
{event.tablo_name && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<CalendarIcon className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div
|
||||
className={twMerge("w-5 h-5 rounded mt-0.5", event.tablo_color)}
|
||||
/>
|
||||
<div>
|
||||
<Strong className="text-sm text-gray-900 dark:text-gray-100">
|
||||
Date et heure
|
||||
Tableau
|
||||
</Strong>
|
||||
<Text className="text-gray-600 dark:text-gray-300">
|
||||
{formatEventDateTime(event)}
|
||||
{event.tablo_name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tablo */}
|
||||
{event.tablo_name && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<div
|
||||
className="w-5 h-5 rounded mt-0.5"
|
||||
style={{ backgroundColor: event.tablo_color || "#6b7280" }}
|
||||
/>
|
||||
<div>
|
||||
<Strong className="text-sm text-gray-900 dark:text-gray-100">
|
||||
Tableau
|
||||
</Strong>
|
||||
<Text className="text-gray-600 dark:text-gray-300">
|
||||
{event.tablo_name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<User className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<Strong className="text-sm text-gray-900 dark:text-gray-100">
|
||||
Description
|
||||
</Strong>
|
||||
<Text className="text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{event.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event ID (for debugging/reference) */}
|
||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>ID: {event.event_id}</span>
|
||||
{event.tablo_id && <span>Tableau ID: {event.tablo_id}</span>}
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<User className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<Strong className="text-sm text-gray-900 dark:text-gray-100">
|
||||
Description
|
||||
</Strong>
|
||||
<Text className="text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
|
||||
{event.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</DialogBody>
|
||||
)}
|
||||
</DialogBody>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 px-6 py-4 bg-gray-50 dark:bg-gray-800">
|
||||
<Button variant="outline" onPress={onClose}>
|
||||
Fermer
|
||||
</Button>
|
||||
{onEdit && (
|
||||
<Button
|
||||
className="bg-emerald-700 text-white hover:bg-emerald-600"
|
||||
onPress={() => {
|
||||
onEdit();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 px-6 py-4 bg-gray-50 dark:bg-gray-800">
|
||||
<Button variant="outline" onPress={onClose}>
|
||||
Fermer
|
||||
</Button>
|
||||
</div>
|
||||
</CustomModal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
263
ui/src/components/EventTypeModal.tsx
Normal file
263
ui/src/components/EventTypeModal.tsx
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import { Button } from "@ui/ui-library/button";
|
||||
import { CustomModal } from "./CustomModal";
|
||||
import {
|
||||
Label,
|
||||
TextField,
|
||||
TextArea,
|
||||
Description,
|
||||
Input,
|
||||
} from "@ui/ui-library/field";
|
||||
import { NumberField, NumberInput } from "@ui/ui-library/number-field";
|
||||
import {
|
||||
Select,
|
||||
SelectButton,
|
||||
SelectListBox,
|
||||
SelectListItem,
|
||||
SelectPopover,
|
||||
} from "@ui/ui-library/select";
|
||||
import { EventType } from "@ui/hooks/event-types";
|
||||
|
||||
export function EventTypeModal({
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
editingEventType,
|
||||
formData,
|
||||
setFormData,
|
||||
handleSaveEventType,
|
||||
}: {
|
||||
isModalOpen: boolean;
|
||||
setIsModalOpen: (isOpen: boolean) => void;
|
||||
editingEventType: EventType | null;
|
||||
formData: EventType;
|
||||
setFormData: (formData: EventType) => void;
|
||||
handleSaveEventType: () => void;
|
||||
}) {
|
||||
return (
|
||||
<CustomModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title={
|
||||
editingEventType
|
||||
? "Modifier le type d'événement"
|
||||
: "Nouveau type d'événement"
|
||||
}
|
||||
width="xl"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Basic Information Section */}
|
||||
<div className="space-y-2">
|
||||
<TextField
|
||||
value={formData.name || ""}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
isRequired
|
||||
>
|
||||
<Label requiredHint>Nom du type d'événement</Label>
|
||||
<Input type="text" />
|
||||
</TextField>
|
||||
|
||||
<TextField>
|
||||
<Label>Description</Label>
|
||||
<TextArea
|
||||
value={formData.description || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
rows={2}
|
||||
required
|
||||
placeholder="Décrivez ce type d'événement et son objectif..."
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
|
||||
{/* Timing Configuration Section */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
Configuration des horaires
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NumberField
|
||||
value={formData.duration || 60}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, duration: value })
|
||||
}
|
||||
minValue={15}
|
||||
maxValue={480}
|
||||
step={15}
|
||||
>
|
||||
<Label requiredHint>Durée (minutes)</Label>
|
||||
<NumberInput />
|
||||
</NumberField>
|
||||
|
||||
<NumberField
|
||||
value={formData.bufferTime || 0}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, bufferTime: value })
|
||||
}
|
||||
minValue={0}
|
||||
maxValue={60}
|
||||
step={5}
|
||||
>
|
||||
<Label>Temps de battement (minutes)</Label>
|
||||
<NumberInput />
|
||||
<Description>
|
||||
Temps de battement avant et après l'événement
|
||||
</Description>
|
||||
</NumberField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Booking Limits Section */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
Limites de réservation
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<NumberField
|
||||
value={formData.maxBookingsPerDay || 8}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, maxBookingsPerDay: value })
|
||||
}
|
||||
minValue={1}
|
||||
maxValue={50}
|
||||
>
|
||||
<Label>Maximum par jour</Label>
|
||||
<NumberInput />
|
||||
</NumberField>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Réservation à l'avance (heures)</Label>
|
||||
<div className="flex flex-row gap-2">
|
||||
<NumberField
|
||||
value={formData.minAdvanceBooking?.value || 0}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
minAdvanceBooking: {
|
||||
value,
|
||||
unit: formData.minAdvanceBooking?.unit || "minutes",
|
||||
},
|
||||
})
|
||||
}
|
||||
minValue={0}
|
||||
maxValue={168}
|
||||
>
|
||||
<NumberInput />
|
||||
</NumberField>
|
||||
<Select
|
||||
selectedKey={String(
|
||||
formData.minAdvanceBooking?.unit || "minutes"
|
||||
)}
|
||||
onSelectionChange={(key) => {
|
||||
console.log(key);
|
||||
setFormData({
|
||||
...formData,
|
||||
minAdvanceBooking: {
|
||||
value: formData.minAdvanceBooking?.value || 0,
|
||||
unit: key as "minutes" | "hours" | "days",
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="..."
|
||||
className="min-w-[110px]"
|
||||
aria-label="Délai minimum pour réserver"
|
||||
>
|
||||
<SelectButton />
|
||||
<SelectPopover className="w-36">
|
||||
<SelectListBox>
|
||||
<SelectListItem id="minutes">minutes</SelectListItem>
|
||||
<SelectListItem id="hours">heures</SelectListItem>
|
||||
<SelectListItem id="days">jours</SelectListItem>
|
||||
</SelectListBox>
|
||||
</SelectPopover>
|
||||
</Select>
|
||||
</div>
|
||||
<Description>Délai minimum pour réserver</Description>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
Tarification (optionnel)
|
||||
</h3>
|
||||
|
||||
<NumberField
|
||||
value={formData.price || 0}
|
||||
onChange={(value) => setFormData({ ...formData, price: value })}
|
||||
minValue={0}
|
||||
maxValue={10000}
|
||||
step={5}
|
||||
formatOptions={{
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}}
|
||||
>
|
||||
<Label>Prix (€)</Label>
|
||||
<Description>
|
||||
Prix de ce type d'événement. Laissez à 0 pour gratuit.
|
||||
</Description>
|
||||
</NumberField>
|
||||
</div> */}
|
||||
|
||||
{/* Settings Section */}
|
||||
{/* <div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
Paramètres
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
isSelected={formData.isActive || false}
|
||||
onChange={(isActive) => setFormData({ ...formData, isActive })}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">Type d'événement actif</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Les clients peuvent réserver ce type d'événement
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
isSelected={formData.requiresApproval || false}
|
||||
onChange={(requiresApproval) =>
|
||||
setFormData({ ...formData, requiresApproval })
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
Nécessite une approbation manuelle
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Vous devrez approuver chaque réservation avant confirmation
|
||||
</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button variant="outline" onPress={() => setIsModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
onPress={handleSaveEventType}
|
||||
className="[--btn-bg:var(--color-green-800)]"
|
||||
isDisabled={!formData.name?.trim() || !formData.duration}
|
||||
>
|
||||
{editingEventType ? "Modifier" : "Créer"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CustomModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Kanban,
|
||||
CalendarIcon,
|
||||
CalendarCheckIcon,
|
||||
ListCheckIcon,
|
||||
} from "lucide-react";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
import { Separator } from "react-aria-components";
|
||||
|
|
@ -271,9 +272,9 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
path: "/planning",
|
||||
label: "Planning",
|
||||
icon: <SquareKanban className="w-5 h-5" />,
|
||||
path: "/event-types",
|
||||
label: "Types d'événements",
|
||||
icon: <ListCheckIcon className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/availabilities",
|
||||
|
|
@ -298,6 +299,12 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
isDisabled: true,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/planning",
|
||||
label: "Planning",
|
||||
icon: <SquareKanban className="w-5 h-5" />,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/",
|
||||
label: "Tableaux",
|
||||
|
|
@ -315,10 +322,10 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
role="list"
|
||||
className={twMerge("grid py-3", isCollapsed ? "pl-2.5 pr-3" : "")}
|
||||
>
|
||||
{navItems.map((item) => {
|
||||
{navItems.map((item, index) => {
|
||||
if ("isHorizontalBar" in item) {
|
||||
return (
|
||||
<li key="horizontal-bar" className="my-2">
|
||||
<li key={index} className="my-2">
|
||||
<Separator className="border-gray-300/20" />
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
156
ui/src/hooks/event-types.ts
Normal file
156
ui/src/hooks/event-types.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { supabase } from "./auth";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type EventType = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
duration: number; // in minutes
|
||||
isActive: boolean;
|
||||
bufferTime?: number; // buffer time before/after in minutes
|
||||
maxBookingsPerDay?: number;
|
||||
requiresApproval: boolean;
|
||||
price?: number; // optional price
|
||||
location?: string; // optional location
|
||||
minAdvanceBooking?: {
|
||||
value: number;
|
||||
unit: "minutes" | "hours" | "days";
|
||||
}; // minimum hours in advance
|
||||
};
|
||||
|
||||
const QUERY_KEY = ["event-types"];
|
||||
|
||||
export function useEventTypes() {
|
||||
const { session } = useSession();
|
||||
|
||||
const { data: eventTypesData, isLoading } = useQuery<
|
||||
Database["public"]["Tables"]["event_types"]["Row"][]
|
||||
>({
|
||||
queryKey: QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", session?.user.id)
|
||||
.is("deleted_at", null);
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
enabled: !!session?.user.id,
|
||||
});
|
||||
|
||||
const { mutate: addEventType } = useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
eventType: EventType;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ eventType }: { eventType: EventType }) => {
|
||||
const { error } = await supabase.from("event_types").insert({
|
||||
config: eventType,
|
||||
user_id: session?.user.id,
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateEventType } = useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
id: string;
|
||||
eventType: EventType;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
eventType,
|
||||
}: {
|
||||
id: string;
|
||||
eventType: EventType;
|
||||
}) => {
|
||||
const { error } = await supabase
|
||||
.from("event_types")
|
||||
.update({
|
||||
config: eventType,
|
||||
is_active: eventType.isActive,
|
||||
})
|
||||
.eq("id", id);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: toggleEventType } = useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
|
||||
const { error } = await supabase
|
||||
.from("event_types")
|
||||
.update({
|
||||
is_active: isActive,
|
||||
})
|
||||
.eq("id", id);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteEventType } = useMutation<
|
||||
void,
|
||||
Error,
|
||||
{
|
||||
id: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ id }: { id: string }) => {
|
||||
const { error } = await supabase
|
||||
.from("event_types")
|
||||
.update({ deleted_at: new Date().toISOString() })
|
||||
.eq("id", id);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypes = useMemo(() => {
|
||||
return (
|
||||
eventTypesData?.map((eventType) => {
|
||||
const eventTypeConfig = eventType.config as EventType;
|
||||
return {
|
||||
...eventTypeConfig,
|
||||
isActive: eventType.is_active,
|
||||
id: eventType.id,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}, [eventTypesData]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
eventTypes,
|
||||
addEventType,
|
||||
updateEventType,
|
||||
toggleEventType,
|
||||
deleteEventType,
|
||||
};
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import { FeedbackPage } from "@ui/pages/feedback";
|
|||
import { SupportPage } from "@ui/pages/support";
|
||||
import { AvailabilitiesPage } from "@ui/pages/availabilities";
|
||||
import { BookingsPage } from "@ui/pages/bookings";
|
||||
import { EventTypesPage } from "@ui/pages/event-types-page";
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
// Protected routes
|
||||
|
|
@ -84,6 +85,10 @@ export const routes: RouteObject[] = [
|
|||
element: <BookingsPage />,
|
||||
children: [{ index: true }, { path: ":tablo_id" }],
|
||||
},
|
||||
{
|
||||
path: "event-types",
|
||||
element: <EventTypesPage />,
|
||||
},
|
||||
{
|
||||
path: "feedback",
|
||||
element: <FeedbackPage />,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
|||
import { EventDetailsModal } from "@ui/components/EventDetailsModal";
|
||||
import { RadioGroup, Radios, Radio } from "@ui/ui-library/radio-group";
|
||||
import { Badge } from "@ui/ui-library/badge";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { getTextColorFromTabloColor } from "@ui/utils/helpers";
|
||||
|
||||
type BookingStatus = "all" | "upcoming" | "past";
|
||||
|
||||
|
|
@ -339,13 +341,11 @@ export const BookingsPage = () => {
|
|||
</span>
|
||||
{event.tablo_name && (
|
||||
<span
|
||||
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: event.tablo_color
|
||||
? `${event.tablo_color}20`
|
||||
: "#f3f4f6",
|
||||
color: event.tablo_color || "#6b7280",
|
||||
}}
|
||||
className={twMerge(
|
||||
"inline-flex items-center px-2 py-1 rounded-md text-xs font-medium",
|
||||
event.tablo_color,
|
||||
getTextColorFromTabloColor(event.tablo_color)
|
||||
)}
|
||||
>
|
||||
{event.tablo_name}
|
||||
</span>
|
||||
|
|
@ -360,13 +360,6 @@ export const BookingsPage = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onPress={() => handleEditEvent(event)}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
|
|||
213
ui/src/pages/event-types-page.tsx
Normal file
213
ui/src/pages/event-types-page.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { useState } from "react";
|
||||
import { Strong, Text } from "@ui/ui-library/text";
|
||||
import { Button, ToggleButton } from "@ui/ui-library/button";
|
||||
import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon } from "lucide-react";
|
||||
import { toast } from "@ui/ui-library/toast/toast-queue";
|
||||
import { EventTypeModal } from "@ui/components/EventTypeModal";
|
||||
import { EventType, useEventTypes } from "@ui/hooks/event-types";
|
||||
|
||||
export function EventTypesPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingEventType, setEditingEventType] = useState<EventType | null>(
|
||||
null
|
||||
);
|
||||
const [formData, setFormData] = useState<Partial<EventType>>({
|
||||
name: "",
|
||||
description: "",
|
||||
duration: 60,
|
||||
isActive: true,
|
||||
bufferTime: 15,
|
||||
maxBookingsPerDay: 8,
|
||||
requiresApproval: false,
|
||||
});
|
||||
const {
|
||||
eventTypes: eventTypesData,
|
||||
addEventType,
|
||||
updateEventType,
|
||||
toggleEventType,
|
||||
deleteEventType,
|
||||
} = useEventTypes();
|
||||
|
||||
const handleCreateEventType = () => {
|
||||
setEditingEventType(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
duration: 60,
|
||||
isActive: true,
|
||||
bufferTime: 15,
|
||||
maxBookingsPerDay: 8,
|
||||
requiresApproval: false,
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditEventType = (eventType: EventType) => {
|
||||
setEditingEventType(eventType);
|
||||
setFormData(eventType as EventType);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveEventType = () => {
|
||||
if (!formData.name) {
|
||||
toast.add({
|
||||
title: "Erreur",
|
||||
description: "Veuillez remplir tous les champs obligatoires",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingEventType) {
|
||||
updateEventType({
|
||||
id: editingEventType.id,
|
||||
eventType: formData as EventType,
|
||||
});
|
||||
} else {
|
||||
addEventType({ eventType: formData as EventType });
|
||||
}
|
||||
|
||||
setIsModalOpen(false);
|
||||
setEditingEventType(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Types d'événements</h2>
|
||||
<Strong className="text-gray-500 mt-2 text-xl">
|
||||
Configurez les différents types d'événements que vous proposez
|
||||
</Strong>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="solid"
|
||||
className="[--btn-bg:var(--color-blue-800)]"
|
||||
onPress={handleCreateEventType}
|
||||
>
|
||||
<PlusIcon /> Nouveau type
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{eventTypesData?.map((eventType) => (
|
||||
<div
|
||||
key={eventType.id}
|
||||
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-6 border ${
|
||||
eventType.isActive ? "opacity-100" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-lg font-semibold">{eventType.name}</h3>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="plain"
|
||||
isIconOnly
|
||||
onPress={() => handleEditEventType(eventType as EventType)}
|
||||
className="text-gray-500 hover:text-blue-600"
|
||||
>
|
||||
<EditIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="plain"
|
||||
isIconOnly
|
||||
onPress={() => deleteEventType({ id: eventType.id })}
|
||||
className="text-gray-500 hover:text-red-600"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{eventType.description}
|
||||
</Text>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Durée:</span>
|
||||
<span className="font-medium">{eventType.duration} min</span>
|
||||
</div>
|
||||
{eventType.bufferTime && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Temps de battement:</span>
|
||||
<span className="font-medium">
|
||||
{eventType.bufferTime} min
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{eventType.maxBookingsPerDay && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Max par jour:</span>
|
||||
<span className="font-medium">
|
||||
{eventType.maxBookingsPerDay}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{eventType.minAdvanceBooking && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">
|
||||
Réservation à l'avance:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{eventType.minAdvanceBooking.value}{" "}
|
||||
{eventType.minAdvanceBooking.unit === "minutes"
|
||||
? "min"
|
||||
: eventType.minAdvanceBooking.unit === "hours"
|
||||
? "h"
|
||||
: "j"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500">Statut:</span>
|
||||
<ToggleButton
|
||||
isSelected={eventType.isActive}
|
||||
onChange={() =>
|
||||
toggleEventType({
|
||||
id: eventType.id,
|
||||
isActive: !eventType.isActive,
|
||||
})
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{eventType.isActive ? <CheckIcon /> : <XIcon />}
|
||||
{eventType.isActive ? "Actif" : "Inactif"}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{eventTypesData?.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Text className="text-gray-500 mb-4">
|
||||
Aucun type d'événement configuré
|
||||
</Text>
|
||||
<Button
|
||||
variant="solid"
|
||||
onPress={handleCreateEventType}
|
||||
className="[--btn-bg:var(--color-blue-800)]"
|
||||
>
|
||||
<PlusIcon /> Créer votre premier type
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<EventTypeModal
|
||||
isModalOpen={isModalOpen}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
editingEventType={editingEventType}
|
||||
formData={formData as EventType}
|
||||
setFormData={setFormData}
|
||||
handleSaveEventType={handleSaveEventType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -138,6 +138,36 @@ export type Database = {
|
|||
}
|
||||
Relationships: []
|
||||
}
|
||||
event_types: {
|
||||
Row: {
|
||||
config: Json
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string
|
||||
is_active: boolean
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
events: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
|
|
|
|||
|
|
@ -270,3 +270,98 @@ export const parseICSFile = (icsContent: string): ParsedICSEvent[] => {
|
|||
|
||||
return events;
|
||||
};
|
||||
|
||||
export const getTextColorFromTabloColor = (tabloColor: string): string => {
|
||||
// Map of tablo background colors to their corresponding text colors
|
||||
const colorMap: Record<string, string> = {
|
||||
// Blue variants
|
||||
"bg-blue-100": "text-blue-800",
|
||||
"bg-blue-200": "text-blue-900",
|
||||
"bg-blue-300": "text-blue-900",
|
||||
"bg-blue-400": "text-white",
|
||||
"bg-blue-500": "text-white",
|
||||
"bg-blue-600": "text-white",
|
||||
"bg-blue-700": "text-white",
|
||||
"bg-blue-800": "text-white",
|
||||
"bg-blue-900": "text-white",
|
||||
|
||||
// Green variants
|
||||
"bg-green-100": "text-green-800",
|
||||
"bg-green-200": "text-green-900",
|
||||
"bg-green-300": "text-green-900",
|
||||
"bg-green-400": "text-white",
|
||||
"bg-green-500": "text-white",
|
||||
"bg-green-600": "text-white",
|
||||
"bg-green-700": "text-white",
|
||||
"bg-green-800": "text-white",
|
||||
"bg-green-900": "text-white",
|
||||
|
||||
// Red variants
|
||||
"bg-red-100": "text-red-800",
|
||||
"bg-red-200": "text-red-900",
|
||||
"bg-red-300": "text-red-900",
|
||||
"bg-red-400": "text-white",
|
||||
"bg-red-500": "text-white",
|
||||
"bg-red-600": "text-white",
|
||||
"bg-red-700": "text-white",
|
||||
"bg-red-800": "text-white",
|
||||
"bg-red-900": "text-white",
|
||||
|
||||
// Yellow variants
|
||||
"bg-yellow-100": "text-yellow-800",
|
||||
"bg-yellow-200": "text-yellow-900",
|
||||
"bg-yellow-300": "text-yellow-900",
|
||||
"bg-yellow-400": "text-yellow-900",
|
||||
"bg-yellow-500": "text-white",
|
||||
"bg-yellow-600": "text-white",
|
||||
"bg-yellow-700": "text-white",
|
||||
"bg-yellow-800": "text-white",
|
||||
"bg-yellow-900": "text-white",
|
||||
|
||||
// Purple variants
|
||||
"bg-purple-100": "text-purple-800",
|
||||
"bg-purple-200": "text-purple-900",
|
||||
"bg-purple-300": "text-purple-900",
|
||||
"bg-purple-400": "text-white",
|
||||
"bg-purple-500": "text-white",
|
||||
"bg-purple-600": "text-white",
|
||||
"bg-purple-700": "text-white",
|
||||
"bg-purple-800": "text-white",
|
||||
"bg-purple-900": "text-white",
|
||||
|
||||
// Pink variants
|
||||
"bg-pink-100": "text-pink-800",
|
||||
"bg-pink-200": "text-pink-900",
|
||||
"bg-pink-300": "text-pink-900",
|
||||
"bg-pink-400": "text-white",
|
||||
"bg-pink-500": "text-white",
|
||||
"bg-pink-600": "text-white",
|
||||
"bg-pink-700": "text-white",
|
||||
"bg-pink-800": "text-white",
|
||||
"bg-pink-900": "text-white",
|
||||
|
||||
// Indigo variants
|
||||
"bg-indigo-100": "text-indigo-800",
|
||||
"bg-indigo-200": "text-indigo-900",
|
||||
"bg-indigo-300": "text-indigo-900",
|
||||
"bg-indigo-400": "text-white",
|
||||
"bg-indigo-500": "text-white",
|
||||
"bg-indigo-600": "text-white",
|
||||
"bg-indigo-700": "text-white",
|
||||
"bg-indigo-800": "text-white",
|
||||
"bg-indigo-900": "text-white",
|
||||
|
||||
// Gray variants
|
||||
"bg-gray-100": "text-gray-800",
|
||||
"bg-gray-200": "text-gray-900",
|
||||
"bg-gray-300": "text-gray-900",
|
||||
"bg-gray-400": "text-white",
|
||||
"bg-gray-500": "text-white",
|
||||
"bg-gray-600": "text-white",
|
||||
"bg-gray-700": "text-white",
|
||||
"bg-gray-800": "text-white",
|
||||
"bg-gray-900": "text-white",
|
||||
};
|
||||
|
||||
return colorMap[tabloColor] || "text-gray-900";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -138,6 +138,36 @@ export type Database = {
|
|||
}
|
||||
Relationships: []
|
||||
}
|
||||
event_types: {
|
||||
Row: {
|
||||
config: Json
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: string
|
||||
is_active: boolean
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
config?: Json
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: string
|
||||
is_active?: boolean
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
events: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
|
|
|
|||
Loading…
Reference in a new issue