feat: Permettre aux administrateurs de configurer les règles de devoirs
Les établissements ont besoin de protéger les élèves et familles des devoirs de dernière minute. Cette configuration au niveau tenant permet de définir des règles de timing (délai minimum, pas de devoir pour lundi après une heure limite) et un mode d'application (avertissement, blocage ou désactivé). Le service de validation est prêt pour être branché dans le flux de création de devoirs (Stories 5.4/5.5). L'historique des changements assure la traçabilité des modifications de configuration.
This commit is contained in:
868
frontend/src/routes/admin/homework-rules/+page.svelte
Normal file
868
frontend/src/routes/admin/homework-rules/+page.svelte
Normal file
@@ -0,0 +1,868 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { authenticatedFetch } from '$lib/auth/auth.svelte';
|
||||
import { getApiBaseUrl } from '$lib/api/config';
|
||||
|
||||
type RuleDefinition = {
|
||||
type: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type HomeworkRulesConfig = {
|
||||
id: string;
|
||||
rules: RuleDefinition[];
|
||||
enforcementMode: string;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type HistoryEntry = {
|
||||
previousRules: string | null;
|
||||
newRules: string;
|
||||
enforcementMode: string;
|
||||
enabled: boolean;
|
||||
changedBy: string;
|
||||
changedAt: string;
|
||||
};
|
||||
|
||||
// State
|
||||
let config = $state<HomeworkRulesConfig | null>(null);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let successMessage = $state<string | null>(null);
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
// Form state
|
||||
let enforcementMode = $state<string>('soft');
|
||||
let enabled = $state(true);
|
||||
let minimumDelayDays = $state<number>(3);
|
||||
let minimumDelayEnabled = $state(false);
|
||||
let noMondayAfterEnabled = $state(false);
|
||||
let noMondayAfterDay = $state<string>('friday');
|
||||
let noMondayAfterTime = $state<string>('12:00');
|
||||
|
||||
// History
|
||||
let history = $state<HistoryEntry[]>([]);
|
||||
let showHistory = $state(false);
|
||||
|
||||
// Derived
|
||||
let hasChanges = $derived.by(() => {
|
||||
if (!config) return false;
|
||||
|
||||
const currentRules = buildRules();
|
||||
const configRulesJson = JSON.stringify(config.rules);
|
||||
const newRulesJson = JSON.stringify(currentRules);
|
||||
|
||||
return (
|
||||
configRulesJson !== newRulesJson ||
|
||||
enforcementMode !== config.enforcementMode ||
|
||||
enabled !== config.enabled
|
||||
);
|
||||
});
|
||||
|
||||
let modeLabel = $derived(
|
||||
enforcementMode === 'soft'
|
||||
? 'Avertissement'
|
||||
: enforcementMode === 'hard'
|
||||
? 'Blocage'
|
||||
: 'Désactivé'
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
loadConfig();
|
||||
});
|
||||
|
||||
function buildRules(): RuleDefinition[] {
|
||||
const rules: RuleDefinition[] = [];
|
||||
|
||||
if (minimumDelayEnabled) {
|
||||
rules.push({
|
||||
type: 'minimum_delay',
|
||||
params: { days: minimumDelayDays }
|
||||
});
|
||||
}
|
||||
|
||||
if (noMondayAfterEnabled) {
|
||||
rules.push({
|
||||
type: 'no_monday_after',
|
||||
params: { day: noMondayAfterDay, time: noMondayAfterTime }
|
||||
});
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
function syncFormFromConfig(data: HomeworkRulesConfig) {
|
||||
enforcementMode = data.enforcementMode;
|
||||
enabled = data.enabled;
|
||||
|
||||
minimumDelayEnabled = false;
|
||||
noMondayAfterEnabled = false;
|
||||
|
||||
for (const rule of data.rules) {
|
||||
if (rule.type === 'minimum_delay') {
|
||||
minimumDelayEnabled = true;
|
||||
minimumDelayDays = (rule.params['days'] as number) ?? 3;
|
||||
}
|
||||
if (rule.type === 'no_monday_after') {
|
||||
noMondayAfterEnabled = true;
|
||||
noMondayAfterDay = (rule.params['day'] as string) ?? 'friday';
|
||||
noMondayAfterTime = (rule.params['time'] as string) ?? '12:00';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
const apiUrl = getApiBaseUrl();
|
||||
const response = await authenticatedFetch(`${apiUrl}/settings/homework-rules`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors du chargement de la configuration.');
|
||||
}
|
||||
|
||||
const data: HomeworkRulesConfig = await response.json();
|
||||
config = data;
|
||||
syncFormFromConfig(data);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
||||
config = null;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!hasChanges) return;
|
||||
|
||||
try {
|
||||
isSubmitting = true;
|
||||
error = null;
|
||||
successMessage = null;
|
||||
|
||||
const apiUrl = getApiBaseUrl();
|
||||
const response = await authenticatedFetch(`${apiUrl}/settings/homework-rules`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
rules: buildRules(),
|
||||
enforcementMode,
|
||||
enabled
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
errorData['hydra:description'] || errorData.message || 'Erreur lors de la sauvegarde.'
|
||||
);
|
||||
}
|
||||
|
||||
const data: HomeworkRulesConfig = await response.json();
|
||||
config = data;
|
||||
syncFormFromConfig(data);
|
||||
successMessage = 'Règles de devoirs mises à jour avec succès.';
|
||||
window.setTimeout(() => (successMessage = null), 4000);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (config) {
|
||||
syncFormFromConfig(config);
|
||||
}
|
||||
error = null;
|
||||
successMessage = null;
|
||||
}
|
||||
|
||||
function handleModeChange(newMode: string) {
|
||||
enforcementMode = newMode;
|
||||
if (newMode === 'disabled') {
|
||||
enabled = false;
|
||||
} else {
|
||||
enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const apiUrl = getApiBaseUrl();
|
||||
const response = await authenticatedFetch(`${apiUrl}/settings/homework-rules/history`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors du chargement de l\'historique.');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
history = (data['hydra:member'] ?? data['member'] ?? data) as HistoryEntry[];
|
||||
showHistory = true;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
||||
}
|
||||
}
|
||||
|
||||
function ruleTypeLabel(type: string): string {
|
||||
return type === 'minimum_delay'
|
||||
? 'Délai minimum'
|
||||
: type === 'no_monday_after'
|
||||
? 'Pas de devoir pour lundi'
|
||||
: type;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Règles de devoirs - Administration</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Règles de devoirs</h1>
|
||||
<p class="page-description">
|
||||
Configurez les règles de timing pour protéger les élèves et familles des devoirs de dernière
|
||||
minute.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-error" role="alert">
|
||||
<span>{error}</span>
|
||||
<button class="alert-close" onclick={() => (error = null)}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if successMessage}
|
||||
<div class="alert alert-success" role="status">
|
||||
<span>{successMessage}</span>
|
||||
<button class="alert-close" onclick={() => (successMessage = null)}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Chargement de la configuration...</p>
|
||||
</div>
|
||||
{:else if config}
|
||||
<!-- Mode d'application -->
|
||||
<section class="card">
|
||||
<h2>Mode d'application</h2>
|
||||
<p class="section-description">
|
||||
Choisissez comment les règles sont appliquées lorsqu'un enseignant crée un devoir.
|
||||
</p>
|
||||
|
||||
<div class="mode-selector" role="radiogroup" aria-label="Mode d'application">
|
||||
<button
|
||||
class="mode-option"
|
||||
class:selected={enforcementMode === 'soft'}
|
||||
role="radio"
|
||||
aria-checked={enforcementMode === 'soft'}
|
||||
onclick={() => handleModeChange('soft')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<span class="mode-icon">⚠</span>
|
||||
<span class="mode-label">Avertissement</span>
|
||||
<span class="mode-description"
|
||||
>L'enseignant voit un avertissement mais peut créer le devoir.</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="mode-option"
|
||||
class:selected={enforcementMode === 'hard'}
|
||||
role="radio"
|
||||
aria-checked={enforcementMode === 'hard'}
|
||||
onclick={() => handleModeChange('hard')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<span class="mode-icon">🚫</span>
|
||||
<span class="mode-label">Blocage</span>
|
||||
<span class="mode-description"
|
||||
>L'enseignant ne peut pas créer le devoir si la règle est enfreinte.</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="mode-option"
|
||||
class:selected={enforcementMode === 'disabled'}
|
||||
role="radio"
|
||||
aria-checked={enforcementMode === 'disabled'}
|
||||
onclick={() => handleModeChange('disabled')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<span class="mode-icon">❌</span>
|
||||
<span class="mode-label">Désactivé</span>
|
||||
<span class="mode-description"
|
||||
>Aucune vérification. Les enseignants créent les devoirs librement.</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Règles -->
|
||||
{#if enforcementMode !== 'disabled'}
|
||||
<section class="card">
|
||||
<h2>Règles actives</h2>
|
||||
<p class="section-description">Définissez les règles de timing des devoirs.</p>
|
||||
|
||||
<!-- Délai minimum -->
|
||||
<div class="rule-block">
|
||||
<label class="rule-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={minimumDelayEnabled}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<span class="rule-name">Délai minimum avant échéance</span>
|
||||
</label>
|
||||
|
||||
{#if minimumDelayEnabled}
|
||||
<div class="rule-params">
|
||||
<label class="param-label">
|
||||
Nombre de jours minimum :
|
||||
<input
|
||||
type="number"
|
||||
bind:value={minimumDelayDays}
|
||||
min="1"
|
||||
max="30"
|
||||
disabled={isSubmitting}
|
||||
class="param-input"
|
||||
/>
|
||||
</label>
|
||||
<p class="rule-example">
|
||||
Exemple : avec {minimumDelayDays} jours, un devoir pour vendredi ne peut pas être
|
||||
créé après {minimumDelayDays === 3 ? 'mardi' : `J-${minimumDelayDays}`}.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Pas de devoir pour lundi -->
|
||||
<div class="rule-block">
|
||||
<label class="rule-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={noMondayAfterEnabled}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<span class="rule-name">Pas de devoir pour lundi donné trop tard</span>
|
||||
</label>
|
||||
|
||||
{#if noMondayAfterEnabled}
|
||||
<div class="rule-params">
|
||||
<label class="param-label">
|
||||
Jour limite :
|
||||
<select bind:value={noMondayAfterDay} disabled={isSubmitting} class="param-select">
|
||||
<option value="thursday">Jeudi</option>
|
||||
<option value="friday">Vendredi</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="param-label">
|
||||
Heure limite :
|
||||
<input
|
||||
type="time"
|
||||
bind:value={noMondayAfterTime}
|
||||
disabled={isSubmitting}
|
||||
class="param-input"
|
||||
/>
|
||||
</label>
|
||||
<p class="rule-example">
|
||||
Exemple : un devoir pour lundi ne peut pas être créé après {noMondayAfterDay === 'friday' ? 'vendredi' : 'jeudi'}
|
||||
{noMondayAfterTime}.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Preview impact -->
|
||||
<section class="card">
|
||||
<h2>Résumé</h2>
|
||||
<div class="preview">
|
||||
<p>
|
||||
<strong>Mode :</strong>
|
||||
{modeLabel}
|
||||
</p>
|
||||
{#if enforcementMode !== 'disabled'}
|
||||
<p>
|
||||
<strong>Règles :</strong>
|
||||
{#if !minimumDelayEnabled && !noMondayAfterEnabled}
|
||||
Aucune règle configurée
|
||||
{:else}
|
||||
{#if minimumDelayEnabled}
|
||||
Délai minimum de {minimumDelayDays} jours.
|
||||
{/if}
|
||||
{#if noMondayAfterEnabled}
|
||||
Pas de devoir pour lundi après {noMondayAfterDay === 'friday' ? 'vendredi' : 'jeudi'}
|
||||
{noMondayAfterTime}.
|
||||
{/if}
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="preview-disabled">Les enseignants peuvent créer des devoirs librement.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<button class="btn-secondary" onclick={loadHistory} disabled={isSubmitting}>
|
||||
Voir l'historique
|
||||
</button>
|
||||
<div class="action-group">
|
||||
<button class="btn-secondary" onclick={handleCancel} disabled={isSubmitting || !hasChanges}>
|
||||
Annuler
|
||||
</button>
|
||||
<button class="btn-primary" onclick={handleSave} disabled={isSubmitting || !hasChanges}>
|
||||
{#if isSubmitting}
|
||||
Enregistrement...
|
||||
{:else}
|
||||
Enregistrer
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History modal -->
|
||||
{#if showHistory}
|
||||
<div
|
||||
class="modal-overlay"
|
||||
onclick={() => (showHistory = false)}
|
||||
onkeydown={(e) => e.key === 'Escape' && (showHistory = false)}
|
||||
role="presentation"
|
||||
>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="modal-content"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Historique des changements"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2>Historique des changements</h2>
|
||||
<button class="modal-close" onclick={() => (showHistory = false)}>×</button>
|
||||
</div>
|
||||
|
||||
{#if history.length === 0}
|
||||
<p class="empty-history">Aucun changement enregistré.</p>
|
||||
{:else}
|
||||
<div class="history-list">
|
||||
{#each history as entry}
|
||||
<div class="history-entry">
|
||||
<div class="history-date">{formatDate(entry.changedAt)}</div>
|
||||
<div class="history-details">
|
||||
<span class="history-mode">{entry.enforcementMode}</span>
|
||||
{#if !entry.enabled}
|
||||
<span class="history-badge disabled">Désactivé</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.alert-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
opacity: 0.6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 2px dashed #e5e7eb;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #8b5cf6;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
/* Mode selector */
|
||||
.mode-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mode-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mode-option:hover:not(:disabled) {
|
||||
border-color: #a5b4fc;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.mode-option.selected {
|
||||
border-color: #6366f1;
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.mode-option:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.mode-description {
|
||||
font-size: 0.8125rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Rules */
|
||||
.rule-block {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.rule-block:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.rule-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.rule-toggle input[type='checkbox'] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
accent-color: #6366f1;
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.rule-params {
|
||||
margin-top: 0.75rem;
|
||||
margin-left: 1.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.param-select {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.rule-example {
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Preview */
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.preview p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-disabled {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: white;
|
||||
color: #4b5563;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 300;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 0.8125rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.history-details {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.history-mode {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.history-badge.disabled {
|
||||
font-size: 0.75rem;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user