From d993a545d93d90c1e109dcf02715f3f4ed5f3398 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Tue, 3 Feb 2026 14:53:11 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(Product):=20Ajoute=20la?= =?UTF-8?q?=20publication=20des=20produits=20et=20les=20p=C3=A9riodes=20bl?= =?UTF-8?q?oqu=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute la possibilité de publier ou masquer un produit. Permet de bloquer des périodes pour un produit. Corrige des bugs liés à la suppression des produits du panier. Mise à jour de l'affichage du calendrier pour les blocages. ``` --- assets/libs/PlaningLogestics.js | 60 ++++- assets/tools/FlowReserve.js | 29 +++ migrations/Version20260203131644.php | 244 ++++++++++++++++++ migrations/Version20260203133249.php | 31 +++ .../Dashboard/ProductController.php | 67 ++++- .../Dashboard/ReservationController.php | 23 +- src/Controller/ReserverController.php | 38 ++- src/Entity/Options.php | 15 ++ src/Entity/Product.php | 53 ++++ src/Entity/ProductBlocked.php | 82 ++++++ src/Form/ProductBlockedType.php | 40 +++ src/Repository/ProductBlockedRepository.php | 75 ++++++ src/Security/SiteMapListener.php | 4 +- templates/dashboard/products/add.twig | 114 +++++++- 14 files changed, 850 insertions(+), 25 deletions(-) create mode 100644 migrations/Version20260203131644.php create mode 100644 migrations/Version20260203133249.php create mode 100644 src/Entity/ProductBlocked.php create mode 100644 src/Form/ProductBlockedType.php create mode 100644 src/Repository/ProductBlockedRepository.php diff --git a/assets/libs/PlaningLogestics.js b/assets/libs/PlaningLogestics.js index 7faf5e0..a080f64 100644 --- a/assets/libs/PlaningLogestics.js +++ b/assets/libs/PlaningLogestics.js @@ -194,10 +194,19 @@ export default class PlaningLogistics extends HTMLElement { fetch(`/crm/reservation/data?${params.toString()}`) .then(response => response.json()) .then(data => { - const formattedData = data.map(item => ({ - ...item, - backgroundColor: item.extendedProps.isSigned ? '#059669' : '#2563eb' - })); + const formattedData = data.map(item => { + let bgColor = '#2563eb'; + if (item.extendedProps.type === 'blocked') { + bgColor = '#ef4444'; + } else if (item.extendedProps.isSigned) { + bgColor = '#059669'; + } + return { + ...item, + backgroundColor: bgColor, + borderColor: bgColor + }; + }); successCallback(formattedData); this.updateStats(formattedData); loader.classList.add('hidden'); @@ -208,7 +217,22 @@ export default class PlaningLogistics extends HTMLElement { }); }, eventContent: (arg) => { - const { caution, acompte, solde, isSigned } = arg.event.extendedProps; + const props = arg.event.extendedProps; + + if (props.type === 'blocked') { + return { + html: ` +
+
+ ⛔ ${arg.event.title.replace('BLOCAGE: ', '')} +
+
${props.reason || 'Pas de raison'}
+
+ ` + }; + } + + const { caution, acompte, solde, isSigned } = props; return { html: `
@@ -243,6 +267,32 @@ export default class PlaningLogistics extends HTMLElement { const modal = this.querySelector('#calendar-modal'); const props = event.extendedProps; const modalStatus = this.querySelector('#modal-status-container'); + + // Reset visibility + this.querySelector('#link-phone').style.display = 'flex'; + this.querySelector('#link-email').style.display = 'flex'; + this.querySelector('#modal-link-contrat').parentElement.style.display = 'flex'; + this.querySelector('.bg-slate-900\\/50.rounded-2xl.border').style.display = 'block'; // Address container + + if (props.type === 'blocked') { + this.querySelector('#modal-contract-number').innerText = 'INDISPONIBILITÉ'; + this.querySelector('#modal-title').innerText = props.productName; + this.querySelector('#modal-client').innerText = props.reason || 'Aucune raison spécifiée'; + this.querySelector('#modal-email').innerText = '-'; + this.querySelector('#modal-phone').innerText = '-'; + this.querySelector('#modal-start').innerText = props.start; + this.querySelector('#modal-end').innerText = props.end; + + // Hide irrelevant sections + this.querySelector('#link-phone').style.display = 'none'; + this.querySelector('#link-email').style.display = 'none'; + this.querySelector('#modal-link-contrat').parentElement.style.display = 'none'; + this.querySelector('.bg-slate-900\\/50.rounded-2xl.border').style.display = 'none'; + modalStatus.innerHTML = 'Blocage Matériel'; + + modal.classList.replace('hidden', 'flex'); + return; + } this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`; this.querySelector('#modal-title').innerText = event.title; diff --git a/assets/tools/FlowReserve.js b/assets/tools/FlowReserve.js index 4e479c7..b610170 100644 --- a/assets/tools/FlowReserve.js +++ b/assets/tools/FlowReserve.js @@ -128,6 +128,21 @@ export class FlowReserve extends HTMLAnchorElement { const data = await response.json(); this.productsAvailable = data.available; + if (data.unavailable_products_ids && data.unavailable_products_ids.length > 0) { + let currentList = this.getList(); + const initialLength = currentList.length; + // Filter out unavailable items + currentList = currentList.filter(id => !data.unavailable_products_ids.includes(parseInt(id)) && !data.unavailable_products_ids.includes(String(id))); + + if (currentList.length !== initialLength) { + localStorage.setItem(this.storageKey, JSON.stringify(currentList)); + window.dispatchEvent(new CustomEvent('cart:updated')); + console.warn('Produits indisponibles retirés du panier:', data.unavailable_products_ids); + // Force refresh to update UI immediately + this.refreshContent(); + } + } + } catch (error) { console.error('Erreur lors de la vérification de disponibilité:', error); this.productsAvailable = false; @@ -242,6 +257,20 @@ export class FlowReserve extends HTMLAnchorElement { const data = await response.json(); + // Handle removed products (deleted from DB) + if (data.unavailable_products_ids && data.unavailable_products_ids.length > 0) { + let currentList = this.getList(); + const initialLength = currentList.length; + currentList = currentList.filter(id => !data.unavailable_products_ids.includes(parseInt(id)) && !data.unavailable_products_ids.includes(String(id))); // Handle string/int types + + if (currentList.length !== initialLength) { + localStorage.setItem(this.storageKey, JSON.stringify(currentList)); + window.dispatchEvent(new CustomEvent('cart:updated')); + // We don't recurse here to avoid infinite loops, but the UI will update next time or we could just use the returned 'products' which already excludes them. + console.warn('Certains produits ont été retirés car ils n\'existent plus:', data.unavailable_products_ids); + } + } + // Merge client-side dates if server didn't return them (or to prioritize client choice) if (!data.start_date && dates.start) data.start_date = this.formatDate(dates.start); if (!data.end_date && dates.end) data.end_date = this.formatDate(dates.end); diff --git a/migrations/Version20260203131644.php b/migrations/Version20260203131644.php new file mode 100644 index 0000000..e10ce80 --- /dev/null +++ b/migrations/Version20260203131644.php @@ -0,0 +1,244 @@ +addSql('CREATE TABLE product_blocked (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, date_start TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, date_end TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, reason TEXT DEFAULT NULL, product_id INT NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_B8CF0304584665A ON product_blocked (product_id)'); + $this->addSql('ALTER TABLE product_blocked ADD CONSTRAINT FK_B8CF0304584665A FOREIGN KEY (product_id) REFERENCES product (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE account ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE account ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN account.update_at IS \'\''); + $this->addSql('ALTER TABLE account_login_register ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE account_login_register ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN account_login_register.login_at IS \'\''); + $this->addSql('ALTER TABLE account_reset_password_request ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE account_reset_password_request ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN account_reset_password_request.requested_at IS \'\''); + $this->addSql('COMMENT ON COLUMN account_reset_password_request.expires_at IS \'\''); + $this->addSql('ALTER TABLE audit_log ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE audit_log ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN audit_log.action_at IS \'\''); + $this->addSql('ALTER TABLE backup ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE backup ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN backup.created_at IS \'\''); + $this->addSql('ALTER TABLE contrats ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE contrats ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN contrats.create_at IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats.date_at IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats.end_at IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats.update_at IS \'\''); + $this->addSql('ALTER TABLE contrats_line ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE contrats_line ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE contrats_option ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE contrats_option ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE contrats_payments ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE contrats_payments ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE contrats_payments ALTER card TYPE JSON USING card::json'); + $this->addSql('COMMENT ON COLUMN contrats_payments.payment_at IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.validate_at IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.card IS \'\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.update_at IS \'\''); + $this->addSql('ALTER TABLE customer ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE customer ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN customer.verification_code_expires_at IS \'\''); + $this->addSql('ALTER TABLE customer_address ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE customer_address ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE customer_tracking ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE customer_tracking ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE devis ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE devis ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN devis.create_a IS \'\''); + $this->addSql('COMMENT ON COLUMN devis.update_at IS \'\''); + $this->addSql('COMMENT ON COLUMN devis.start_at IS \'\''); + $this->addSql('COMMENT ON COLUMN devis.end_at IS \'\''); + $this->addSql('ALTER TABLE devis_line ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE devis_line ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE devis_options ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE devis_options ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE etat_lieux ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE etat_lieux ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE facture ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE facture ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN facture.create_at IS \'\''); + $this->addSql('COMMENT ON COLUMN facture.update_at IS \'\''); + $this->addSql('ALTER TABLE formules ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE formules ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN formules.updated_at IS \'\''); + $this->addSql('ALTER TABLE formules_options_inclus ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE formules_options_inclus ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER config TYPE JSON USING config::json'); + $this->addSql('COMMENT ON COLUMN formules_product_inclus.config IS \'\''); + $this->addSql('ALTER TABLE formules_restriction ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE formules_restriction ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE formules_restriction ALTER restriction_config TYPE JSON USING restriction_config::json'); + $this->addSql('COMMENT ON COLUMN formules_restriction.restriction_config IS \'\''); + $this->addSql('ALTER TABLE options ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE options ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN options.updated_at IS \'\''); + $this->addSql('ALTER TABLE order_session ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE order_session ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN order_session.created_at IS \'\''); + $this->addSql('COMMENT ON COLUMN order_session.updated_at IS \'\''); + $this->addSql('ALTER TABLE prestaire ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE prestaire ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE product ADD is_publish BOOLEAN DEFAULT true'); + $this->addSql('ALTER TABLE product ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE product ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN product.updated_at IS \'\''); + $this->addSql('ALTER TABLE product_doc ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE product_doc ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_doc.updated_at IS \'\''); + $this->addSql('ALTER TABLE product_photos ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE product_photos ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_photos.updated_at IS \'\''); + $this->addSql('ALTER TABLE product_reserve ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE product_reserve ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_reserve.start_at IS \'\''); + $this->addSql('COMMENT ON COLUMN product_reserve.end_at IS \'\''); + $this->addSql('ALTER TABLE product_video ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE product_video ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_video.updated_at IS \'\''); + $this->addSql('ALTER TABLE site_performance ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE site_performance ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN site_performance.created_at IS \'\''); + $this->addSql('ALTER TABLE stripe_config ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE stripe_config ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('ALTER TABLE messenger_messages ALTER id DROP DEFAULT'); + $this->addSql('ALTER TABLE messenger_messages ALTER id ADD GENERATED BY DEFAULT AS IDENTITY'); + $this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE product_blocked DROP CONSTRAINT FK_B8CF0304584665A'); + $this->addSql('DROP TABLE product_blocked'); + $this->addSql('ALTER TABLE "account" ALTER id SET DEFAULT nextval(\'account_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE "account" ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN "account".update_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE account_login_register ALTER id SET DEFAULT nextval(\'account_login_register_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE account_login_register ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN account_login_register.login_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE account_reset_password_request ALTER id SET DEFAULT nextval(\'account_reset_password_request_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE account_reset_password_request ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN account_reset_password_request.requested_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN account_reset_password_request.expires_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE audit_log ALTER id SET DEFAULT nextval(\'audit_log_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE audit_log ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN audit_log.action_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE backup ALTER id SET DEFAULT nextval(\'backup_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE backup ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN backup.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE contrats ALTER id SET DEFAULT nextval(\'contrats_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE contrats ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN contrats.create_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN contrats.date_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN contrats.end_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN contrats.update_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE contrats_line ALTER id SET DEFAULT nextval(\'contrats_line_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE contrats_line ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE contrats_option ALTER id SET DEFAULT nextval(\'contrats_option_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE contrats_option ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE contrats_payments ALTER id SET DEFAULT nextval(\'contrats_payments_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE contrats_payments ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE contrats_payments ALTER card TYPE TEXT'); + $this->addSql('COMMENT ON COLUMN contrats_payments.payment_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.validate_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.card IS \'(DC2Type:array)\''); + $this->addSql('COMMENT ON COLUMN contrats_payments.update_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE customer ALTER id SET DEFAULT nextval(\'customer_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE customer ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN customer.verification_code_expires_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE customer_address ALTER id SET DEFAULT nextval(\'customer_address_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE customer_address ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE customer_tracking ALTER id SET DEFAULT nextval(\'customer_tracking_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE customer_tracking ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE devis ALTER id SET DEFAULT nextval(\'devis_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE devis ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN devis.create_a IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN devis.update_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN devis.start_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN devis.end_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE devis_line ALTER id SET DEFAULT nextval(\'devis_line_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE devis_line ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE devis_options ALTER id SET DEFAULT nextval(\'devis_options_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE devis_options ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE etat_lieux ALTER id SET DEFAULT nextval(\'etat_lieux_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE etat_lieux ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE facture ALTER id SET DEFAULT nextval(\'facture_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE facture ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN facture.create_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN facture.update_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE formules ALTER id SET DEFAULT nextval(\'formules_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE formules ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN formules.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE formules_options_inclus ALTER id SET DEFAULT nextval(\'formules_options_inclus_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE formules_options_inclus ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER id SET DEFAULT nextval(\'formules_product_inclus_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE formules_product_inclus ALTER config TYPE TEXT'); + $this->addSql('COMMENT ON COLUMN formules_product_inclus.config IS \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE formules_restriction ALTER id SET DEFAULT nextval(\'formules_restriction_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE formules_restriction ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE formules_restriction ALTER restriction_config TYPE TEXT'); + $this->addSql('COMMENT ON COLUMN formules_restriction.restriction_config IS \'(DC2Type:array)\''); + $this->addSql('ALTER TABLE messenger_messages ALTER id SET DEFAULT nextval(\'messenger_messages_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE messenger_messages ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE options ALTER id SET DEFAULT nextval(\'options_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE options ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN options.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE order_session ALTER id SET DEFAULT nextval(\'order_session_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE order_session ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN order_session.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN order_session.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE prestaire ALTER id SET DEFAULT nextval(\'prestaire_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE prestaire ALTER id DROP IDENTITY'); + $this->addSql('ALTER TABLE product DROP is_publish'); + $this->addSql('ALTER TABLE product ALTER id SET DEFAULT nextval(\'product_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE product ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN product.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE product_doc ALTER id SET DEFAULT nextval(\'product_doc_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE product_doc ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_doc.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE product_photos ALTER id SET DEFAULT nextval(\'product_photos_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE product_photos ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_photos.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE product_reserve ALTER id SET DEFAULT nextval(\'product_reserve_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE product_reserve ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_reserve.start_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN product_reserve.end_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE product_video ALTER id SET DEFAULT nextval(\'product_video_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE product_video ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN product_video.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE site_performance ALTER id SET DEFAULT nextval(\'site_performance_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE site_performance ALTER id DROP IDENTITY'); + $this->addSql('COMMENT ON COLUMN site_performance.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE stripe_config ALTER id SET DEFAULT nextval(\'stripe_config_id_seq\'::regclass)'); + $this->addSql('ALTER TABLE stripe_config ALTER id DROP IDENTITY'); + } +} diff --git a/migrations/Version20260203133249.php b/migrations/Version20260203133249.php new file mode 100644 index 0000000..357d8c6 --- /dev/null +++ b/migrations/Version20260203133249.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE options ADD is_publish BOOLEAN DEFAULT true NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE options DROP is_publish'); + } +} diff --git a/src/Controller/Dashboard/ProductController.php b/src/Controller/Dashboard/ProductController.php index 7870e0b..a6ccb59 100644 --- a/src/Controller/Dashboard/ProductController.php +++ b/src/Controller/Dashboard/ProductController.php @@ -4,10 +4,13 @@ namespace App\Controller\Dashboard; use App\Entity\Options; use App\Entity\Product; +use App\Entity\ProductBlocked; use App\Entity\ProductDoc; use App\Entity\ProductPhotos; +use App\Entity\ProductReserve; use App\Entity\ProductVideo; use App\Form\OptionsType; +use App\Form\ProductBlockedType; use App\Form\ProductDocType; use App\Form\ProductPhotosType; use App\Form\ProductType; @@ -128,8 +131,19 @@ class ProductController extends AbstractController } #[Route(path: '/crm/products/edit/{id}', name: 'app_crm_product_edit', methods: ['GET', 'POST'])] - public function productEdit(Product $product, EntityManagerInterface $em, AppLogger $logger, Request $request, Client $stripe): Response + public function productEdit(Product $product, EntityManagerInterface $em, AppLogger $logger, Request $request, Client $stripe, \App\Repository\ProductBlockedRepository $blockedRepo, \App\Repository\ProductReserveRepository $reserveRepo): Response { + // 0. Toggle Publish + if ($request->query->get('act') === 'togglePublish') { + $status = $request->query->get('status') === 'true'; + $product->setIsPublish($status); + $em->flush(); + + $logger->record('UPDATE', "Statut de publication modifié pour {$product->getName()} : " . ($status ? 'En ligne' : 'Hors ligne')); + $this->addFlash('success', 'Le statut du produit a été mis à jour.'); + return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]); + } + // 1. Suppression de Document if ($idDoc = $request->query->get('idDoc')) { $doc = $em->getRepository(ProductDoc::class)->find($idDoc); @@ -167,7 +181,23 @@ class ProductController extends AbstractController return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]); } - // 4. Formulaire Ajout Photo + // 4. Suppression de Blocage + if ($request->query->get('act') === 'deleteBlocked' && $idBlocked = $request->query->get('idBlocked')) { + if ($this->isCsrfTokenValid('delete' . $idBlocked, $request->request->get('_token'))) { + $blocked = $em->getRepository(ProductBlocked::class)->find($idBlocked); + if ($blocked && $blocked->getProduct() === $product) { + $em->remove($blocked); + $em->flush(); + $logger->record('DELETE', "Période bloquée supprimée sur {$product->getName()}"); + $this->addFlash('success', 'Période bloquée supprimée.'); + } + } else { + $this->addFlash('error', 'Action non autorisée (Token invalide).'); + } + return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]); + } + + // 5. Formulaire Ajout Photo $photo = new ProductPhotos(); $photo->setProduct($product); $formPhotos = $this->createForm(ProductPhotosType::class, $photo); @@ -210,7 +240,37 @@ class ProductController extends AbstractController return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]); } - // 7. Formulaire Principal Produit + // 7. Formulaire Ajout Blocage + $blocked = new ProductBlocked(); + $blocked->setProduct($product); + $formBlocked = $this->createForm(ProductBlockedType::class, $blocked); + $formBlocked->handleRequest($request); + + if ($formBlocked->isSubmitted() && $formBlocked->isValid()) { + // Check for overlaps with other BLOCKS + $overlaps = $blockedRepo->findOverlappingForProduct($product, $blocked->getDateStart(), $blocked->getDateEnd()); + + // Check for overlaps with RESERVATIONS + $dummyReserve = new ProductReserve(); + $dummyReserve->setProduct($product); + $dummyReserve->setStartAt($blocked->getDateStart()); + $dummyReserve->setEndAt($blocked->getDateEnd()); + $isAvailable = $reserveRepo->checkAvailability($dummyReserve); + + if (count($overlaps) > 0) { + $this->addFlash('error', 'Impossible de bloquer cette période : elle chevauche une période DÉJÀ BLOQUÉE.'); + } elseif (!$isAvailable) { + $this->addFlash('error', 'Impossible de bloquer cette période : elle chevauche une RÉSERVATION existante.'); + } else { + $em->persist($blocked); + $em->flush(); + $logger->record('CREATE', "Période bloquée ajoutée sur {$product->getName()}"); + $this->addFlash('success', 'Période bloquée ajoutée.'); + return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]); + } + } + + // 8. Formulaire Principal Produit $form = $this->createForm(ProductType::class, $product); $form->handleRequest($request); @@ -229,6 +289,7 @@ class ProductController extends AbstractController 'formDoc' => $formDoc->createView(), 'formPhoto' => $formPhotos->createView(), 'formVideo' => $formVideo->createView(), + 'formBlocked' => $formBlocked->createView(), 'product' => $product, 'is_edit' => true ]); diff --git a/src/Controller/Dashboard/ReservationController.php b/src/Controller/Dashboard/ReservationController.php index 7c07a7f..c609446 100644 --- a/src/Controller/Dashboard/ReservationController.php +++ b/src/Controller/Dashboard/ReservationController.php @@ -34,13 +34,15 @@ class ReservationController extends AbstractController * Endpoint pour alimenter le calendrier en JSON */ #[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])] - public function getReservationData(Request $request,ContratsRepository $contratsRepository): JsonResponse + public function getReservationData(Request $request, ContratsRepository $contratsRepository, \App\Repository\ProductBlockedRepository $productBlockedRepository): JsonResponse { $start = new \DateTimeImmutable($request->query->get('start')); $end = new \DateTimeImmutable($request->query->get('end')); + /** @var Contrats[] $reservations */ $reservations = $contratsRepository->findBetweenDates($start, $end); $events = []; + foreach ($reservations as $reservation) { if($reservation->getProductReserves()->count() >0) { $events[] = [ @@ -48,6 +50,7 @@ class ReservationController extends AbstractController 'start' => $reservation->getDateAt()->format(\DateTimeInterface::ATOM), 'end' => $reservation->getEndAt()->format(\DateTimeInterface::ATOM), 'extendedProps' => [ + 'type' => 'reservation', 'start' => $reservation->getDateAt()->format('d/m/Y'), 'end' => $reservation->getEndAt()->format('d/m/Y'), 'contractNumber' => $reservation->getCustomer()->getPhone(), @@ -71,8 +74,22 @@ class ReservationController extends AbstractController } } - - + $blockedProducts = $productBlockedRepository->findBetweenDates($start, $end); + foreach ($blockedProducts as $blocked) { + $events[] = [ + 'title' => 'BLOCAGE: ' . $blocked->getProduct()->getName(), + 'start' => $blocked->getDateStart()->format(\DateTimeInterface::ATOM), + 'end' => $blocked->getDateEnd()->format(\DateTimeInterface::ATOM), + 'color' => '#ef4444', // Red for blocked + 'extendedProps' => [ + 'type' => 'blocked', + 'reason' => $blocked->getReason(), + 'productName' => $blocked->getProduct()->getName(), + 'start' => $blocked->getDateStart()->format('d/m/Y H:i'), + 'end' => $blocked->getDateEnd()->format('d/m/Y H:i'), + ] + ]; + } return new JsonResponse($events); } diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index 2405ffe..4daa221 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -171,7 +171,7 @@ class ReserverController extends AbstractController #[Route('/', name: 'reservation')] public function revervation(FormulesRepository $formulesRepository, ProductRepository $productRepository): Response { - $products = $productRepository->findBy(['category' => '3-15 ans'], ['updatedAt' => 'DESC'], 3); + $products = $productRepository->findBy(['category' => '3-15 ans', 'isPublish' => true], ['updatedAt' => 'DESC'], 3); $formules = $formulesRepository->findBy(['isPublish' => true], ['pos' => 'ASC'], 3); return $this->render('revervation/home.twig', [ @@ -339,15 +339,14 @@ class ReserverController extends AbstractController } } - if (!is_array($ids)) { - $ids = []; - } - $products = []; if (!empty($ids)) { $products = $productRepository->findBy(['id' => $ids]); } + $foundIds = array_map(fn($p) => $p->getId(), $products); + $removedIds = array_values(array_diff($ids, $foundIds)); + $items = []; $totalHT = 0; $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; @@ -382,6 +381,7 @@ class ReserverController extends AbstractController 'start_date' => $startStr, 'end_date' => $endStr, 'products' => $items, + 'unavailable_products_ids' => $removedIds, 'total' => [ 'totalHT' => $totalHT, 'totalTva' => $totalTva, @@ -431,7 +431,8 @@ class ReserverController extends AbstractController OrderSessionRepository $repository, ProductRepository $productRepository, UploaderHelper $uploaderHelper, - ProductReserveRepository $productReserveRepository + ProductReserveRepository $productReserveRepository, + EntityManagerInterface $em ): Response { $session = $repository->findOneBy(['uuid' => $sessionId]); if (!$session) { @@ -470,6 +471,14 @@ class ReserverController extends AbstractController $products = $productRepository->findBy(['id' => $ids]); } + // Cleanup missing products from session + $foundIds = array_map(fn($p) => $p->getId(), $products); + if (count($foundIds) !== count($ids)) { + $sessionData['ids'] = $foundIds; + $session->setProducts($sessionData); + $em->flush(); + } + $items = []; $totalHT = 0; $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; @@ -520,7 +529,8 @@ class ReserverController extends AbstractController OrderSessionRepository $repository, ProductRepository $productRepository, UploaderHelper $uploaderHelper, - ProductReserveRepository $productReserveRepository // Added dependency + ProductReserveRepository $productReserveRepository, // Added dependency + EntityManagerInterface $em ): Response { // This is the POST target for the login form, but also the GET page. // The authenticator handles the POST. For GET, we just render the page. @@ -561,6 +571,14 @@ class ReserverController extends AbstractController $products = $productRepository->findBy(['id' => $ids]); } + // Cleanup missing products from session + $foundIds = array_map(fn($p) => $p->getId(), $products); + if (count($foundIds) !== count($ids)) { + $sessionData['ids'] = $foundIds; + $session->setProducts($sessionData); + $em->flush(); + } + $items = []; $totalHT = 0; $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; @@ -685,7 +703,7 @@ class ReserverController extends AbstractController public function revervationCatalogue(ProductRepository $productRepository): Response { return $this->render('revervation/catalogue.twig', [ - 'products' => $productRepository->findAll(), + 'products' => $productRepository->findBy(['isPublish' => true]), 'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true", ]); } @@ -801,7 +819,7 @@ class ReserverController extends AbstractController $customer->getName() . " " . $customer->getSurname(), "[Ludikevent] - Code de récupération", "mails/welcome.twig", - ['account' => $customer] + ['customer' => $customer] ); $em->persist($customer); @@ -933,7 +951,7 @@ class ReserverController extends AbstractController foreach ($results['hits'] as $result) { $p = $productRepository->find($result['id']); - if ($p instanceof Product) { + if ($p instanceof Product && $p->isPublish()) { $items[] = [ 'image' => $uploaderHelper->asset($p, 'imageFile') ?: "/provider/images/favicon.png", "name" => $p->getName(), diff --git a/src/Entity/Options.php b/src/Entity/Options.php index 9c1178f..5d71e09 100644 --- a/src/Entity/Options.php +++ b/src/Entity/Options.php @@ -41,11 +41,26 @@ class Options #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; + #[ORM\Column(type: 'boolean', options: ['default' => true])] + private ?bool $isPublish = true; + public function __construct() { } + public function isPublish(): ?bool + { + return $this->isPublish; + } + + public function setIsPublish(bool $isPublish): static + { + $this->isPublish = $isPublish; + + return $this; + } + public function getId(): ?int { diff --git a/src/Entity/Product.php b/src/Entity/Product.php index a112185..510b18a 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -99,6 +99,15 @@ class Product #[ORM\OneToMany(targetEntity: ProductVideo::class, mappedBy: 'product')] private Collection $productVideos; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: ProductBlocked::class, mappedBy: 'product', orphanRemoval: true)] + private Collection $productBlockeds; + + #[ORM\Column(nullable: true, options: ['default' => true])] + private ?bool $isPublish = true; + public function __construct() { $this->productReserves = new ArrayCollection(); @@ -106,6 +115,8 @@ class Product $this->formulesProductIncluses = new ArrayCollection(); $this->productPhotos = new ArrayCollection(); $this->productVideos = new ArrayCollection(); + $this->productBlockeds = new ArrayCollection(); + $this->isPublish = true; } public function slug() { @@ -474,4 +485,46 @@ class Product return $this; } + + /** + * @return Collection + */ + public function getProductBlockeds(): Collection + { + return $this->productBlockeds; + } + + public function addProductBlocked(ProductBlocked $productBlocked): static + { + if (!$this->productBlockeds->contains($productBlocked)) { + $this->productBlockeds->add($productBlocked); + $productBlocked->setProduct($this); + } + + return $this; + } + + public function removeProductBlocked(ProductBlocked $productBlocked): static + { + if ($this->productBlockeds->removeElement($productBlocked)) { + // set the owning side to null (unless already changed) + if ($productBlocked->getProduct() === $this) { + $productBlocked->setProduct(null); + } + } + + return $this; + } + + public function isPublish(): ?bool + { + return $this->isPublish; + } + + public function setIsPublish(?bool $isPublish): static + { + $this->isPublish = $isPublish; + + return $this; + } } diff --git a/src/Entity/ProductBlocked.php b/src/Entity/ProductBlocked.php new file mode 100644 index 0000000..bb1e5d8 --- /dev/null +++ b/src/Entity/ProductBlocked.php @@ -0,0 +1,82 @@ +id; + } + + public function getProduct(): ?Product + { + return $this->product; + } + + public function setProduct(?Product $product): static + { + $this->product = $product; + + return $this; + } + + public function getDateStart(): ?\DateTimeImmutable + { + return $this->dateStart; + } + + public function setDateStart(\DateTimeImmutable $dateStart): static + { + $this->dateStart = $dateStart; + + return $this; + } + + public function getDateEnd(): ?\DateTimeImmutable + { + return $this->dateEnd; + } + + public function setDateEnd(\DateTimeImmutable $dateEnd): static + { + $this->dateEnd = $dateEnd; + + return $this; + } + + public function getReason(): ?string + { + return $this->reason; + } + + public function setReason(?string $reason): static + { + $this->reason = $reason; + + return $this; + } +} diff --git a/src/Form/ProductBlockedType.php b/src/Form/ProductBlockedType.php new file mode 100644 index 0000000..c5401fd --- /dev/null +++ b/src/Form/ProductBlockedType.php @@ -0,0 +1,40 @@ +add('dateStart', DateTimeType::class, [ + 'widget' => 'single_text', + 'label' => 'Début du blocage', + 'input' => 'datetime_immutable', + ]) + ->add('dateEnd', DateTimeType::class, [ + 'widget' => 'single_text', + 'label' => 'Fin du blocage', + 'input' => 'datetime_immutable', + ]) + ->add('reason', TextareaType::class, [ + 'label' => 'Raison (optionnel)', + 'required' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ProductBlocked::class, + ]); + } +} diff --git a/src/Repository/ProductBlockedRepository.php b/src/Repository/ProductBlockedRepository.php new file mode 100644 index 0000000..c04b286 --- /dev/null +++ b/src/Repository/ProductBlockedRepository.php @@ -0,0 +1,75 @@ + + * + * @method ProductBlocked|null find($id, $lockMode = null, $lockVersion = null) + * @method ProductBlocked|null findOneBy(array $criteria, array $orderBy = null) + * @method ProductBlocked[] findAll() + * @method ProductBlocked[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ProductBlockedRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ProductBlocked::class); + } + + /** + * @return ProductBlocked[] + */ + public function findBetweenDates(\DateTimeInterface $start, \DateTimeInterface $end): array + { + return $this->createQueryBuilder('pb') + ->where('pb.dateStart < :end') + ->andWhere('pb.dateEnd > :start') + ->setParameter('start', $start) + ->setParameter('end', $end) + ->getQuery() + ->getResult(); + } + + public function findOverlappingForProduct(\App\Entity\Product $product, \DateTimeInterface $start, \DateTimeInterface $end): array + { + return $this->createQueryBuilder('pb') + ->where('pb.product = :product') + ->andWhere('pb.dateStart < :end') + ->andWhere('pb.dateEnd > :start') + ->setParameter('product', $product) + ->setParameter('start', $start) + ->setParameter('end', $end) + ->getQuery() + ->getResult(); + } + +// /** +// * @return ProductBlocked[] Returns an array of ProductBlocked objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('p.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?ProductBlocked +// { +// return $this->createQueryBuilder('p') +// ->andWhere('p.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Security/SiteMapListener.php b/src/Security/SiteMapListener.php index 3089347..40a4e9e 100644 --- a/src/Security/SiteMapListener.php +++ b/src/Security/SiteMapListener.php @@ -127,7 +127,7 @@ class SiteMapListener implements EventSubscriberInterface $urlContainer->addUrl($decoratedUrl, 'reservation_formules'); } // --- 2. PRODUITS & IMAGES --- - foreach ($this->productRepository->findAll() as $product) { + foreach ($this->productRepository->findBy(['isPublish' => true]) as $product) { $productUrl = $urlGenerator->generate( 'reservation_product_show', ['id' => $product->slug()], @@ -148,7 +148,7 @@ class SiteMapListener implements EventSubscriberInterface $urlContainer->addUrl($decoratedUrl, 'reservation_product'); } - foreach ($this->optionsRepository->findAll() as $product) { + foreach ($this->optionsRepository->findBy(['isPublish' => true]) as $product) { $productUrl = $urlGenerator->generate( 'reservation_options_show', ['id' => $product->slug()], diff --git a/templates/dashboard/products/add.twig b/templates/dashboard/products/add.twig index 09d81c8..74e7b8f 100644 --- a/templates/dashboard/products/add.twig +++ b/templates/dashboard/products/add.twig @@ -5,6 +5,35 @@ {% block actions %}
+ {% if product is defined and product is not null and product.id %} +
+ {% if not product.isPublish %} +
+
+
+ Hors ligne +
+ + Publier + +
+ {% else %} +
+
+
+ En ligne +
+ + Désactiver + +
+ {% endif %} +
+ {% endif %} + {% if product.slug != "" %}

Com. Stripe (EEE) : 1,5% + 0,25€

- ~ {{ ((form.priceDay.vars.value * 0.015) + 0.25)|number_format(2, ',', ' ') }} € + ~ {{ ((form.priceDay.vars.value|default(0) * 0.015) + 0.25)|number_format(2, ',', ' ') }} €

@@ -186,7 +215,7 @@

Com. Stripe (EEE) : 1,5% + 0,25€

- ~ {{ ((form.priceSup.vars.value * 0.015) + 0.25)|number_format(2, ',', ' ') }} € + ~ {{ ((form.priceSup.vars.value|default(0) * 0.015) + 0.25)|number_format(2, ',', ' ') }} €

@@ -425,5 +454,86 @@ {{ form_end(formPhoto) }} {% endif %} + + {# 07. PÉRIODES BLOQUÉES #} + {% if formBlocked is defined %} +
+

+ 07 + Indisponibilités & Blocages +

+ + {# LISTE DES BLOCAGES #} +
+ {% for blocked in product.productBlockeds %} +
+
+
+
+ +
+ Indisponible +
+
+

Du {{ blocked.dateStart|date('d/m/Y H:i') }}

+

Au {{ blocked.dateEnd|date('d/m/Y H:i') }}

+
+ {% if blocked.reason %} +

+ {{ blocked.reason }} +

+ {% endif %} +
+ + {# DELETE BUTTON BLOCKED #} +
+
+ + +
+
+
+ {% else %} +
+

Aucune période bloquée

+
+ {% endfor %} +
+ +
+ + {# FORMULAIRE D'AJOUT BLOCAGE #} + {{ form_start(formBlocked) }} +
+
+ {{ form_label(formBlocked.dateStart, 'Début du blocage', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(formBlocked.dateStart, {'attr': {'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-rose-500/20 focus:border-rose-500 transition-all py-4 px-5 font-bold text-sm'}}) }} +
+ +
+ {{ form_label(formBlocked.dateEnd, 'Fin du blocage', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(formBlocked.dateEnd, {'attr': {'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-rose-500/20 focus:border-rose-500 transition-all py-4 px-5 font-bold text-sm'}}) }} +
+ +
+ {{ form_label(formBlocked.reason, 'Raison (Optionnel)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }} + {{ form_widget(formBlocked.reason, {'attr': {'placeholder': 'Ex: Maintenance annuelle, Réparation...', 'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-rose-500/20 focus:border-rose-500 transition-all py-4 px-5 font-medium text-sm min-h-[100px]'}}) }} +
+
+ +
+ +
+ {{ form_end(formBlocked) }} +
+ {% endif %} {% endblock %}