perf(contracts): optimize queries, add server-side pagination, search, thematic/contract filters and redesign UI
This commit is contained in:
parent
34e9743cc3
commit
0732f89274
11 changed files with 2088 additions and 289 deletions
Binary file not shown.
|
|
@ -2399,3 +2399,93 @@ msgstr "Map color"
|
||||||
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
||||||
msgstr "Display color of the lot on the map (hexadecimal code, e.g. #28a745)."
|
msgstr "Display color of the lot on the map (hexadecimal code, e.g. #28a745)."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer les commandes et marchés"
|
||||||
|
msgstr "Filter orders and contracts"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Sélectionnez les critères de recherche"
|
||||||
|
msgstr "Select search criteria"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun"
|
||||||
|
msgstr "None"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Marchés"
|
||||||
|
msgstr "Contracts"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer la liste des marchés..."
|
||||||
|
msgstr "Filter contract list..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Période de commande"
|
||||||
|
msgstr "Order period"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Rechercher une commande, un marché, une description, un prestataire..."
|
||||||
|
msgstr "Search order, contract, description, provider..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Effacer la recherche"
|
||||||
|
msgstr "Clear search"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filters"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Afficher par"
|
||||||
|
msgstr "Show by"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres actifs"
|
||||||
|
msgstr "Active filters"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Supprimer ce critère"
|
||||||
|
msgstr "Remove this filter"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Clear all"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Télécharger le bon de commande"
|
||||||
|
msgstr "Download order document"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucune commande ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "No orders match your current search criteria."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Réinitialiser les filtres"
|
||||||
|
msgstr "Reset filters"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> commande(s)"
|
||||||
|
msgstr "Showing <strong>%(start)s</strong> to <strong>%(end)s</strong> of <strong>%(total)s</strong> order(s)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des commandes"
|
||||||
|
msgstr "Orders pagination"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Détail du contrat"
|
||||||
|
msgstr "Contract details"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun marché ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "No contracts match your current search criteria."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> marché(s)"
|
||||||
|
msgstr "Showing <strong>%(start)s</strong> to <strong>%(end)s</strong> of <strong>%(total)s</strong> contract(s)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des marchés"
|
||||||
|
msgstr "Contracts pagination"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2449,3 +2449,93 @@ msgstr "Couleur cartographique"
|
||||||
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
||||||
msgstr "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
msgstr "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer les commandes et marchés"
|
||||||
|
msgstr "Filtrer les commandes et marchés"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Sélectionnez les critères de recherche"
|
||||||
|
msgstr "Sélectionnez les critères de recherche"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun"
|
||||||
|
msgstr "Aucun"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Marchés"
|
||||||
|
msgstr "Marchés"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer la liste des marchés..."
|
||||||
|
msgstr "Filtrer la liste des marchés..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Période de commande"
|
||||||
|
msgstr "Période de commande"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Rechercher une commande, un marché, une description, un prestataire..."
|
||||||
|
msgstr "Rechercher une commande, un marché, une description, un prestataire..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Effacer la recherche"
|
||||||
|
msgstr "Effacer la recherche"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filtres"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Afficher par"
|
||||||
|
msgstr "Afficher par"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres actifs"
|
||||||
|
msgstr "Filtres actifs"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Supprimer ce critère"
|
||||||
|
msgstr "Supprimer ce critère"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Tout effacer"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Télécharger le bon de commande"
|
||||||
|
msgstr "Télécharger le bon de commande"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucune commande ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "Aucune commande ne correspond à vos critères de recherche actuels."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Réinitialiser les filtres"
|
||||||
|
msgstr "Réinitialiser les filtres"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> commande(s)"
|
||||||
|
msgstr "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> commande(s)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des commandes"
|
||||||
|
msgstr "Pagination des commandes"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Détail du contrat"
|
||||||
|
msgstr "Détail du contrat"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun marché ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "Aucun marché ne correspond à vos critères de recherche actuels."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> marché(s)"
|
||||||
|
msgstr "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> marché(s)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des marchés"
|
||||||
|
msgstr "Pagination des marchés"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2405,3 +2405,93 @@ msgstr "Kaartkleur"
|
||||||
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
msgid "Couleur d'affichage du lot sur la carte (code hexadécimal, ex: #28a745)."
|
||||||
msgstr "Weergavekleur van het perceel op de kaart (hexadecimale code, bijv. #28a745)."
|
msgstr "Weergavekleur van het perceel op de kaart (hexadecimale code, bijv. #28a745)."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer les commandes et marchés"
|
||||||
|
msgstr "Bestellingen en opdrachten filteren"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Sélectionnez les critères de recherche"
|
||||||
|
msgstr "Selecteer de zoekcriteria"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun"
|
||||||
|
msgstr "Geen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Marchés"
|
||||||
|
msgstr "Opdrachten"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtrer la liste des marchés..."
|
||||||
|
msgstr "Lijst met opdrachten filteren..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Période de commande"
|
||||||
|
msgstr "Bestelperiode"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Rechercher une commande, un marché, une description, un prestataire..."
|
||||||
|
msgstr "Zoeken op bestelnummer, opdracht, omschrijving, dienstverlener..."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Effacer la recherche"
|
||||||
|
msgstr "Zoekopdracht wissen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filters"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Afficher par"
|
||||||
|
msgstr "Weergeven per"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Filtres actifs"
|
||||||
|
msgstr "Actieve filters"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Supprimer ce critère"
|
||||||
|
msgstr "Dit criterium verwijderen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Alles wissen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Télécharger le bon de commande"
|
||||||
|
msgstr "Bestelbon downloaden"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucune commande ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "Geen enkele bestelling voldoet aan de huidige zoekcriteria."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Réinitialiser les filtres"
|
||||||
|
msgstr "Filters opnieuw instellen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> commande(s)"
|
||||||
|
msgstr "Weergave van <strong>%(start)s</strong> tot <strong>%(end)s</strong> van <strong>%(total)s</strong> bestelling(en)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des commandes"
|
||||||
|
msgstr "Paginering van bestellingen"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Détail du contrat"
|
||||||
|
msgstr "Contractdetail"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Aucun marché ne correspond à vos critères de recherche actuels."
|
||||||
|
msgstr "Geen enkele opdracht voldoet aan de huidige zoekcriteria."
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Affichage de <strong>%(start)s</strong> à <strong>%(end)s</strong> sur <strong>%(total)s</strong> marché(s)"
|
||||||
|
msgstr "Weergave van <strong>%(start)s</strong> tot <strong>%(end)s</strong> van <strong>%(total)s</strong> opdracht(en)"
|
||||||
|
|
||||||
|
#: contracts/templates/contracts/contracts_index.html
|
||||||
|
msgid "Pagination des marchés"
|
||||||
|
msgstr "Paginering van opdrachten"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
831
loko/contracts/static/contracts/css/contracts_index.css
Normal file
831
loko/contracts/static/contracts/css/contracts_index.css
Normal file
|
|
@ -0,0 +1,831 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600&display=swap');
|
||||||
|
|
||||||
|
/* Main Page Container */
|
||||||
|
.contracts-page-container {
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 1.5rem auto 3rem auto;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Header */
|
||||||
|
.contracts-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-tabs {
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-tabs .nav-link {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
padding: 0.65rem 1.25rem;
|
||||||
|
background: transparent;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-tabs .nav-link:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-tabs .nav-link.active {
|
||||||
|
color: #0f172a;
|
||||||
|
border-bottom-color: #0f172a;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-counter-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-tabs .nav-link.active .tab-counter-badge {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Action Button */
|
||||||
|
.btn-new-order {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #0f172a;
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-new-order:hover {
|
||||||
|
background-color: #1e293b;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #1e293b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar: Search, Filters & Settings */
|
||||||
|
.contracts-toolbar {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.85rem 1.15rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.85rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-widget {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 360px;
|
||||||
|
max-width: 550px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon-left {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.85rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 2.25rem 0.5rem 2.35rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #0f172a;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
display: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear-btn:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-trigger-btn {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-trigger-btn:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-count-badge {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #334155;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Filters Pills */
|
||||||
|
.active-filters-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filter-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filter-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
transition: all 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filter-chip .remove-filter-btn {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filter-chip .remove-filter-btn:hover {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-all-filters {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #dc2626;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-all-filters:hover {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Container & Design */
|
||||||
|
.contracts-table-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table thead th {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1.5px solid #e2e8f0;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table thead th.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table thead th.sortable:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table tbody tr {
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table tbody tr:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table tbody td {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #334155;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Link Badge */
|
||||||
|
.table-code-link {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #0f172a !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-code-link:hover {
|
||||||
|
color: #2563eb !important;
|
||||||
|
border-color: #93c5fd;
|
||||||
|
background-color: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Action Button */
|
||||||
|
.table-action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-action-btn:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.32rem 0.7rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.pending {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.sent {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
color: #0284c7;
|
||||||
|
border-color: #bae6fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.completed {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.invoiced {
|
||||||
|
background-color: #faf5ff;
|
||||||
|
color: #7c3aed;
|
||||||
|
border-color: #e9d5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.paid {
|
||||||
|
background-color: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
border-color: #a7f3d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.cancelled {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Icon Link */
|
||||||
|
.file-download-btn {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-download-btn:hover {
|
||||||
|
color: #b91c1c;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.table-empty-state {
|
||||||
|
padding: 4rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto 1.25rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Bar */
|
||||||
|
.contracts-pagination-bar {
|
||||||
|
padding: 0.85rem 1.25rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-nav .pagination {
|
||||||
|
margin-bottom: 0;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-nav .page-link {
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
min-width: 32px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-nav .page-link:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-nav .page-item.active .page-link {
|
||||||
|
background-color: #0f172a;
|
||||||
|
border-color: #0f172a;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-nav .page-item.disabled .page-link {
|
||||||
|
color: #94a3b8;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Airbnb-style Modern Filter Modal ──────────────────── */
|
||||||
|
#filterModal .modal-dialog {
|
||||||
|
max-width: 820px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-content {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 88vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .btn-close {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Sections */
|
||||||
|
.filter-section {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.15rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-header label,
|
||||||
|
.filter-section-header .section-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons button {
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons button:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Chips */
|
||||||
|
.filter-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.4rem 0.85rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #334155;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option label:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default Chips Checked (Thematics) */
|
||||||
|
.filter-option input[type="checkbox"]:checked + label:not(.context-badge) {
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
border-color: #0f172a !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contextual Status Badges (Filter Modal) */
|
||||||
|
.filter-option label.context-badge {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-width: 1.5px;
|
||||||
|
border-style: solid;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option label.context-badge.pending { border-color: #f59e0b; color: #d97706; }
|
||||||
|
.filter-option label.context-badge.sent { border-color: #0284c7; color: #0284c7; }
|
||||||
|
.filter-option label.context-badge.completed { border-color: #16a34a; color: #16a34a; }
|
||||||
|
.filter-option label.context-badge.invoiced { border-color: #7c3aed; color: #7c3aed; }
|
||||||
|
.filter-option label.context-badge.paid { border-color: #059669; color: #059669; }
|
||||||
|
.filter-option label.context-badge.cancelled { border-color: #dc2626; color: #dc2626; }
|
||||||
|
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.pending { background-color: #fffbeb; }
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.sent { background-color: #f0f9ff; }
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.completed { background-color: #f0fdf4; }
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.invoiced { background-color: #faf5ff; }
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.paid { background-color: #ecfdf5; }
|
||||||
|
.filter-option input[type="checkbox"]:not(:checked):hover + label.context-badge.cancelled { background-color: #fef2f2; }
|
||||||
|
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.pending { background-color: #f59e0b !important; border-color: #f59e0b !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.sent { background-color: #0284c7 !important; border-color: #0284c7 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.completed { background-color: #16a34a !important; border-color: #16a34a !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.invoiced { background-color: #7c3aed !important; border-color: #7c3aed !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.paid { background-color: #059669 !important; border-color: #059669 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge.cancelled { background-color: #dc2626 !important; border-color: #dc2626 !important; }
|
||||||
|
|
||||||
|
/* Contracts scrollable list in Filter Modal */
|
||||||
|
.contracts-filter-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-filter-search-box {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-filter-search-box input {
|
||||||
|
padding-left: 2rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-filter-search-box .search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.65rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-filter-scroll {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-filter-item {
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-filter-item:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-filter-item label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
color: #334155;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.contracts-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-widget {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contracts-pagination-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
155
loko/contracts/static/contracts/js/contracts_index.js
Normal file
155
loko/contracts/static/contracts/js/contracts_index.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// ─── Recherche principale ───
|
||||||
|
const searchInput = document.getElementById('main-search-input');
|
||||||
|
const searchClearBtn = document.getElementById('main-search-clear');
|
||||||
|
const searchBtn = document.getElementById('main-search-btn');
|
||||||
|
|
||||||
|
function toggleClearBtn() {
|
||||||
|
if (!searchClearBtn) return;
|
||||||
|
if (searchInput && searchInput.value.trim().length > 0) {
|
||||||
|
searchClearBtn.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
searchClearBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
toggleClearBtn();
|
||||||
|
searchInput.addEventListener('input', toggleClearBtn);
|
||||||
|
|
||||||
|
searchInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
executeSearch(searchInput.value.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchBtn && searchInput) {
|
||||||
|
searchBtn.addEventListener('click', function() {
|
||||||
|
executeSearch(searchInput.value.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchClearBtn) {
|
||||||
|
searchClearBtn.addEventListener('click', function() {
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = '';
|
||||||
|
toggleClearBtn();
|
||||||
|
}
|
||||||
|
executeSearch('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSearch(query) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (query) {
|
||||||
|
url.searchParams.set('search', query);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('search');
|
||||||
|
}
|
||||||
|
url.searchParams.delete('page');
|
||||||
|
url.searchParams.delete('contracts_page');
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sélecteur de taille de page ───
|
||||||
|
const pageSizeSelect = document.getElementById('page-size-select');
|
||||||
|
if (pageSizeSelect) {
|
||||||
|
pageSizeSelect.addEventListener('change', function() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('page_size', this.value);
|
||||||
|
url.searchParams.delete('page');
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Suppression rapide des filtres actifs (chips) ───
|
||||||
|
document.querySelectorAll('.remove-filter-btn').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const param = this.dataset.param;
|
||||||
|
const val = this.dataset.val;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
|
||||||
|
if (param === 'search' || param === 'date_from' || param === 'date_to') {
|
||||||
|
url.searchParams.delete(param);
|
||||||
|
} else {
|
||||||
|
const remaining = url.searchParams.getAll(param).filter(v => v !== val);
|
||||||
|
url.searchParams.delete(param);
|
||||||
|
remaining.forEach(v => url.searchParams.append(param, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
url.searchParams.delete('page');
|
||||||
|
url.searchParams.delete('contracts_page');
|
||||||
|
window.location.href = url.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Modal de Filtres : Boutons "Tous" / "Aucun" ───
|
||||||
|
document.querySelectorAll('.select-all-btn').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const section = this.closest('.filter-section');
|
||||||
|
if (section) {
|
||||||
|
section.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.deselect-all-btn').forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const section = this.closest('.filter-section');
|
||||||
|
if (section) {
|
||||||
|
section.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Filtrage rapide dans la liste des marchés dans la modale ───
|
||||||
|
const contractSearchInput = document.getElementById('contract-filter-search');
|
||||||
|
if (contractSearchInput) {
|
||||||
|
contractSearchInput.addEventListener('input', function() {
|
||||||
|
const term = this.value.toLowerCase().trim();
|
||||||
|
document.querySelectorAll('.contract-filter-item').forEach(function(item) {
|
||||||
|
const text = item.textContent.toLowerCase();
|
||||||
|
if (!term || text.includes(term)) {
|
||||||
|
item.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Synchronisation de l'onglet actif ───
|
||||||
|
const tabButtons = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
||||||
|
const filterTabInput = document.getElementById('filter-tab-input');
|
||||||
|
|
||||||
|
tabButtons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('shown.bs.tab', function(e) {
|
||||||
|
const targetId = e.target.getAttribute('data-bs-target');
|
||||||
|
const tabName = targetId === '#contracts-content' ? 'contracts' : 'orders';
|
||||||
|
|
||||||
|
if (filterTabInput) {
|
||||||
|
filterTabInput.value = tabName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('tab', tabName);
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restaurer l'onglet actif si spécifié dans l'URL
|
||||||
|
const currentTabParam = new URLSearchParams(window.location.search).get('tab');
|
||||||
|
if (currentTabParam === 'contracts') {
|
||||||
|
const contractsTabBtn = document.getElementById('contracts-tab');
|
||||||
|
if (contractsTabBtn && window.bootstrap && bootstrap.Tab) {
|
||||||
|
bootstrap.Tab.getOrCreateInstance(contractsTabBtn).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -3,298 +3,512 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{% static 'contracts/css/contracts_index.css' %}">
|
||||||
<style>
|
|
||||||
.status-badge.pending { background-color: #ffc107; color: black; }
|
|
||||||
.status-badge.sent { background-color: #0dcaf0; color: white; }
|
|
||||||
.status-badge.completed { background-color: #198754; color: white; }
|
|
||||||
.status-badge.invoiced { background-color: #6f42c1; color: white; }
|
|
||||||
.status-badge.paid { background-color: #28a745; color: white; }
|
|
||||||
.status-badge.cancelled { background-color: #dc3545; color: white; }
|
|
||||||
|
|
||||||
/* Styles pour les badges de filtre - Pattern inspiré des interventions */
|
|
||||||
.filter-section {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-option {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-option input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-option label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid currentColor;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Couleurs des badges par statut - Inactive state (outline) */
|
|
||||||
.filter-option label.context-badge.pending {
|
|
||||||
border-color: #ffc107;
|
|
||||||
color: #ffc107;
|
|
||||||
}
|
|
||||||
.filter-option label.context-badge.sent {
|
|
||||||
border-color: #0dcaf0;
|
|
||||||
color: #0dcaf0;
|
|
||||||
}
|
|
||||||
.filter-option label.context-badge.completed {
|
|
||||||
border-color: #198754;
|
|
||||||
color: #198754;
|
|
||||||
}
|
|
||||||
.filter-option label.context-badge.invoiced {
|
|
||||||
border-color: #6f42c1;
|
|
||||||
color: #6f42c1;
|
|
||||||
}
|
|
||||||
.filter-option label.context-badge.paid {
|
|
||||||
border-color: #28a745;
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
.filter-option label.context-badge.cancelled {
|
|
||||||
border-color: #dc3545;
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Checked state - Filled */
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.pending {
|
|
||||||
background-color: #ffc107;
|
|
||||||
border-color: #ffc107;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.sent {
|
|
||||||
background-color: #0dcaf0;
|
|
||||||
border-color: #0dcaf0;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.completed {
|
|
||||||
background-color: #198754;
|
|
||||||
border-color: #198754;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.invoiced {
|
|
||||||
background-color: #6f42c1;
|
|
||||||
border-color: #6f42c1;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.paid {
|
|
||||||
background-color: #28a745;
|
|
||||||
border-color: #28a745;
|
|
||||||
}
|
|
||||||
.filter-option input[type="checkbox"]:checked + label.context-badge.cancelled {
|
|
||||||
background-color: #dc3545;
|
|
||||||
border-color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover state */
|
|
||||||
.filter-option input[type="checkbox"]:hover + label.context-badge {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{% endblock head %}
|
{% endblock head %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="container mt-5 position-relative">
|
<!-- ─── Modale de Filtres ──────────────────────────────────────── -->
|
||||||
<div class="position-absolute" style="top: 0px; right: 5px;">
|
<div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true">
|
||||||
{% if is_add_order_allowed == True %}
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||||
<a href="{% url 'contracts:contracts_orders_add' %}" class="btn btn-light">
|
<div class="modal-content">
|
||||||
<i class="bi bi-plus-circle"></i> {% translate "Nouvelle commande" %}
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h5 class="modal-title" id="filterModalLabel">{% translate "Filtrer les commandes et marchés" %}</h5>
|
||||||
|
<div class="text-muted small" style="font-size: 0.8rem;">{% translate "Sélectionnez les critères de recherche" %}</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form method="get" action="{% url 'contracts:contracts_index' %}" id="filter-form">
|
||||||
|
<input type="hidden" name="tab" id="filter-tab-input" value="{{ active_tab }}">
|
||||||
|
{% if search_query %}<input type="hidden" name="search" value="{{ search_query }}">{% endif %}
|
||||||
|
{% if sort_field %}<input type="hidden" name="sort" value="{{ sort_field }}">{% endif %}
|
||||||
|
{% if sort_dir %}<input type="hidden" name="dir" value="{{ sort_dir }}">{% endif %}
|
||||||
|
{% if page_size %}<input type="hidden" name="page_size" value="{{ page_size }}">{% endif %}
|
||||||
|
|
||||||
|
<!-- Statuts -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Statut" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-options">
|
||||||
|
{% for key, label in status_choices %}
|
||||||
|
<div class="filter-option">
|
||||||
|
<input type="checkbox" name="status" id="status_{{ key }}" value="{{ key }}" {% if key in selected_statuses %}checked{% endif %}>
|
||||||
|
<label for="status_{{ key }}" class="context-badge {{ key }}">{{ label }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thématiques -->
|
||||||
|
{% if available_thematics %}
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Thématiques" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-options">
|
||||||
|
{% for thematic in available_thematics %}
|
||||||
|
<div class="filter-option">
|
||||||
|
<input type="checkbox" name="thematic" id="thematic_{{ thematic.id }}" value="{{ thematic.id }}" {% if thematic.id|stringformat:"s" in selected_thematics or thematic.code in selected_thematics %}checked{% endif %}>
|
||||||
|
<label for="thematic_{{ thematic.id }}">
|
||||||
|
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{ thematic.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Marchés / Contrats -->
|
||||||
|
{% if available_contracts %}
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Marchés" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contracts-filter-container">
|
||||||
|
<div class="contracts-filter-search-box">
|
||||||
|
<i class="bi bi-search search-icon"></i>
|
||||||
|
<input type="text" id="contract-filter-search" class="form-control form-control-sm" placeholder="{% translate 'Filtrer la liste des marchés...' %}">
|
||||||
|
</div>
|
||||||
|
<div class="contracts-filter-scroll">
|
||||||
|
{% for c in available_contracts %}
|
||||||
|
<div class="contract-filter-item">
|
||||||
|
<input type="checkbox" name="contract" id="contract_{{ c.id }}" value="{{ c.id }}" class="form-check-input" {% if c.id|stringformat:"s" in selected_contracts %}checked{% endif %}>
|
||||||
|
<label for="contract_{{ c.id }}">
|
||||||
|
<span class="fw-semibold">{{ c.contract_number }}</span> – {{ c.company.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Période -->
|
||||||
|
<div class="filter-section mb-0">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Période de commande" %}</label>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="date-from" class="form-label small text-muted mb-1">{% translate "Commandes à partir du" %}</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="date-from" name="date_from" value="{{ date_from }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="date-to" class="form-label small text-muted mb-1">{% translate "jusqu'au" %}</label>
|
||||||
|
<input type="date" class="form-control form-control-sm" id="date-to" name="date_to" value="{{ date_to }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="{% url 'contracts:contracts_index' %}" class="btn btn-link text-muted p-0 text-decoration-none d-flex align-items-center gap-1" style="font-size: 0.85rem; font-weight: 500;">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> {% translate "Réinitialiser" %}
|
||||||
|
</a>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary px-3 py-2" data-bs-dismiss="modal" style="border-radius: 8px; font-weight: 500;">{% translate "Annuler" %}</button>
|
||||||
|
<button type="submit" form="filter-form" class="btn btn-sm btn-dark px-4 py-2" style="background-color: #0f172a; border-color: #0f172a; border-radius: 8px; font-weight: 600;">
|
||||||
|
<i class="bi bi-check2 me-1"></i>{% translate "Filtrer" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Contenu Principal ─────────────────────────────────────── -->
|
||||||
|
<div class="contracts-page-container">
|
||||||
|
|
||||||
|
<!-- Header avec onglets et bouton d'action -->
|
||||||
|
<div class="contracts-header">
|
||||||
|
<ul class="nav nav-tabs contracts-tabs" id="tablesTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link {% if active_tab == 'orders' %}active{% endif %}" id="orders-tab" data-bs-toggle="tab" data-bs-target="#orders-content" type="button" role="tab" aria-controls="orders-content" aria-selected="{% if active_tab == 'orders' %}true{% else %}false{% endif %}">
|
||||||
|
<i class="bi bi-receipt"></i>
|
||||||
|
{% translate "Commandes" %}
|
||||||
|
<span class="tab-counter-badge">{{ orders_paginator.count }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link {% if active_tab == 'contracts' %}active{% endif %}" id="contracts-tab" data-bs-toggle="tab" data-bs-target="#contracts-content" type="button" role="tab" aria-controls="contracts-content" aria-selected="{% if active_tab == 'contracts' %}true{% else %}false{% endif %}">
|
||||||
|
<i class="bi bi-briefcase"></i>
|
||||||
|
{% translate "Marchés/Contrats" %}
|
||||||
|
<span class="tab-counter-badge">{{ contracts_paginator.count }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if is_add_order_allowed %}
|
||||||
|
<a href="{% url 'contracts:contracts_orders_add' %}" class="btn-new-order">
|
||||||
|
<i class="bi bi-plus-circle"></i>
|
||||||
|
{% translate "Nouvelle commande" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-tabs" id="tablesTab" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
<!-- Barre d'outils : Recherche et Filtres -->
|
||||||
<button class="nav-link active" id="orders-tab" data-bs-toggle="tab" data-bs-target="#orders-content" type="button" role="tab" aria-controls="orders" aria-selected="true" style="color: black;">{% translate "Commandes" %}</button>
|
<div class="contracts-toolbar">
|
||||||
</li>
|
<div class="search-widget">
|
||||||
<li class="nav-item" role="presentation">
|
<div class="search-input-wrapper">
|
||||||
<button class="nav-link" id="contracts-tab" data-bs-toggle="tab" data-bs-target="#contracts-content" type="button" role="tab" aria-controls="contracts-table" aria-selected="false" style="color: black;">{% translate "Marchés/Contrats" %}</button>
|
<i class="bi bi-search search-icon-left"></i>
|
||||||
</li>
|
<input type="text" id="main-search-input" class="search-input" value="{{ search_query }}" placeholder="{% translate 'Rechercher une commande, un marché, une description, un prestataire...' %}">
|
||||||
</ul>
|
<button type="button" id="main-search-clear" class="search-clear-btn" title="{% translate 'Effacer la recherche' %}">
|
||||||
<div class="tab-content my-2" id="tablesTabContent">
|
<i class="bi bi-x-circle-fill"></i>
|
||||||
<div class="tab-pane fade show active" id="orders-content" role="tabpanel" aria-labelledby="orders-tab">
|
</button>
|
||||||
<!-- Filtres pour les commandes -->
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="get" id="filters-form" class="row g-3">
|
|
||||||
<!-- Statuts - Full width -->
|
|
||||||
<div class="col-12 filter-section">
|
|
||||||
<label class="form-label">{% translate "Statuts" %}</label>
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
<!-- Pending -->
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_pending" value="pending" {% if 'pending' in selected_statuses %}checked{% endif %}>
|
|
||||||
<label for="status_pending" class="context-badge pending">{% translate "En préparation" %}</label>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Sent -->
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_sent" value="sent" {% if 'sent' in selected_statuses %}checked{% endif %}>
|
|
||||||
<label for="status_sent" class="context-badge sent">{% translate "Envoyé" %}</label>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Completed -->
|
|
||||||
<div class="filter-option">
|
<div class="toolbar-actions">
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_completed" value="completed" {% if 'completed' in selected_statuses %}checked{% endif %}>
|
<button type="button" class="filter-trigger-btn" data-bs-toggle="modal" data-bs-target="#filterModal">
|
||||||
<label for="status_completed" class="context-badge completed">{% translate "Terminé" %}</label>
|
<i class="bi bi-funnel"></i>
|
||||||
</div>
|
{% translate "Filtres" %}
|
||||||
<!-- Invoiced -->
|
{% if active_filters_count > 0 %}
|
||||||
<div class="filter-option">
|
<span class="filter-count-badge">{{ active_filters_count }}</span>
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_invoiced" value="invoiced" {% if 'invoiced' in selected_statuses %}checked{% endif %}>
|
{% endif %}
|
||||||
<label for="status_invoiced" class="context-badge invoiced">{% translate "Comptabilisé" %}</label>
|
</button>
|
||||||
</div>
|
|
||||||
<!-- Paid -->
|
<div class="page-size-selector">
|
||||||
<div class="filter-option">
|
<label for="page-size-select" class="d-none d-sm-inline">{% translate "Afficher par" %}</label>
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_paid" value="paid" {% if 'paid' in selected_statuses %}checked{% endif %}>
|
<select id="page-size-select" class="page-size-select">
|
||||||
<label for="status_paid" class="context-badge paid">{% translate "Payé" %}</label>
|
<option value="25" {% if page_size == 25 %}selected{% endif %}>25</option>
|
||||||
</div>
|
<option value="50" {% if page_size == 50 %}selected{% endif %}>50</option>
|
||||||
<!-- Cancelled -->
|
<option value="100" {% if page_size == 100 %}selected{% endif %}>100</option>
|
||||||
<div class="filter-option">
|
</select>
|
||||||
<input type="checkbox" class="form-check-input" name="status" id="status_cancelled" value="cancelled" {% if 'cancelled' in selected_statuses %}checked{% endif %}>
|
|
||||||
<label for="status_cancelled" class="context-badge cancelled">{% translate "Annulé" %}</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dates - Next row -->
|
<!-- Puces des filtres actifs -->
|
||||||
<div class="col-md-6">
|
{% if active_filters_list %}
|
||||||
<label for="date-from" class="form-label">{% translate "Commandes à partir du" %}</label>
|
<div class="active-filters-bar">
|
||||||
<input type="date" class="form-control" id="date-from" name="date_from" value="{{ request.GET.date_from }}">
|
<span class="active-filter-label">{% translate "Filtres actifs" %}:</span>
|
||||||
</div>
|
{% for filter in active_filters_list %}
|
||||||
<div class="col-md-6">
|
<span class="active-filter-chip">
|
||||||
<label for="date-to" class="form-label">{% translate "jusqu'au" %}</label>
|
{% if filter.is_status %}
|
||||||
<input type="date" class="form-control" id="date-to" name="date_to" value="{{ request.GET.date_to }}">
|
<span class="badge rounded-pill status-badge {{ filter.status_class }}" style="padding: 2px 6px; font-size: 0.72rem;">{{ filter.label }}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ filter.label }}
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="remove-filter-btn" data-param="{{ filter.param }}" data-val="{{ filter.val }}" title="{% translate 'Supprimer ce critère' %}">
|
||||||
|
<i class="bi bi-x-circle-fill"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{% url 'contracts:contracts_index' %}" class="btn-clear-all-filters">
|
||||||
|
<i class="bi bi-x me-1"></i>{% translate "Tout effacer" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Submit buttons -->
|
<!-- Contenu des onglets -->
|
||||||
<div class="col-12 d-flex align-items-end gap-2">
|
<div class="tab-content" id="tablesTabContent">
|
||||||
<button type="submit" class="btn btn-primary">{% translate "Filtrer" %}</button>
|
|
||||||
<a href="{% url 'contracts:contracts_index' %}" class="btn btn-secondary">{% translate "Réinitialiser" %}</a>
|
<!-- ─── Onglet Commandes ──────────────────────────────────── -->
|
||||||
</div>
|
<div class="tab-pane fade {% if active_tab == 'orders' %}show active{% endif %}" id="orders-content" role="tabpanel" aria-labelledby="orders-tab">
|
||||||
</form>
|
<div class="contracts-table-card">
|
||||||
</div>
|
<div class="table-responsive">
|
||||||
</div>
|
<table class="contracts-table">
|
||||||
<div id="orders-container\">
|
|
||||||
<table id="orders-table" class="table"
|
|
||||||
data-height="800"
|
|
||||||
data-pagination="true"
|
|
||||||
data-page-size="50"
|
|
||||||
data-click-to-select="true"
|
|
||||||
data-search="true" >
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-field="actions" data-width="100"></th>
|
<th style="width: 48px;"></th>
|
||||||
<th data-sortable="true">{% translate "Numéro de commande" %}</th>
|
<th class="sortable">
|
||||||
<th>{% translate "Description" %}</th>
|
<a href="{{ sort_urls.code }}" class="sort-link">
|
||||||
<th>{% translate "Date" %}</th>
|
<span>{% translate "Numéro de commande" %}</span>
|
||||||
<th>{% translate "Statut" %}</th>
|
<i class="bi {% if sort_field == 'code' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %}"></i>
|
||||||
<th>{% translate "Fichier" %}</th>
|
</a>
|
||||||
<th data-sortable="true">{% translate "Marché" %}</th>
|
</th>
|
||||||
|
<th class="sortable">
|
||||||
|
<a href="{{ sort_urls.description }}" class="sort-link">
|
||||||
|
<span>{% translate "Description" %}</span>
|
||||||
|
<i class="bi {% if sort_field == 'description' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th class="sortable">
|
||||||
|
<a href="{{ sort_urls.date }}" class="sort-link">
|
||||||
|
<span>{% translate "Date" %}</span>
|
||||||
|
<i class="bi {% if sort_field == 'date' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th class="sortable">
|
||||||
|
<a href="{{ sort_urls.status }}" class="sort-link">
|
||||||
|
<span>{% translate "Statut" %}</span>
|
||||||
|
<i class="bi {% if sort_field == 'status' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
{% if can_view_prices %}
|
||||||
|
<th class="sortable text-end">
|
||||||
|
<a href="{{ sort_urls.amount }}" class="sort-link justify-content-end">
|
||||||
|
<span>{% translate "Montant" %}</span>
|
||||||
|
<i class="bi {% if sort_field == 'amount' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %} ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
<th style="width: 60px; text-align: center;">{% translate "Fichier" %}</th>
|
||||||
|
<th class="sortable">
|
||||||
|
<a href="{{ sort_urls.contract }}" class="sort-link">
|
||||||
|
<span>{% translate "Marché" %}</span>
|
||||||
|
<i class="bi {% if sort_field == 'contract' %}{% if sort_dir == 'asc' %}bi-sort-up text-primary{% else %}bi-sort-down text-primary{% endif %}{% else %}bi-arrow-down-up text-muted opacity-50{% endif %}"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for order in orders %}
|
{% for order in orders_page %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="text-center">
|
||||||
<a href="/contracts/orders/{{ order.id }}" type="button" class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="Detail">
|
<a href="/contracts/orders/{{ order.id }}" class="table-action-btn" title="{% translate 'Détail de la commande' %}">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ order.order_code }}</td>
|
|
||||||
<td title="{{ order.description }}">{{ order.description|truncatechars:100 }}</td>
|
|
||||||
<td>{{ order.order_date }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge rounded-pill status-badge {{ order.order_status }}">
|
<a href="/contracts/orders/{{ order.id }}" class="table-code-link">
|
||||||
|
{{ order.order_code }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span title="{{ order.description }}">{{ order.description|default:"-"|truncatechars:90 }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">{{ order.order_date|date:"d/m/Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge {{ order.order_status }}">
|
||||||
{{ order.get_order_status_display }}
|
{{ order.get_order_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{% if order.order_file %}<a href="{{ order.order_file.url }}"><i class="bi bi-file-pdf"></i></a>{% endif %}</td>
|
{% if can_view_prices %}
|
||||||
<td>{{ order.contract }}</td>
|
<td class="text-end text-nowrap fw-semibold">
|
||||||
|
{% if order.total_amount %}
|
||||||
|
{{ order.total_amount }} €
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted font-monospace">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="text-center">
|
||||||
|
{% if order.order_file %}
|
||||||
|
<a href="{{ order.order_file.url }}" target="_blank" class="file-download-btn" title="{% translate 'Télécharger le bon de commande' %}">
|
||||||
|
<i class="bi bi-file-earmark-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted opacity-50">–</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/contracts/{{ order.contract.id }}" class="text-decoration-none text-dark fw-medium">
|
||||||
|
{{ order.contract.contract_number }}
|
||||||
|
</a>
|
||||||
|
<span class="text-muted small d-block">{{ order.contract.company.name }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="{% if can_view_prices %}8{% else %}7{% endif %}">
|
||||||
|
<div class="table-empty-state">
|
||||||
|
<div class="empty-icon"><i class="bi bi-inbox"></i></div>
|
||||||
|
<div class="empty-title">{% translate "Aucune commande trouvée" %}</div>
|
||||||
|
<div class="empty-desc">{% translate "Aucune commande ne correspond à vos critères de recherche actuels." %}</div>
|
||||||
|
<a href="{% url 'contracts:contracts_index' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Réinitialiser les filtres" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de pagination Commandes -->
|
||||||
|
{% if orders_paginator.count > 0 %}
|
||||||
|
<div class="contracts-pagination-bar">
|
||||||
|
<div class="pagination-info">
|
||||||
|
{% blocktranslate with start=orders_page.start_index end=orders_page.end_index total=orders_paginator.count %}
|
||||||
|
Affichage de <strong>{{ start }}</strong> à <strong>{{ end }}</strong> sur <strong>{{ total }}</strong> commande(s)
|
||||||
|
{% endblocktranslate %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade show" id="contracts-content" role="tabpanel" aria-labelledby="contracts-tab">
|
|
||||||
<div id="contracts-container">
|
{% if orders_page.has_other_pages %}
|
||||||
<table id="contracts-table" class="table"
|
<nav class="pagination-nav" aria-label="{% translate 'Pagination des commandes' %}">
|
||||||
data-height="500"
|
<ul class="pagination pagination-sm">
|
||||||
data-pagination="true"
|
{% if orders_page.has_previous %}
|
||||||
data-page-size="25"
|
<li class="page-item">
|
||||||
data-click-to-select="true"
|
<a class="page-link" href="?{{ orders_querystring }}&page={{ orders_page.previous_page_number }}" aria-label="{% translate 'Précédent' %}">
|
||||||
data-search="true">
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_num in orders_elided_range %}
|
||||||
|
{% if page_num == orders_paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item {% if page_num == orders_page.number %}active{% endif %}">
|
||||||
|
<a class="page-link" href="?{{ orders_querystring }}&page={{ page_num }}">{{ page_num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if orders_page.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{{ orders_querystring }}&page={{ orders_page.next_page_number }}" aria-label="{% translate 'Suivant' %}">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Onglet Marchés / Contrats ────────────────────────── -->
|
||||||
|
<div class="tab-pane fade {% if active_tab == 'contracts' %}show active{% endif %}" id="contracts-content" role="tabpanel" aria-labelledby="contracts-tab">
|
||||||
|
<div class="contracts-table-card">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="contracts-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-field="actions" data-width="100"></th>
|
<th style="width: 48px;"></th>
|
||||||
<th>{% translate "Numéro" %}</th>
|
<th>{% translate "Numéro" %}</th>
|
||||||
<th>{% translate "Description" %}</th>
|
<th>{% translate "Description" %}</th>
|
||||||
|
<th>{% translate "Thématiques" %}</th>
|
||||||
<th>{% translate "Prestataire" %}</th>
|
<th>{% translate "Prestataire" %}</th>
|
||||||
<th>{% translate "Début" %}</th>
|
<th>{% translate "Début" %}</th>
|
||||||
<th>{% translate "Fin" %}</th>
|
<th>{% translate "Fin" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for contract in contracts %}
|
{% for contract in contracts_page %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="text-center">
|
||||||
<a href="/contracts/{{ contract.id }}" type="button" class="btn btn-sm btn-light detail-btn" data-id="${row.id}" data-bs-toggle="tooltip" title="Detail">
|
<a href="/contracts/{{ contract.id }}" class="table-action-btn" title="{% translate 'Détail du contrat' %}">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ contract.contract_number }}</td>
|
<td>
|
||||||
<td title="{{ contract.description }}">{{ contract.description|truncatechars:100 }}</td>
|
<a href="/contracts/{{ contract.id }}" class="table-code-link">
|
||||||
<td>{{ contract.company.name }}</td>
|
{{ contract.contract_number }}
|
||||||
<td>{{ contract.start_date }}</td>
|
</a>
|
||||||
<td>{{ contract.end_date }}</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span title="{{ contract.description }}">{{ contract.description|default:"-"|truncatechars:90 }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
{% for thematic in contract.thematics.all %}
|
||||||
|
<span class="badge rounded-pill bg-light text-dark border" style="font-size: 0.72rem; font-weight: 500;">
|
||||||
|
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{ thematic.name }}
|
||||||
|
</span>
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-muted small">–</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="fw-medium">{{ contract.company.name }}</td>
|
||||||
|
<td class="text-nowrap">{{ contract.start_date|date:"d/m/Y" }}</td>
|
||||||
|
<td class="text-nowrap">{{ contract.end_date|date:"d/m/Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7">
|
||||||
|
<div class="table-empty-state">
|
||||||
|
<div class="empty-icon"><i class="bi bi-inbox"></i></div>
|
||||||
|
<div class="empty-title">{% translate "Aucun marché trouvé" %}</div>
|
||||||
|
<div class="empty-desc">{% translate "Aucun marché ne correspond à vos critères de recherche actuels." %}</div>
|
||||||
|
<a href="{% url 'contracts:contracts_index' %}?tab=contracts" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Réinitialiser les filtres" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de pagination Marchés -->
|
||||||
|
{% if contracts_paginator.count > 0 %}
|
||||||
|
<div class="contracts-pagination-bar">
|
||||||
|
<div class="pagination-info">
|
||||||
|
{% blocktranslate with start=contracts_page.start_index end=contracts_page.end_index total=contracts_paginator.count %}
|
||||||
|
Affichage de <strong>{{ start }}</strong> à <strong>{{ end }}</strong> sur <strong>{{ total }}</strong> marché(s)
|
||||||
|
{% endblocktranslate %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if contracts_page.has_other_pages %}
|
||||||
|
<nav class="pagination-nav" aria-label="{% translate 'Pagination des marchés' %}">
|
||||||
|
<ul class="pagination pagination-sm">
|
||||||
|
{% if contracts_page.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{{ contracts_querystring }}&contracts_page={{ contracts_page.previous_page_number }}" aria-label="{% translate 'Précédent' %}">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_num in contracts_elided_range %}
|
||||||
|
{% if page_num == contracts_paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item {% if page_num == contracts_page.number %}active{% endif %}">
|
||||||
|
<a class="page-link" href="?{{ contracts_querystring }}&contracts_page={{ page_num }}">{{ page_num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if contracts_page.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{{ contracts_querystring }}&contracts_page={{ contracts_page.next_page_number }}" aria-label="{% translate 'Suivant' %}">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock content%}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
<script src="{% static 'contracts/js/contracts_index.js' %}"></script>
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
// Gestion des badges de statut
|
|
||||||
$('#status-badges label').on('click', function(e) {
|
|
||||||
// Éviter de double-trigger si on clique sur le checkbox
|
|
||||||
if (e.target.tagName === 'INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$(this).find('input[type="checkbox"]').prop('checked', function(i, val) {
|
|
||||||
return !val;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#orders-table').bootstrapTable({
|
|
||||||
formatNoMatches: function() {
|
|
||||||
return '{% translate "Aucune commande trouvée" %}';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#contracts-table').bootstrapTable({
|
|
||||||
formatNoMatches: function() {
|
|
||||||
return '{% translate "Aucun marché trouvé" %}';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
@ -802,6 +802,121 @@ class GlobalPostsAndPricesPermissionsTests(TestCase):
|
||||||
self.assertEqual(data['posts'][0]['unit_price'], '10.50')
|
self.assertEqual(data['posts'][0]['unit_price'], '10.50')
|
||||||
|
|
||||||
|
|
||||||
|
class ContractsIndexViewTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from decimal import Decimal
|
||||||
|
from common.models import Thematic, Role, UserConfig
|
||||||
|
from contracts.models import ContractOrder
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
self.user = User.objects.create_superuser(username="admin_contracts", password="password123")
|
||||||
|
self.config = UserConfig.objects.create(user=self.user)
|
||||||
|
role_admin = Role.objects.create(name="admin")
|
||||||
|
self.config.roles.add(role_admin)
|
||||||
|
|
||||||
|
self.company = Company.objects.create(name="Acme Infra")
|
||||||
|
self.thematic_green = Thematic.objects.create(code="green_test", name_fr="Espaces Verts", name_nl="Groene Ruimten")
|
||||||
|
self.thematic_roads = Thematic.objects.create(code="roads_test", name_fr="Voiries", name_nl="Wegen")
|
||||||
|
|
||||||
|
today = date(2026, 1, 15)
|
||||||
|
self.contract_green = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number="CTR-VERT-001",
|
||||||
|
start_date=today,
|
||||||
|
end_date=today + timedelta(days=365),
|
||||||
|
description="Entretien des espaces verts"
|
||||||
|
)
|
||||||
|
self.contract_green.thematics.add(self.thematic_green)
|
||||||
|
|
||||||
|
self.contract_roads = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number="CTR-VOIRIE-002",
|
||||||
|
start_date=today,
|
||||||
|
end_date=today + timedelta(days=365),
|
||||||
|
description="Réparation voiries"
|
||||||
|
)
|
||||||
|
self.contract_roads.thematics.add(self.thematic_roads)
|
||||||
|
|
||||||
|
# Créer 60 commandes pour tester la pagination et les filtres
|
||||||
|
orders_to_create = []
|
||||||
|
for i in range(1, 61):
|
||||||
|
target_contract = self.contract_green if i <= 40 else self.contract_roads
|
||||||
|
st = 'cancelled' if i == 1 else ('completed' if i % 2 == 0 else 'pending')
|
||||||
|
orders_to_create.append(ContractOrder(
|
||||||
|
contract=target_contract,
|
||||||
|
order_code=f"CMD-{i:03d}",
|
||||||
|
description=f"Commande spéciale numéro {i}" if i == 5 else f"Travaux réguliers {i}",
|
||||||
|
order_date=today + timedelta(days=i),
|
||||||
|
delivery_date=today + timedelta(days=i + 10),
|
||||||
|
order_status=st,
|
||||||
|
total_amount=Decimal(f"{100 + i}.00")
|
||||||
|
))
|
||||||
|
ContractOrder.objects.bulk_create(orders_to_create)
|
||||||
|
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_contracts_index_renders_and_paginates(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTemplateUsed(resp, 'contracts/contracts_index.html')
|
||||||
|
|
||||||
|
# Par défaut, page 1 avec 50 commandes sur les 59 éligibles (1 annulée exclue par défaut)
|
||||||
|
orders_page = resp.context['orders_page']
|
||||||
|
self.assertEqual(len(orders_page), 50)
|
||||||
|
self.assertTrue(orders_page.has_next())
|
||||||
|
self.assertEqual(resp.context['orders_paginator'].count, 59)
|
||||||
|
|
||||||
|
# Page 2
|
||||||
|
resp_p2 = self.client.get(url, {'page': 2})
|
||||||
|
self.assertEqual(resp_p2.status_code, 200)
|
||||||
|
orders_page_2 = resp_p2.context['orders_page']
|
||||||
|
self.assertEqual(len(orders_page_2), 9)
|
||||||
|
|
||||||
|
def test_contracts_index_filter_status(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'status': ['cancelled']})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
orders_page = resp.context['orders_page']
|
||||||
|
self.assertEqual(len(orders_page), 1)
|
||||||
|
self.assertEqual(orders_page[0].order_code, "CMD-001")
|
||||||
|
|
||||||
|
def test_contracts_index_filter_thematics(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'thematic': [str(self.thematic_roads.id)]})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
# Il y a 20 commandes roads (41 à 60), toutes non annulées
|
||||||
|
self.assertEqual(resp.context['orders_paginator'].count, 20)
|
||||||
|
|
||||||
|
def test_contracts_index_filter_contract(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'contract': [str(self.contract_roads.id)]})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(resp.context['orders_paginator'].count, 20)
|
||||||
|
|
||||||
|
def test_contracts_index_search(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'search': 'spéciale'})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(resp.context['orders_paginator'].count, 1)
|
||||||
|
self.assertEqual(resp.context['orders_page'][0].order_code, 'CMD-005')
|
||||||
|
|
||||||
|
def test_contracts_index_custom_page_size(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'page_size': 25})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(len(resp.context['orders_page']), 25)
|
||||||
|
|
||||||
|
def test_contracts_index_sorting(self):
|
||||||
|
url = reverse('contracts:contracts_index')
|
||||||
|
resp = self.client.get(url, {'sort': 'code', 'dir': 'asc'})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
# Premier élément doit être CMD-002 car CMD-001 est cancelled
|
||||||
|
self.assertEqual(resp.context['orders_page'][0].order_code, 'CMD-002')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.views.decorators.http import require_GET, require_POST
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
from django.http import JsonResponse, FileResponse, Http404
|
from django.http import JsonResponse, FileResponse, Http404
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
|
@ -27,6 +28,7 @@ from contracts.models import (
|
||||||
ContractOrderAttachment,
|
ContractOrderAttachment,
|
||||||
DirectOrderPost,
|
DirectOrderPost,
|
||||||
DIRECT_ORDER_POST_STATUS_CHOICES,
|
DIRECT_ORDER_POST_STATUS_CHOICES,
|
||||||
|
CONTRACT_ORDER_STATUS_CHOICES,
|
||||||
)
|
)
|
||||||
from contracts.forms import ContractOrderForm, ContractOrderQuoteQuantityForm, ClaimDeclarationAttachmentForm, ContractOrderAttachmentForm
|
from contracts.forms import ContractOrderForm, ContractOrderQuoteQuantityForm, ClaimDeclarationAttachmentForm, ContractOrderAttachmentForm
|
||||||
from contracts.permissions import get_allowed_contracts_for_user, contract_view_required, order_view_required, user_can_generate_order_documents
|
from contracts.permissions import get_allowed_contracts_for_user, contract_view_required, order_view_required, user_can_generate_order_documents
|
||||||
|
|
@ -39,39 +41,251 @@ from contracts.views_pdf_reports import (
|
||||||
generate_documents_for_order,
|
generate_documents_for_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
from common.models import UserConfig, UserThematics
|
from common.models import UserConfig, UserThematics, Thematic
|
||||||
from common.private_files.helpers import build_private_url
|
from common.private_files.helpers import build_private_url
|
||||||
from interventions.permissions import can_add_intervention
|
from interventions.permissions import can_add_intervention
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
def contracts_index(request):
|
def contracts_index(request):
|
||||||
contracts = get_allowed_contracts_for_user(request.user)
|
contracts = get_allowed_contracts_for_user(request.user)
|
||||||
|
contracts = contracts.select_related('company').prefetch_related('thematics').defer('geom')
|
||||||
|
|
||||||
|
active_tab = request.GET.get('tab', 'orders')
|
||||||
|
if active_tab not in ('orders', 'contracts'):
|
||||||
|
active_tab = 'orders'
|
||||||
|
|
||||||
|
# Choix disponibles pour les filtres
|
||||||
|
available_thematics = Thematic.objects.filter(contracts__in=contracts).distinct().order_by('name_fr')
|
||||||
|
available_contracts = contracts.order_by('contract_number')
|
||||||
|
|
||||||
|
# Recherche textuelle
|
||||||
|
search_query = request.GET.get('search', '').strip()
|
||||||
|
|
||||||
# Filtrage par statut(s)
|
# Filtrage par statut(s)
|
||||||
|
has_status_param = 'status' in request.GET
|
||||||
status_filters = request.GET.getlist('status')
|
status_filters = request.GET.getlist('status')
|
||||||
if status_filters == []:
|
if not has_status_param:
|
||||||
# Par défaut, afficher les commandes en 'pending' et 'sent'
|
# Par défaut, afficher les commandes en 'pending', 'sent', 'completed' et 'invoiced'
|
||||||
status_filters = ['pending', 'sent', 'completed', 'invoiced']
|
status_filters = ['pending', 'sent', 'completed', 'invoiced']
|
||||||
|
|
||||||
orders = orders = ContractOrder.objects.filter(contract__in=contracts, order_status__in=status_filters)
|
# Filtrage par thématique(s)
|
||||||
|
selected_thematics = request.GET.getlist('thematic')
|
||||||
|
|
||||||
|
# Filtrage par marché(s)
|
||||||
|
selected_contracts = request.GET.getlist('contract')
|
||||||
|
|
||||||
# Filtrage par date
|
# Filtrage par date
|
||||||
date_from = request.GET.get('date_from', '')
|
date_from = request.GET.get('date_from', '').strip()
|
||||||
|
date_to = request.GET.get('date_to', '').strip()
|
||||||
|
|
||||||
|
# Taille de page
|
||||||
|
try:
|
||||||
|
page_size = int(request.GET.get('page_size', 50))
|
||||||
|
if page_size not in (25, 50, 100):
|
||||||
|
page_size = 50
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page_size = 50
|
||||||
|
|
||||||
|
# Tri des commandes
|
||||||
|
sort_field = request.GET.get('sort', 'date')
|
||||||
|
sort_dir = request.GET.get('dir', 'desc')
|
||||||
|
if sort_dir not in ('asc', 'desc'):
|
||||||
|
sort_dir = 'desc'
|
||||||
|
|
||||||
|
sort_mapping = {
|
||||||
|
'code': 'order_code',
|
||||||
|
'description': 'description',
|
||||||
|
'date': 'order_date',
|
||||||
|
'status': 'order_status',
|
||||||
|
'contract': 'contract__contract_number',
|
||||||
|
'company': 'contract__company__name',
|
||||||
|
'amount': 'total_amount',
|
||||||
|
}
|
||||||
|
db_sort_col = sort_mapping.get(sort_field, 'order_date')
|
||||||
|
order_by_arg = db_sort_col if sort_dir == 'asc' else f"-{db_sort_col}"
|
||||||
|
|
||||||
|
# Construction de la requête commandes optimisée
|
||||||
|
orders = ContractOrder.objects.filter(contract__in=contracts)
|
||||||
|
|
||||||
|
if status_filters:
|
||||||
|
orders = orders.filter(order_status__in=status_filters)
|
||||||
|
|
||||||
|
if selected_thematics:
|
||||||
|
orders = orders.filter(
|
||||||
|
Q(contract__thematics__id__in=selected_thematics) |
|
||||||
|
Q(contract__thematics__code__in=selected_thematics)
|
||||||
|
)
|
||||||
|
|
||||||
|
if selected_contracts:
|
||||||
|
orders = orders.filter(contract_id__in=selected_contracts)
|
||||||
|
|
||||||
if date_from:
|
if date_from:
|
||||||
orders = orders.filter(order_date__gte=date_from)
|
orders = orders.filter(order_date__gte=date_from)
|
||||||
|
|
||||||
date_to = request.GET.get('date_to', '')
|
|
||||||
if date_to:
|
if date_to:
|
||||||
orders = orders.filter(order_date__lte=date_to)
|
orders = orders.filter(order_date__lte=date_to)
|
||||||
|
|
||||||
# Tri par date décroissante
|
if search_query:
|
||||||
orders = orders.order_by('-order_date')
|
orders = orders.filter(
|
||||||
|
Q(order_code__icontains=search_query) |
|
||||||
|
Q(description__icontains=search_query) |
|
||||||
|
Q(contract__contract_number__icontains=search_query) |
|
||||||
|
Q(contract__description__icontains=search_query) |
|
||||||
|
Q(contract__company__name__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = orders.distinct().select_related(
|
||||||
|
'contract',
|
||||||
|
'contract__company',
|
||||||
|
'ordered_by'
|
||||||
|
).defer('contract__geom').order_by(order_by_arg, '-id')
|
||||||
|
|
||||||
|
# Pagination des commandes
|
||||||
|
orders_paginator = Paginator(orders, page_size)
|
||||||
|
orders_page_num = request.GET.get('page', 1)
|
||||||
|
orders_page = orders_paginator.get_page(orders_page_num)
|
||||||
|
orders_elided_range = orders_paginator.get_elided_page_range(orders_page.number, on_each_side=2, on_ends=1)
|
||||||
|
|
||||||
|
# Construction de la requête marchés/contrats
|
||||||
|
contracts_qs = contracts
|
||||||
|
if search_query:
|
||||||
|
contracts_qs = contracts_qs.filter(
|
||||||
|
Q(contract_number__icontains=search_query) |
|
||||||
|
Q(description__icontains=search_query) |
|
||||||
|
Q(company__name__icontains=search_query)
|
||||||
|
)
|
||||||
|
if selected_thematics:
|
||||||
|
contracts_qs = contracts_qs.filter(
|
||||||
|
Q(thematics__id__in=selected_thematics) |
|
||||||
|
Q(thematics__code__in=selected_thematics)
|
||||||
|
)
|
||||||
|
contracts_qs = contracts_qs.distinct().order_by('-start_date')
|
||||||
|
|
||||||
|
contracts_paginator = Paginator(contracts_qs, 25)
|
||||||
|
contracts_page_num = request.GET.get('contracts_page', 1)
|
||||||
|
contracts_page = contracts_paginator.get_page(contracts_page_num)
|
||||||
|
contracts_elided_range = contracts_paginator.get_elided_page_range(contracts_page.number, on_each_side=2, on_ends=1)
|
||||||
|
|
||||||
|
# Liste des filtres actifs pour affichage sous forme de badges
|
||||||
|
active_filters_list = []
|
||||||
|
if search_query:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'search',
|
||||||
|
'label': f'"{search_query}"',
|
||||||
|
'param': 'search',
|
||||||
|
'val': search_query,
|
||||||
|
})
|
||||||
|
if has_status_param:
|
||||||
|
status_dict = dict(CONTRACT_ORDER_STATUS_CHOICES)
|
||||||
|
for s in status_filters:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'status',
|
||||||
|
'label': str(status_dict.get(s, s)),
|
||||||
|
'param': 'status',
|
||||||
|
'val': s,
|
||||||
|
'status_class': s,
|
||||||
|
'is_status': True,
|
||||||
|
})
|
||||||
|
if selected_thematics:
|
||||||
|
thematic_names = {str(t.id): t.name for t in available_thematics}
|
||||||
|
thematic_names.update({t.code: t.name for t in available_thematics})
|
||||||
|
for th in selected_thematics:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'thematic',
|
||||||
|
'label': thematic_names.get(str(th), str(th)),
|
||||||
|
'param': 'thematic',
|
||||||
|
'val': th,
|
||||||
|
})
|
||||||
|
if selected_contracts:
|
||||||
|
contract_names = {str(c.id): f"{c.contract_number} ({c.company.name})" for c in available_contracts}
|
||||||
|
for cid in selected_contracts:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'contract',
|
||||||
|
'label': contract_names.get(str(cid), str(cid)),
|
||||||
|
'param': 'contract',
|
||||||
|
'val': cid,
|
||||||
|
})
|
||||||
|
if date_from:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'date_from',
|
||||||
|
'label': f"≥ {date_from}",
|
||||||
|
'param': 'date_from',
|
||||||
|
'val': date_from,
|
||||||
|
})
|
||||||
|
if date_to:
|
||||||
|
active_filters_list.append({
|
||||||
|
'type': 'date_to',
|
||||||
|
'label': f"≤ {date_to}",
|
||||||
|
'param': 'date_to',
|
||||||
|
'val': date_to,
|
||||||
|
})
|
||||||
|
|
||||||
|
active_filters_count = len(active_filters_list)
|
||||||
|
|
||||||
|
# Query strings pour les liens de pagination (en préservant les autres paramètres)
|
||||||
|
p_orders = request.GET.copy()
|
||||||
|
if 'page' in p_orders:
|
||||||
|
del p_orders['page']
|
||||||
|
orders_querystring = p_orders.urlencode()
|
||||||
|
|
||||||
|
p_contracts = request.GET.copy()
|
||||||
|
if 'contracts_page' in p_contracts:
|
||||||
|
del p_contracts['contracts_page']
|
||||||
|
contracts_querystring = p_contracts.urlencode()
|
||||||
|
|
||||||
|
# Générateur d'URLs de tri
|
||||||
|
def build_sort_url(col):
|
||||||
|
next_dir = 'asc' if (sort_field == col and sort_dir == 'desc') else 'desc'
|
||||||
|
p = request.GET.copy()
|
||||||
|
p['sort'] = col
|
||||||
|
p['dir'] = next_dir
|
||||||
|
if 'page' in p:
|
||||||
|
del p['page']
|
||||||
|
return f"?{p.urlencode()}"
|
||||||
|
|
||||||
|
sort_urls = {col: build_sort_url(col) for col in ('code', 'description', 'date', 'status', 'contract', 'amount')}
|
||||||
|
|
||||||
|
# Vérification des droits pour voir les prix
|
||||||
|
can_view_prices = False
|
||||||
|
if request.user.is_superuser:
|
||||||
|
can_view_prices = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cfg = request.user.config
|
||||||
|
can_view_prices = getattr(cfg, 'can_view_all_prices', False) or bool(
|
||||||
|
set(cfg.roles.values_list('name', flat=True)).intersection({'admin', 'top_manager'})
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'contracts': contracts,
|
'contracts': contracts,
|
||||||
'orders': orders,
|
'orders_page': orders_page,
|
||||||
|
'orders_elided_range': orders_elided_range,
|
||||||
|
'orders_paginator': orders_paginator,
|
||||||
|
'contracts_page': contracts_page,
|
||||||
|
'contracts_elided_range': contracts_elided_range,
|
||||||
|
'contracts_paginator': contracts_paginator,
|
||||||
|
'available_thematics': available_thematics,
|
||||||
|
'available_contracts': available_contracts,
|
||||||
|
'status_choices': CONTRACT_ORDER_STATUS_CHOICES,
|
||||||
|
'selected_statuses': status_filters,
|
||||||
|
'selected_thematics': selected_thematics,
|
||||||
|
'selected_contracts': selected_contracts,
|
||||||
|
'search_query': search_query,
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'page_size': page_size,
|
||||||
|
'sort_field': sort_field,
|
||||||
|
'sort_dir': sort_dir,
|
||||||
|
'sort_urls': sort_urls,
|
||||||
|
'active_tab': active_tab,
|
||||||
|
'active_filters_list': active_filters_list,
|
||||||
|
'active_filters_count': active_filters_count,
|
||||||
|
'orders_querystring': orders_querystring,
|
||||||
|
'contracts_querystring': contracts_querystring,
|
||||||
'is_add_order_allowed': user_can_add_order(request.user),
|
'is_add_order_allowed': user_can_add_order(request.user),
|
||||||
'selected_statuses': status_filters, # Passer les statuts sélectionnés au template
|
'can_view_prices': can_view_prices,
|
||||||
}
|
}
|
||||||
return render(request, "contracts/contracts_index.html", context)
|
return render(request, "contracts/contracts_index.html", context)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue