```
✨ feat(Product): Ajoute la publication des produits et les périodes bloquées
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.
```
This commit is contained in:
@@ -194,10 +194,19 @@ export default class PlaningLogistics extends HTMLElement {
|
|||||||
fetch(`/crm/reservation/data?${params.toString()}`)
|
fetch(`/crm/reservation/data?${params.toString()}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const formattedData = data.map(item => ({
|
const formattedData = data.map(item => {
|
||||||
...item,
|
let bgColor = '#2563eb';
|
||||||
backgroundColor: item.extendedProps.isSigned ? '#059669' : '#2563eb'
|
if (item.extendedProps.type === 'blocked') {
|
||||||
}));
|
bgColor = '#ef4444';
|
||||||
|
} else if (item.extendedProps.isSigned) {
|
||||||
|
bgColor = '#059669';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
borderColor: bgColor
|
||||||
|
};
|
||||||
|
});
|
||||||
successCallback(formattedData);
|
successCallback(formattedData);
|
||||||
this.updateStats(formattedData);
|
this.updateStats(formattedData);
|
||||||
loader.classList.add('hidden');
|
loader.classList.add('hidden');
|
||||||
@@ -208,7 +217,22 @@ export default class PlaningLogistics extends HTMLElement {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
eventContent: (arg) => {
|
eventContent: (arg) => {
|
||||||
const { caution, acompte, solde, isSigned } = arg.event.extendedProps;
|
const props = arg.event.extendedProps;
|
||||||
|
|
||||||
|
if (props.type === 'blocked') {
|
||||||
|
return {
|
||||||
|
html: `
|
||||||
|
<div class="fc-event-main-frame">
|
||||||
|
<div class="text-[10px] font-black uppercase tracking-tight leading-tight line-clamp-2 flex items-center gap-1 text-white">
|
||||||
|
⛔ ${arg.event.title.replace('BLOCAGE: ', '')}
|
||||||
|
</div>
|
||||||
|
<div class="text-[9px] text-white/80 italic truncate mt-0.5">${props.reason || 'Pas de raison'}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { caution, acompte, solde, isSigned } = props;
|
||||||
return {
|
return {
|
||||||
html: `
|
html: `
|
||||||
<div class="fc-event-main-frame">
|
<div class="fc-event-main-frame">
|
||||||
@@ -243,6 +267,32 @@ export default class PlaningLogistics extends HTMLElement {
|
|||||||
const modal = this.querySelector('#calendar-modal');
|
const modal = this.querySelector('#calendar-modal');
|
||||||
const props = event.extendedProps;
|
const props = event.extendedProps;
|
||||||
const modalStatus = this.querySelector('#modal-status-container');
|
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 = '<span class="px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest bg-rose-500/10 text-rose-500 border border-rose-500/20">Blocage Matériel</span>';
|
||||||
|
|
||||||
|
modal.classList.replace('hidden', 'flex');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`;
|
this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`;
|
||||||
this.querySelector('#modal-title').innerText = event.title;
|
this.querySelector('#modal-title').innerText = event.title;
|
||||||
|
|||||||
@@ -128,6 +128,21 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.productsAvailable = data.available;
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la vérification de disponibilité:', error);
|
console.error('Erreur lors de la vérification de disponibilité:', error);
|
||||||
this.productsAvailable = false;
|
this.productsAvailable = false;
|
||||||
@@ -242,6 +257,20 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
|
|
||||||
const data = await response.json();
|
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)
|
// 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.start_date && dates.start) data.start_date = this.formatDate(dates.start);
|
||||||
if (!data.end_date && dates.end) data.end_date = this.formatDate(dates.end);
|
if (!data.end_date && dates.end) data.end_date = this.formatDate(dates.end);
|
||||||
|
|||||||
244
migrations/Version20260203131644.php
Normal file
244
migrations/Version20260203131644.php
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260203131644 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
migrations/Version20260203133249.php
Normal file
31
migrations/Version20260203133249.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260203133249 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,13 @@ namespace App\Controller\Dashboard;
|
|||||||
|
|
||||||
use App\Entity\Options;
|
use App\Entity\Options;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
|
use App\Entity\ProductBlocked;
|
||||||
use App\Entity\ProductDoc;
|
use App\Entity\ProductDoc;
|
||||||
use App\Entity\ProductPhotos;
|
use App\Entity\ProductPhotos;
|
||||||
|
use App\Entity\ProductReserve;
|
||||||
use App\Entity\ProductVideo;
|
use App\Entity\ProductVideo;
|
||||||
use App\Form\OptionsType;
|
use App\Form\OptionsType;
|
||||||
|
use App\Form\ProductBlockedType;
|
||||||
use App\Form\ProductDocType;
|
use App\Form\ProductDocType;
|
||||||
use App\Form\ProductPhotosType;
|
use App\Form\ProductPhotosType;
|
||||||
use App\Form\ProductType;
|
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'])]
|
#[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
|
// 1. Suppression de Document
|
||||||
if ($idDoc = $request->query->get('idDoc')) {
|
if ($idDoc = $request->query->get('idDoc')) {
|
||||||
$doc = $em->getRepository(ProductDoc::class)->find($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()]);
|
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 = new ProductPhotos();
|
||||||
$photo->setProduct($product);
|
$photo->setProduct($product);
|
||||||
$formPhotos = $this->createForm(ProductPhotosType::class, $photo);
|
$formPhotos = $this->createForm(ProductPhotosType::class, $photo);
|
||||||
@@ -210,7 +240,37 @@ class ProductController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
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 = $this->createForm(ProductType::class, $product);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
@@ -229,6 +289,7 @@ class ProductController extends AbstractController
|
|||||||
'formDoc' => $formDoc->createView(),
|
'formDoc' => $formDoc->createView(),
|
||||||
'formPhoto' => $formPhotos->createView(),
|
'formPhoto' => $formPhotos->createView(),
|
||||||
'formVideo' => $formVideo->createView(),
|
'formVideo' => $formVideo->createView(),
|
||||||
|
'formBlocked' => $formBlocked->createView(),
|
||||||
'product' => $product,
|
'product' => $product,
|
||||||
'is_edit' => true
|
'is_edit' => true
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -34,13 +34,15 @@ class ReservationController extends AbstractController
|
|||||||
* Endpoint pour alimenter le calendrier en JSON
|
* Endpoint pour alimenter le calendrier en JSON
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])]
|
#[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'));
|
$start = new \DateTimeImmutable($request->query->get('start'));
|
||||||
$end = new \DateTimeImmutable($request->query->get('end'));
|
$end = new \DateTimeImmutable($request->query->get('end'));
|
||||||
|
|
||||||
/** @var Contrats[] $reservations */
|
/** @var Contrats[] $reservations */
|
||||||
$reservations = $contratsRepository->findBetweenDates($start, $end);
|
$reservations = $contratsRepository->findBetweenDates($start, $end);
|
||||||
$events = [];
|
$events = [];
|
||||||
|
|
||||||
foreach ($reservations as $reservation) {
|
foreach ($reservations as $reservation) {
|
||||||
if($reservation->getProductReserves()->count() >0) {
|
if($reservation->getProductReserves()->count() >0) {
|
||||||
$events[] = [
|
$events[] = [
|
||||||
@@ -48,6 +50,7 @@ class ReservationController extends AbstractController
|
|||||||
'start' => $reservation->getDateAt()->format(\DateTimeInterface::ATOM),
|
'start' => $reservation->getDateAt()->format(\DateTimeInterface::ATOM),
|
||||||
'end' => $reservation->getEndAt()->format(\DateTimeInterface::ATOM),
|
'end' => $reservation->getEndAt()->format(\DateTimeInterface::ATOM),
|
||||||
'extendedProps' => [
|
'extendedProps' => [
|
||||||
|
'type' => 'reservation',
|
||||||
'start' => $reservation->getDateAt()->format('d/m/Y'),
|
'start' => $reservation->getDateAt()->format('d/m/Y'),
|
||||||
'end' => $reservation->getEndAt()->format('d/m/Y'),
|
'end' => $reservation->getEndAt()->format('d/m/Y'),
|
||||||
'contractNumber' => $reservation->getCustomer()->getPhone(),
|
'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);
|
return new JsonResponse($events);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class ReserverController extends AbstractController
|
|||||||
#[Route('/', name: 'reservation')]
|
#[Route('/', name: 'reservation')]
|
||||||
public function revervation(FormulesRepository $formulesRepository, ProductRepository $productRepository): Response
|
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);
|
$formules = $formulesRepository->findBy(['isPublish' => true], ['pos' => 'ASC'], 3);
|
||||||
|
|
||||||
return $this->render('revervation/home.twig', [
|
return $this->render('revervation/home.twig', [
|
||||||
@@ -339,15 +339,14 @@ class ReserverController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_array($ids)) {
|
|
||||||
$ids = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$products = [];
|
$products = [];
|
||||||
if (!empty($ids)) {
|
if (!empty($ids)) {
|
||||||
$products = $productRepository->findBy(['id' => $ids]);
|
$products = $productRepository->findBy(['id' => $ids]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$foundIds = array_map(fn($p) => $p->getId(), $products);
|
||||||
|
$removedIds = array_values(array_diff($ids, $foundIds));
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$totalHT = 0;
|
$totalHT = 0;
|
||||||
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
||||||
@@ -382,6 +381,7 @@ class ReserverController extends AbstractController
|
|||||||
'start_date' => $startStr,
|
'start_date' => $startStr,
|
||||||
'end_date' => $endStr,
|
'end_date' => $endStr,
|
||||||
'products' => $items,
|
'products' => $items,
|
||||||
|
'unavailable_products_ids' => $removedIds,
|
||||||
'total' => [
|
'total' => [
|
||||||
'totalHT' => $totalHT,
|
'totalHT' => $totalHT,
|
||||||
'totalTva' => $totalTva,
|
'totalTva' => $totalTva,
|
||||||
@@ -431,7 +431,8 @@ class ReserverController extends AbstractController
|
|||||||
OrderSessionRepository $repository,
|
OrderSessionRepository $repository,
|
||||||
ProductRepository $productRepository,
|
ProductRepository $productRepository,
|
||||||
UploaderHelper $uploaderHelper,
|
UploaderHelper $uploaderHelper,
|
||||||
ProductReserveRepository $productReserveRepository
|
ProductReserveRepository $productReserveRepository,
|
||||||
|
EntityManagerInterface $em
|
||||||
): Response {
|
): Response {
|
||||||
$session = $repository->findOneBy(['uuid' => $sessionId]);
|
$session = $repository->findOneBy(['uuid' => $sessionId]);
|
||||||
if (!$session) {
|
if (!$session) {
|
||||||
@@ -470,6 +471,14 @@ class ReserverController extends AbstractController
|
|||||||
$products = $productRepository->findBy(['id' => $ids]);
|
$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 = [];
|
$items = [];
|
||||||
$totalHT = 0;
|
$totalHT = 0;
|
||||||
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
||||||
@@ -520,7 +529,8 @@ class ReserverController extends AbstractController
|
|||||||
OrderSessionRepository $repository,
|
OrderSessionRepository $repository,
|
||||||
ProductRepository $productRepository,
|
ProductRepository $productRepository,
|
||||||
UploaderHelper $uploaderHelper,
|
UploaderHelper $uploaderHelper,
|
||||||
ProductReserveRepository $productReserveRepository // Added dependency
|
ProductReserveRepository $productReserveRepository, // Added dependency
|
||||||
|
EntityManagerInterface $em
|
||||||
): Response {
|
): Response {
|
||||||
// This is the POST target for the login form, but also the GET page.
|
// 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.
|
// 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]);
|
$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 = [];
|
$items = [];
|
||||||
$totalHT = 0;
|
$totalHT = 0;
|
||||||
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
||||||
@@ -685,7 +703,7 @@ class ReserverController extends AbstractController
|
|||||||
public function revervationCatalogue(ProductRepository $productRepository): Response
|
public function revervationCatalogue(ProductRepository $productRepository): Response
|
||||||
{
|
{
|
||||||
return $this->render('revervation/catalogue.twig', [
|
return $this->render('revervation/catalogue.twig', [
|
||||||
'products' => $productRepository->findAll(),
|
'products' => $productRepository->findBy(['isPublish' => true]),
|
||||||
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
|
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -801,7 +819,7 @@ class ReserverController extends AbstractController
|
|||||||
$customer->getName() . " " . $customer->getSurname(),
|
$customer->getName() . " " . $customer->getSurname(),
|
||||||
"[Ludikevent] - Code de récupération",
|
"[Ludikevent] - Code de récupération",
|
||||||
"mails/welcome.twig",
|
"mails/welcome.twig",
|
||||||
['account' => $customer]
|
['customer' => $customer]
|
||||||
);
|
);
|
||||||
|
|
||||||
$em->persist($customer);
|
$em->persist($customer);
|
||||||
@@ -933,7 +951,7 @@ class ReserverController extends AbstractController
|
|||||||
|
|
||||||
foreach ($results['hits'] as $result) {
|
foreach ($results['hits'] as $result) {
|
||||||
$p = $productRepository->find($result['id']);
|
$p = $productRepository->find($result['id']);
|
||||||
if ($p instanceof Product) {
|
if ($p instanceof Product && $p->isPublish()) {
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'image' => $uploaderHelper->asset($p, 'imageFile') ?: "/provider/images/favicon.png",
|
'image' => $uploaderHelper->asset($p, 'imageFile') ?: "/provider/images/favicon.png",
|
||||||
"name" => $p->getName(),
|
"name" => $p->getName(),
|
||||||
|
|||||||
@@ -41,11 +41,26 @@ class Options
|
|||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?\DateTimeImmutable $updatedAt = null;
|
private ?\DateTimeImmutable $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean', options: ['default' => true])]
|
||||||
|
private ?bool $isPublish = true;
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
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
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,6 +99,15 @@ class Product
|
|||||||
#[ORM\OneToMany(targetEntity: ProductVideo::class, mappedBy: 'product')]
|
#[ORM\OneToMany(targetEntity: ProductVideo::class, mappedBy: 'product')]
|
||||||
private Collection $productVideos;
|
private Collection $productVideos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, ProductBlocked>
|
||||||
|
*/
|
||||||
|
#[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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->productReserves = new ArrayCollection();
|
$this->productReserves = new ArrayCollection();
|
||||||
@@ -106,6 +115,8 @@ class Product
|
|||||||
$this->formulesProductIncluses = new ArrayCollection();
|
$this->formulesProductIncluses = new ArrayCollection();
|
||||||
$this->productPhotos = new ArrayCollection();
|
$this->productPhotos = new ArrayCollection();
|
||||||
$this->productVideos = new ArrayCollection();
|
$this->productVideos = new ArrayCollection();
|
||||||
|
$this->productBlockeds = new ArrayCollection();
|
||||||
|
$this->isPublish = true;
|
||||||
}
|
}
|
||||||
public function slug()
|
public function slug()
|
||||||
{
|
{
|
||||||
@@ -474,4 +485,46 @@ class Product
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, ProductBlocked>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/Entity/ProductBlocked.php
Normal file
82
src/Entity/ProductBlocked.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ProductBlockedRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ProductBlockedRepository::class)]
|
||||||
|
class ProductBlocked
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'productBlockeds')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Product $product = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private ?\DateTimeImmutable $dateStart = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private ?\DateTimeImmutable $dateEnd = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $reason = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Form/ProductBlockedType.php
Normal file
40
src/Form/ProductBlockedType.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\ProductBlocked;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class ProductBlockedType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/Repository/ProductBlockedRepository.php
Normal file
75
src/Repository/ProductBlockedRepository.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ProductBlocked;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<ProductBlocked>
|
||||||
|
*
|
||||||
|
* @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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -127,7 +127,7 @@ class SiteMapListener implements EventSubscriberInterface
|
|||||||
$urlContainer->addUrl($decoratedUrl, 'reservation_formules');
|
$urlContainer->addUrl($decoratedUrl, 'reservation_formules');
|
||||||
}
|
}
|
||||||
// --- 2. PRODUITS & IMAGES ---
|
// --- 2. PRODUITS & IMAGES ---
|
||||||
foreach ($this->productRepository->findAll() as $product) {
|
foreach ($this->productRepository->findBy(['isPublish' => true]) as $product) {
|
||||||
$productUrl = $urlGenerator->generate(
|
$productUrl = $urlGenerator->generate(
|
||||||
'reservation_product_show',
|
'reservation_product_show',
|
||||||
['id' => $product->slug()],
|
['id' => $product->slug()],
|
||||||
@@ -148,7 +148,7 @@ class SiteMapListener implements EventSubscriberInterface
|
|||||||
|
|
||||||
$urlContainer->addUrl($decoratedUrl, 'reservation_product');
|
$urlContainer->addUrl($decoratedUrl, 'reservation_product');
|
||||||
}
|
}
|
||||||
foreach ($this->optionsRepository->findAll() as $product) {
|
foreach ($this->optionsRepository->findBy(['isPublish' => true]) as $product) {
|
||||||
$productUrl = $urlGenerator->generate(
|
$productUrl = $urlGenerator->generate(
|
||||||
'reservation_options_show',
|
'reservation_options_show',
|
||||||
['id' => $product->slug()],
|
['id' => $product->slug()],
|
||||||
|
|||||||
@@ -5,6 +5,35 @@
|
|||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
|
{% if product is defined and product is not null and product.id %}
|
||||||
|
<div class="flex items-center">
|
||||||
|
{% if not product.isPublish %}
|
||||||
|
<div class="flex items-center bg-white/5 backdrop-blur-xl border border-rose-500/30 rounded-2xl overflow-hidden shadow-xl">
|
||||||
|
<div class="flex items-center px-5 py-3 space-x-3 bg-rose-500/5">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.8)]"></div>
|
||||||
|
<span class="text-[10px] font-black text-rose-500 uppercase tracking-widest">Hors ligne</span>
|
||||||
|
</div>
|
||||||
|
<a data-turbo="false" href="{{ path('app_crm_product_edit', {id: product.id, act: 'togglePublish', status: 'true'}) }}"
|
||||||
|
class="px-6 py-3 bg-emerald-600 hover:bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/5">
|
||||||
|
Publier
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex items-center bg-white/5 backdrop-blur-xl border border-emerald-500/30 rounded-2xl overflow-hidden shadow-xl">
|
||||||
|
<div class="flex items-center px-5 py-3 space-x-3 bg-emerald-500/5">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.8)] animate-pulse"></div>
|
||||||
|
<span class="text-[10px] font-black text-emerald-400 uppercase tracking-widest">En ligne</span>
|
||||||
|
</div>
|
||||||
|
<a data-turbo="false" href="{{ path('app_crm_product_edit', {id: product.id, act: 'togglePublish', status: 'false'}) }}"
|
||||||
|
onclick="return confirm('Voulez-vous vraiment masquer ce produit ?')"
|
||||||
|
class="px-6 py-3 bg-white/5 hover:bg-rose-600 text-slate-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/10">
|
||||||
|
Désactiver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if product.slug != "" %}
|
{% if product.slug != "" %}
|
||||||
<a target="_blank" rel="nofollow"
|
<a target="_blank" rel="nofollow"
|
||||||
href="https://reservation.ludikevent.fr{{ path('reservation_product_show', {id: product.slug}) }}"
|
href="https://reservation.ludikevent.fr{{ path('reservation_product_show', {id: product.slug}) }}"
|
||||||
@@ -172,7 +201,7 @@
|
|||||||
<div class="flex justify-between items-center mt-1 px-1 {{ form.priceDay.vars.value ? '' : 'hidden' }}">
|
<div class="flex justify-between items-center mt-1 px-1 {{ form.priceDay.vars.value ? '' : 'hidden' }}">
|
||||||
<p class="text-[9px] text-slate-500 italic" title="Pour les cartes standard de l'Espace économique européen">Com. Stripe (EEE) : 1,5% + 0,25€</p>
|
<p class="text-[9px] text-slate-500 italic" title="Pour les cartes standard de l'Espace économique européen">Com. Stripe (EEE) : 1,5% + 0,25€</p>
|
||||||
<p class="text-[10px] font-bold text-slate-400 commission-display">
|
<p class="text-[10px] font-bold text-slate-400 commission-display">
|
||||||
~ {{ ((form.priceDay.vars.value * 0.015) + 0.25)|number_format(2, ',', ' ') }} €
|
~ {{ ((form.priceDay.vars.value|default(0) * 0.015) + 0.25)|number_format(2, ',', ' ') }} €
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +215,7 @@
|
|||||||
<div class="flex justify-between items-center mt-1 px-1 {{ form.priceSup.vars.value ? '' : 'hidden' }}">
|
<div class="flex justify-between items-center mt-1 px-1 {{ form.priceSup.vars.value ? '' : 'hidden' }}">
|
||||||
<p class="text-[9px] text-slate-500 italic" title="Pour les cartes standard de l'Espace économique européen">Com. Stripe (EEE) : 1,5% + 0,25€</p>
|
<p class="text-[9px] text-slate-500 italic" title="Pour les cartes standard de l'Espace économique européen">Com. Stripe (EEE) : 1,5% + 0,25€</p>
|
||||||
<p class="text-[10px] font-bold text-slate-400 commission-display">
|
<p class="text-[10px] font-bold text-slate-400 commission-display">
|
||||||
~ {{ ((form.priceSup.vars.value * 0.015) + 0.25)|number_format(2, ',', ' ') }} €
|
~ {{ ((form.priceSup.vars.value|default(0) * 0.015) + 0.25)|number_format(2, ',', ' ') }} €
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -425,5 +454,86 @@
|
|||||||
{{ form_end(formPhoto) }}
|
{{ form_end(formPhoto) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# 07. PÉRIODES BLOQUÉES #}
|
||||||
|
{% if formBlocked is defined %}
|
||||||
|
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl mt-8">
|
||||||
|
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||||
|
<span class="w-8 h-8 bg-rose-600/20 text-rose-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">07</span>
|
||||||
|
Indisponibilités & Blocages
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{# LISTE DES BLOCAGES #}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
|
||||||
|
{% for blocked in product.productBlockeds %}
|
||||||
|
<div class="relative group p-6 rounded-2xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-rose-500/20 text-rose-500 flex items-center justify-center">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black uppercase tracking-widest text-white">Indisponible</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-xs text-slate-400">Du <strong class="text-white">{{ blocked.dateStart|date('d/m/Y H:i') }}</strong></p>
|
||||||
|
<p class="text-xs text-slate-400">Au <strong class="text-white">{{ blocked.dateEnd|date('d/m/Y H:i') }}</strong></p>
|
||||||
|
</div>
|
||||||
|
{% if blocked.reason %}
|
||||||
|
<p class="mt-4 text-[10px] text-slate-500 italic border-l-2 border-slate-700 pl-3">
|
||||||
|
{{ blocked.reason }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# DELETE BUTTON BLOCKED #}
|
||||||
|
<div class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all">
|
||||||
|
<form data-turbo="false" method="post" action="{{ path('app_crm_product_edit', {'id': product.id, act:'deleteBlocked', idBlocked: blocked.id}) }}"
|
||||||
|
onsubmit="return confirm('Supprimer ce blocage ?');" class="inline-block">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ blocked.id) }}">
|
||||||
|
<button type="submit" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" title="Supprimer">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-full py-8 text-center border-2 border-dashed border-white/5 rounded-3xl">
|
||||||
|
<p class="text-[10px] font-black text-slate-600 uppercase tracking-[0.2em]">Aucune période bloquée</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-px bg-white/5 w-full mb-10"></div>
|
||||||
|
|
||||||
|
{# FORMULAIRE D'AJOUT BLOCAGE #}
|
||||||
|
{{ form_start(formBlocked) }}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
|
||||||
|
<div>
|
||||||
|
{{ 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'}}) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ 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'}}) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
{{ 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]'}}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-end">
|
||||||
|
<button type="submit" class="group px-8 py-4 bg-rose-600/10 hover:bg-rose-600 text-rose-500 hover:text-white text-[10px] font-black uppercase tracking-widest rounded-2xl transition-all border border-rose-500/20 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Bloquer cette période
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{ form_end(formBlocked) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user