from django.db import models from django.utils.translation import gettext_lazy as _ # Widget code choices WIDGET_CODE_CHOICES = [ ('intervention_stats', _('Statistiques interventions')), ('contracts_summary', _('Résumé contrats')), ('map_today', _('Carte du jour')), ('operator_shortcuts', _('Raccourcis opérateur')), ('assets_links', _('Liens assets')), ('stock_summary', _('Résumé stock')), ('observer_shortcuts', _('Raccourcis observateur')), ('incident_counter', _('Compteur incidents à traiter')), ('intervention_to_verify', _('Compteur interventions à vérifier')), ('intervention_to_plan', _('Compteur interventions à planifier')), ('intervention_to_approve', _('Compteur interventions à approuver')), ('intervention_checked_to_approve', _('Compteur interventions vérifiées à approuver')), ('intervention_finished', _('Compteur interventions terminées')), ('stock_actions', _('Actions stock')), ] # Condition type choices CONDITION_TYPE_CHOICES = [ ('role', _('Rôle')), ('team', _('Équipe')), ('contract', _('Contrat')), ('thematic', _('Thématique')), ('is_intern', _('Statut interne')), ] class HomeWidget(models.Model): """Définit un widget disponible pour la page d'accueil.""" widget_code = models.CharField( max_length=50, choices=WIDGET_CODE_CHOICES, unique=True, verbose_name=_("Code du widget") ) default_template_override = models.CharField( max_length=255, blank=True, null=True, verbose_name=_("Template par défaut (surcharge)"), help_text=_("Chemin du template optionnel (sinon home/widgets/.html)") ) is_active = models.BooleanField( default=True, verbose_name=_("Actif") ) class Meta: verbose_name = _("Widget home") verbose_name_plural = _("Widgets home") ordering = ['widget_code'] def __str__(self): return self.get_widget_code_display() def get_default_template_name(self): """Retourne le nom du template par défaut pour ce widget.""" if self.default_template_override: return self.default_template_override return f'home/widgets/{self.widget_code}.html' def resolve_for_user(self, user_config, user_team, user_contracts): """ Résout le widget pour un utilisateur donné. Args: user_config: Instance de UserConfig user_team: Instance de CompanyTeam ou None user_contracts: QuerySet de Contract Returns: dict avec {widget, order, template_name} ou None si non visible """ # Get all active condition groups, sorted by priority condition_groups = self.condition_groups.filter(is_active=True).order_by('priority') # Iterate through groups until finding the first one that matches for group in condition_groups: if group.evaluate(user_config, user_team, user_contracts): template_name = group.template_override or self.get_default_template_name() return { 'widget': self, 'order': group.order, 'template_name': template_name, 'col_width': group.col_width, 'col_order': group.col_order, } # If no group is defined, the widget is visible to all with order 9999 if not condition_groups.exists(): return { 'widget': self, 'order': 9999, 'template_name': self.get_default_template_name(), 'col_width': 12, 'col_order': 0, } # No group matches return None class HomeWidgetConditionGroup(models.Model): """Groupe de conditions pour un widget (conditions liées par ET).""" widget = models.ForeignKey( HomeWidget, on_delete=models.CASCADE, related_name='condition_groups', verbose_name=_("Widget") ) label = models.CharField( max_length=255, verbose_name=_("Libellé"), help_text=_("Libellé informatif (ex: 'Stock manager - priorité haute')") ) order = models.PositiveIntegerField( verbose_name=_("Ordre de la ligne"), help_text=_("Les widgets ayant le même ordre seront placés dans la même ligne Bootstrap.") ) col_width = models.PositiveSmallIntegerField( default=12, verbose_name=_("Largeur (colonnes Bootstrap)"), help_text=_("Nombre de colonnes Bootstrap (1-12). 12 = pleine largeur, 6 = demi-largeur, 4 = un tiers, etc.") ) col_order = models.PositiveIntegerField( default=0, verbose_name=_("Ordre dans la ligne"), help_text=_("Position du widget dans sa ligne (parmi les widgets du même ordre). Les valeurs les plus faibles apparaissent en premier.") ) priority = models.PositiveIntegerField( default=0, verbose_name=_("Priorité"), help_text=_("Ordre d'évaluation des groupes (le premier qui matche l'emporte)") ) template_override = models.CharField( max_length=255, blank=True, null=True, verbose_name=_("Template (surcharge)"), help_text=_("Surcharge du template pour ce groupe spécifiquement") ) is_active = models.BooleanField( default=True, verbose_name=_("Actif") ) class Meta: verbose_name = _("Groupe de conditions") verbose_name_plural = _("Groupes de conditions") ordering = ['widget', 'priority'] def __str__(self): return f"{self.widget.widget_code} - {self.label}" def evaluate(self, user_config, user_team, user_contracts): """ Évalue si ce groupe de conditions matche pour l'utilisateur. Args: user_config: Instance de UserConfig user_team: Instance de CompanyTeam ou None user_contracts: QuerySet de Contract Returns: True si TOUTES les conditions du groupe sont vraies (logique ET) """ conditions = self.conditions.filter(is_active=True) # If no conditions, the group doesn't match if not conditions.exists(): return False # All conditions must be true for condition in conditions: if not condition.evaluate(user_config, user_team, user_contracts): return False return True def get_summary(self): """Retourne une chaîne lisible des conditions du groupe.""" conditions = self.conditions.filter(is_active=True) if not conditions.exists(): return "Aucune condition" parts = [] for condition in conditions: part = "" if condition.condition_type == 'role' and condition.role: part = f"role={condition.role.name}" elif condition.condition_type == 'team' and condition.team: part = f"team={condition.team.name}" elif condition.condition_type == 'contract' and condition.contract: part = f"contract={condition.contract.contract_number}" elif condition.condition_type == 'thematic' and condition.thematic: part = f"thematic={condition.thematic.code}" elif condition.condition_type == 'is_intern': part = f"is_intern={condition.is_intern}" if part: if condition.is_negated: part = f"not({part})" parts.append(part) return " ET ".join(parts) if parts else "Aucune condition" class HomeWidgetCondition(models.Model): """Condition individuelle dans un groupe.""" group = models.ForeignKey( HomeWidgetConditionGroup, on_delete=models.CASCADE, related_name='conditions', verbose_name=_("Groupe") ) condition_type = models.CharField( max_length=20, choices=CONDITION_TYPE_CHOICES, verbose_name=_("Type de condition") ) # Foreign keys pour les différents types de conditions role = models.ForeignKey( 'common.Role', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Rôle"), help_text=_("Utilisé si type = 'role'") ) team = models.ForeignKey( 'contracts.CompanyTeam', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Équipe"), help_text=_("Utilisé si type = 'team'") ) contract = models.ForeignKey( 'contracts.Contract', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Contrat"), help_text=_("Utilisé si type = 'contract'") ) thematic = models.ForeignKey( 'common.Thematic', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Thématique"), help_text=_("Utilisé si type = 'thematic'") ) is_intern = models.BooleanField( null=True, blank=True, verbose_name=_("Statut interne"), help_text=_("Utilisé si type = 'is_intern'") ) is_negated = models.BooleanField( default=False, verbose_name=_("NOT"), help_text=_("Inverser le sens de la condition (ex: NOT role=Admin)") ) is_active = models.BooleanField( default=True, verbose_name=_("Actif") ) class Meta: verbose_name = _("Condition") verbose_name_plural = _("Conditions") def __str__(self): prefix = "NOT " if self.is_negated else "" if self.condition_type == 'role' and self.role: return f"{prefix}Role: {self.role.name}" elif self.condition_type == 'team' and self.team: return f"{prefix}Team: {self.team.name}" elif self.condition_type == 'contract' and self.contract: return f"{prefix}Contract: {self.contract.contract_number}" elif self.condition_type == 'thematic' and self.thematic: return f"{prefix}Thematic: {self.thematic.code}" elif self.condition_type == 'is_intern': return f"{prefix}Is intern: {self.is_intern}" return f"{prefix}{self.condition_type}" def evaluate(self, user_config, user_team, user_contracts): """ Évalue si cette condition est vraie pour l'utilisateur. Args: user_config: Instance de UserConfig user_team: Instance de CompanyTeam ou None user_contracts: QuerySet de Contract Returns: True si la condition est remplie """ if self.condition_type == 'role' and self.role: res = user_config.roles.filter(id=self.role.id).exists() elif self.condition_type == 'team' and self.team: if not user_team: res = False elif hasattr(user_team, 'filter'): res = user_team.filter(id=self.team.id).exists() elif hasattr(user_team, '__iter__'): res = any(t.id == self.team.id for t in user_team) else: res = user_team.id == self.team.id elif self.condition_type == 'contract' and self.contract: res = user_contracts.filter(id=self.contract.id).exists() elif self.condition_type == 'thematic' and self.thematic: res = user_config.userthematics.filter( thematic_id=self.thematic.id ).exists() elif self.condition_type == 'is_intern': res = user_config.is_intern == self.is_intern else: res = False if self.is_negated: return not res return res class TeambuildingVote(models.Model): voter_name = models.CharField(max_length=100, blank=True, null=True, verbose_name=_("Nom (optionnel)")) vote_gael = models.IntegerField(default=0, verbose_name=_("La journée 'Gaël Non Di Dju'")) vote_charly = models.IntegerField(default=0, verbose_name=_("La journée 'La cité de Charly'")) vote_tequila = models.IntegerField(default=0, verbose_name=_("La journée 'Tequila Cadena'")) vote_vivien = models.IntegerField(default=0, verbose_name=_("La journée 'Vivien de La Grande Propreté'")) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Vote #{self.id} - {self.voter_name or 'Anonyme'}" class Meta: verbose_name = _("Vote Teambuilding") verbose_name_plural = _("Votes Teambuilding")