nice
This commit is contained in:
parent
29d1facc83
commit
32750a0cd7
@ -105,6 +105,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
},
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
@ -154,4 +155,3 @@ SIMPLE_JWT = {
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from datetime import datetime, date
|
||||
|
||||
class TimeEntry(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='time_entries')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='time_entries')
|
||||
date = models.DateField()
|
||||
start_time = models.TimeField(null=True, blank=True)
|
||||
end_time = models.TimeField(null=True, blank=True)
|
||||
|
||||
@ -3,14 +3,20 @@ from django.contrib.auth.models import User
|
||||
from .models import TimeEntry
|
||||
|
||||
class TimeEntrySerializer(serializers.ModelSerializer):
|
||||
user = serializers.StringRelatedField(read_only=True)
|
||||
username = serializers.CharField(source='user.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TimeEntry
|
||||
fields = ['id', 'user', 'username', 'date', 'start_time', 'end_time', 'duration', 'created_at']
|
||||
read_only_fields = ['user', 'duration', 'created_at']
|
||||
fields = '__all__'
|
||||
read_only_fields = ('user',)
|
||||
|
||||
def create(self, validated_data):
|
||||
# Set the user to the current request user
|
||||
validated_data['user'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Prevent updating the user field
|
||||
validated_data.pop('user', None)
|
||||
return super().update(instance, validated_data)
|
||||
class TimeEntryCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = TimeEntry
|
||||
@ -36,22 +42,7 @@ class TimeEntryUpdateSerializer(serializers.ModelSerializer):
|
||||
return attrs
|
||||
|
||||
class WeeklySummarySerializer(serializers.Serializer):
|
||||
user = serializers.StringRelatedField()
|
||||
user = serializers.CharField()
|
||||
username = serializers.CharField()
|
||||
total_hours = serializers.FloatField()
|
||||
days = serializers.ListField()
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
# Format the days data for frontend consumption
|
||||
formatted_days = []
|
||||
for day in data['days']:
|
||||
formatted_days.append({
|
||||
'date': day['date'],
|
||||
'day': day['day'],
|
||||
'start_time': day['start_time'],
|
||||
'end_time': day['end_time'],
|
||||
'duration': day['duration']
|
||||
})
|
||||
data['days'] = formatted_days
|
||||
return data
|
||||
entries = serializers.ListField(child=serializers.DictField())
|
||||
|
||||
@ -14,26 +14,42 @@ class UsersConfig(AppConfig):
|
||||
if 'runserver' in sys.argv:
|
||||
try:
|
||||
# Import here to avoid issues during Django startup
|
||||
from django.contrib.auth.models import User
|
||||
from .models import User
|
||||
from decouple import config
|
||||
from django.db import transaction
|
||||
|
||||
# Check if any users exist
|
||||
if not User.objects.exists():
|
||||
# Get credentials from environment variables or use defaults
|
||||
username = config('ADMIN_USERNAME', default='admin')
|
||||
password = config('ADMIN_PASSWORD', default='admin123')
|
||||
email = config('ADMIN_EMAIL', default='admin@boulangerie.fr')
|
||||
username = config('ADMIN_USERNAME', default='').strip()
|
||||
password = config('ADMIN_PASSWORD', default='')
|
||||
first_name = config('ADMIN_FIRST_NAME', default='').strip()
|
||||
last_name = config('ADMIN_LAST_NAME', default='').strip()
|
||||
|
||||
# Create the admin user
|
||||
with transaction.atomic():
|
||||
user = User.objects.create_superuser(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password
|
||||
)
|
||||
if not username or not password:
|
||||
print('ADMIN_USERNAME and ADMIN_PASSWORD are required; no admin was created.')
|
||||
return
|
||||
|
||||
print(f'Successfully created admin user "{username}"')
|
||||
with transaction.atomic():
|
||||
user, created = User.objects.get_or_create(
|
||||
username=username,
|
||||
defaults={
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'is_staff': True,
|
||||
'is_superuser': True,
|
||||
},
|
||||
)
|
||||
|
||||
if created:
|
||||
user.set_password(password)
|
||||
else:
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
|
||||
user.save()
|
||||
|
||||
action = 'created' if created else 'synchronized'
|
||||
print(f'Admin user "{username}" {action}.')
|
||||
except Exception as e:
|
||||
# Don't fail the entire Django startup if there's an error
|
||||
print(f'Error creating admin user: {e}')
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
# Generated by Django 5.2.17 on 2026-09-16 16:22
|
||||
# Generated by Django 5.2.17 on 2026-09-18 15:41
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@ -11,10 +14,37 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('color', models.CharField(default='#0000ff', max_length=7)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
|
||||
@ -1,17 +1,43 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import uuid
|
||||
|
||||
# Create your models here.
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
color = models.CharField(
|
||||
max_length=7,
|
||||
default='#0000ff',
|
||||
)
|
||||
|
||||
|
||||
class Invitation(models.Model):
|
||||
token = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_invitations')
|
||||
token = models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
unique=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='created_invitations',
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
expires_at = models.DateTimeField()
|
||||
used_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
used_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
from django.utils import timezone
|
||||
return self.used_at is None and self.expires_at > timezone.now()
|
||||
|
||||
return (
|
||||
self.used_at is None
|
||||
and self.expires_at > timezone.now()
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
@ -6,13 +6,28 @@ from rest_framework import serializers
|
||||
from .models import Invitation
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
is_admin = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
fields = ['id', 'username', 'first_name', 'last_name', 'is_admin', 'date_joined']
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'is_admin',
|
||||
'date_joined',
|
||||
'color',
|
||||
]
|
||||
read_only_fields = ['id', 'date_joined', 'is_admin']
|
||||
|
||||
def get_is_admin(self, obj):
|
||||
return obj.is_superuser or obj.is_staff
|
||||
@ -25,31 +40,61 @@ class RegisterSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
fields = ['username', 'password', 'password_confirm', 'first_name', 'last_name', 'invite']
|
||||
fields = [
|
||||
'username',
|
||||
'password',
|
||||
'password_confirm',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'invite',
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password_confirm']:
|
||||
raise serializers.ValidationError({'password_confirm': 'Les mots de passe ne correspondent pas.'})
|
||||
raise serializers.ValidationError({
|
||||
'password_confirm': (
|
||||
'Les mots de passe ne correspondent pas.'
|
||||
)
|
||||
})
|
||||
|
||||
return attrs
|
||||
|
||||
def validate_invite(self, value):
|
||||
try:
|
||||
invitation = Invitation.objects.get(token=value)
|
||||
except Invitation.DoesNotExist as error:
|
||||
raise serializers.ValidationError('Ce lien d’invitation est invalide.') from error
|
||||
raise serializers.ValidationError(
|
||||
'Ce lien d’invitation est invalide.'
|
||||
) from error
|
||||
|
||||
if not invitation.is_valid:
|
||||
raise serializers.ValidationError('Ce lien d’invitation est expire ou deja utilise.')
|
||||
raise serializers.ValidationError(
|
||||
'Ce lien d’invitation est expiré ou déjà utilisé.'
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
invitation_token = validated_data.pop('invite')
|
||||
validated_data.pop('password_confirm')
|
||||
|
||||
with transaction.atomic():
|
||||
invitation = Invitation.objects.select_for_update().get(token=invitation_token)
|
||||
invitation = (
|
||||
Invitation.objects
|
||||
.select_for_update()
|
||||
.get(token=invitation_token)
|
||||
)
|
||||
|
||||
if not invitation.is_valid:
|
||||
raise serializers.ValidationError({'invite': 'Ce lien d’invitation est expire ou deja utilise.'})
|
||||
raise serializers.ValidationError({
|
||||
'invite': (
|
||||
'Ce lien d’invitation est expiré ou déjà utilisé.'
|
||||
)
|
||||
})
|
||||
|
||||
user = User.objects.create_user(**validated_data)
|
||||
|
||||
invitation.used_at = timezone.now()
|
||||
invitation.save(update_fields=['used_at'])
|
||||
|
||||
return user
|
||||
@ -5,8 +5,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Invitation
|
||||
from .models import Invitation, User
|
||||
from .serializers import UserSerializer, RegisterSerializer
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
@ -44,20 +43,10 @@ class UserDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
return User.objects.all()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
# For updating user permissions, we need to handle is_staff and is_superuser specially
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
raise PermissionDenied('Seuls les administrateurs peuvent modifier les comptes.')
|
||||
|
||||
user = self.get_object()
|
||||
# Only allow updating specific fields
|
||||
allowed_fields = ['is_staff', 'is_superuser']
|
||||
for field in allowed_fields:
|
||||
if field in request.data:
|
||||
setattr(user, field, request.data[field])
|
||||
|
||||
user.save()
|
||||
serializer = self.get_serializer(user)
|
||||
return Response(serializer.data)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserPromoteView(APIView):
|
||||
@ -109,3 +98,4 @@ class InvitationCreateView(APIView):
|
||||
'token': str(invitation.token),
|
||||
'expires_at': invitation.expires_at,
|
||||
}, status=201)
|
||||
|
||||
|
||||
@ -32,6 +32,15 @@ export const userApi = {
|
||||
}
|
||||
},
|
||||
|
||||
updateUser: async (id, data) => {
|
||||
try {
|
||||
const response = await api.patch(`/users/${id}/`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error('Failed to update user: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
createInvitation: async () => {
|
||||
try {
|
||||
const response = await api.post('/users/invitations/');
|
||||
|
||||
@ -13,7 +13,10 @@ import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
const AdminUsersPage = () => {
|
||||
const [users, setUsers] = useState([]);
|
||||
@ -21,6 +24,17 @@ const AdminUsersPage = () => {
|
||||
const [error, setError] = useState('');
|
||||
const [inviteLink, setInviteLink] = useState('');
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [editForm, setEditForm] = useState({
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
color: '#0000ff',
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
});
|
||||
|
||||
const generateInviteLink = async () => {
|
||||
try {
|
||||
@ -106,6 +120,35 @@ const AdminUsersPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUser = async (userId) => {
|
||||
if (!/^#[0-9A-Fa-f]{6}$/.test(editForm.color)) {
|
||||
setError('La couleur doit être au format hexadécimal, par exemple #2563eb.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await userApi.updateUser(userId, editForm);
|
||||
const updatedUsers = users.map(user =>
|
||||
user.id === userId ? updatedUser : user
|
||||
);
|
||||
setUsers(updatedUsers);
|
||||
setEditingUser(null);
|
||||
setEditForm({
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
color: '#0000ff',
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Erreur lors de la mise à jour de l\'utilisateur');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="page-container page-layout admin-page">
|
||||
@ -137,6 +180,7 @@ const AdminUsersPage = () => {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nom d'utilisateur</TableHead>
|
||||
<TableHead>Couleur</TableHead>
|
||||
<TableHead>Rôle</TableHead>
|
||||
<TableHead>Inscrit le</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
@ -146,6 +190,12 @@ const AdminUsersPage = () => {
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: user.color || '#0000ff' }}></div>
|
||||
<span>{user.color || 'Bleu'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.is_admin ? "default" : "secondary"}>
|
||||
{user.is_admin ? 'Administrateur' : 'Utilisateur'}
|
||||
@ -154,6 +204,112 @@ const AdminUsersPage = () => {
|
||||
<TableCell>{new Date(user.date_joined).toLocaleDateString('fr-FR')}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Dialog open={editingUser === user.id} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditingUser(null);
|
||||
} else {
|
||||
setEditingUser(user.id);
|
||||
setEditForm({
|
||||
username: user.username || '',
|
||||
first_name: user.first_name || '',
|
||||
last_name: user.last_name || '',
|
||||
email: user.email || '',
|
||||
color: user.color || '#0000ff',
|
||||
is_active: user.is_active,
|
||||
is_staff: user.is_staff || user.is_admin,
|
||||
is_superuser: user.is_superuser,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={() => setEditingUser(user.id)}>
|
||||
Éditer
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modifier l'utilisateur</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`username-${user.id}`}>Nom d'utilisateur</Label>
|
||||
<Input
|
||||
id={`username-${user.id}`}
|
||||
value={editForm.username}
|
||||
onChange={(event) => setEditForm({ ...editForm, username: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`first-name-${user.id}`}>Prénom</Label>
|
||||
<Input
|
||||
id={`first-name-${user.id}`}
|
||||
value={editForm.first_name}
|
||||
onChange={(event) => setEditForm({ ...editForm, first_name: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`last-name-${user.id}`}>Nom</Label>
|
||||
<Input
|
||||
id={`last-name-${user.id}`}
|
||||
value={editForm.last_name}
|
||||
onChange={(event) => setEditForm({ ...editForm, last_name: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`email-${user.id}`}>E-mail</Label>
|
||||
<Input
|
||||
id={`email-${user.id}`}
|
||||
type="email"
|
||||
value={editForm.email}
|
||||
onChange={(event) => setEditForm({ ...editForm, email: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`color-${user.id}`}>Couleur</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id={`color-${user.id}`}
|
||||
type="color"
|
||||
value={/^#[0-9A-Fa-f]{6}$/.test(editForm.color) ? editForm.color : '#000000'}
|
||||
onChange={(event) => setEditForm({ ...editForm, color: event.target.value })}
|
||||
className="h-10 w-14 cursor-pointer p-1"
|
||||
/>
|
||||
<Input
|
||||
aria-label="Valeur hexadécimale de la couleur"
|
||||
value={editForm.color}
|
||||
pattern="^#[0-9A-Fa-f]{6}$"
|
||||
onChange={(event) => setEditForm({ ...editForm, color: event.target.value })}
|
||||
className="max-w-36 font-mono uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-3 text-sm">
|
||||
<Checkbox
|
||||
checked={editForm.is_active}
|
||||
onCheckedChange={(checked) => setEditForm({ ...editForm, is_active: checked })}
|
||||
/>
|
||||
Compte actif
|
||||
</label>
|
||||
<label className="flex items-center gap-3 text-sm">
|
||||
<Checkbox
|
||||
checked={editForm.is_staff}
|
||||
onCheckedChange={(checked) => setEditForm({
|
||||
...editForm,
|
||||
is_staff: checked,
|
||||
is_superuser: checked ? editForm.is_superuser : false,
|
||||
})}
|
||||
/>
|
||||
Administrateur
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">Le mot de passe ne peut pas être modifié ici.</p>
|
||||
</div>
|
||||
<Button onClick={() => handleEditUser(user.id)}>
|
||||
Sauvegarder
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{!user.is_admin && (
|
||||
<ConfirmDialog
|
||||
title="Promouvoir l'utilisateur"
|
||||
|
||||
@ -45,30 +45,33 @@ const LoginPage = () => {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Nom d'utilisateur:</Label>
|
||||
<Input
|
||||
id="username"
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Mot de passe:</Label>
|
||||
<Input
|
||||
className="current-password"
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Se connecter
|
||||
Se connecter
|
||||
</Button>
|
||||
</form>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -548,16 +548,6 @@ const WeeklySummaryPage = ({ userId }) => {
|
||||
Précédente
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDay(-1)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Jour précédent
|
||||
</Button>
|
||||
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-sm"
|
||||
@ -580,13 +570,33 @@ const WeeklySummaryPage = ({ userId }) => {
|
||||
)}
|
||||
</Badge>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateWeek(1)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
Suivante
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDay(-1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-sm"
|
||||
>
|
||||
{weekDays[
|
||||
selectedDayIndex
|
||||
].toLocaleDateString(
|
||||
{displayDay.toLocaleDateString(
|
||||
'fr-FR',
|
||||
{
|
||||
weekday: 'long',
|
||||
@ -600,58 +610,12 @@ const WeeklySummaryPage = ({ userId }) => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDay(1)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
Jour suivant
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateWeek(1)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
Suivante
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDay(-1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-sm"
|
||||
>
|
||||
{displayDay.toLocaleDateString(
|
||||
'fr-FR',
|
||||
{
|
||||
weekday: 'long',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
},
|
||||
)}
|
||||
</Badge>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDay(1)}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user