From 6f4cc3afffde36a13618458ffda72e6104624f36 Mon Sep 17 00:00:00 2001 From: polo Date: Wed, 30 Aug 2023 12:20:39 +0200 Subject: =?UTF-8?q?section=20client=20=C3=A0=20part,=20section=20modif=20e?= =?UTF-8?q?n=20cours,=20bug=20dans=20Dates,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/dev.sqlite | Bin 36864 -> 36864 bytes src/Dates.php | 12 ++- src/Latex.php | 53 ++++++----- src/functions.php | 199 +++++++++++++++------------------------- src/main.php | 3 +- src/model/CESU.php | 12 ++- src/model/Clients.php | 49 +++++++++- src/model/DevisFactures.php | 34 ++++++- src/model/Locations.php | 18 +++- src/model/Model.php | 25 +++-- src/model/Prestations.php | 26 +++++- src/model/StructTablesDB.php | 2 +- src/model/traits.php | 13 --- src/sections/1_customer.php | 25 +++-- src/sections/2_service.php | 62 +++++++++---- src/sections/3_modify_data.php | 170 +++++++++++++++++++++++++++++++++- src/sections/4_get_document.php | 2 +- src/sections/main_loop.php | 30 +++--- src/view/Zenity.php | 30 ++++-- src/view/ZenitySetup.php | 28 ++++-- 20 files changed, 549 insertions(+), 244 deletions(-) delete mode 100644 src/model/traits.php diff --git a/data/dev.sqlite b/data/dev.sqlite index 857931f..61eb65d 100644 Binary files a/data/dev.sqlite and b/data/dev.sqlite differ diff --git a/src/Dates.php b/src/Dates.php index 2fe93cb..d7ed6c7 100644 --- a/src/Dates.php +++ b/src/Dates.php @@ -59,8 +59,7 @@ class Dates $input = $this->dashOrSlash($input); // formate pour strtotime() $this->setDayMonthYear($input); - //~ if(checkdate($splitedDate[0], $splitedDate[1], $splitedDate[2])) - if(checkdate($this->day, $this->month, $this->year)) + if(checkdate($this->month, $this->day, $this->year)) // checkdate() veut un format américain { $this->date = $input; $this->timestamp = strtotime($input); // date (string) -> timestamp (int) @@ -86,14 +85,19 @@ class Dates $splitedDate = preg_split('#\D#', $input); // \D = tout sauf chiffre if(self::$date_format === 'euro') + { + $this->day = $splitedDate[0]; + $this->month = $splitedDate[1]; + } + elseif(self::$date_format === 'usa') { $this->day = $splitedDate[1]; $this->month = $splitedDate[0]; } else { - $this->day = $splitedDate[0]; - $this->month = $splitedDate[1]; + echo("Le fichier config.php comporte une erreur. La variable $date_format doit avoir pour valeur 'euro' ou 'usa'"); + die(); // brutal } $this->year = $splitedDate[2]; } diff --git a/src/Latex.php b/src/Latex.php index 547c130..fd76f7b 100644 --- a/src/Latex.php +++ b/src/Latex.php @@ -12,21 +12,21 @@ abstract class Latex protected $data = []; // données à insérer dans le template protected $latex = ''; // latex pur - static function makeLatexSubClass(string $type) + static function makeInstance(string $type) { switch($type) { // documents pour les clients case 'devis': - return new DevisLatex($type); + return new DevisLatex; case 'facture': - return new FactureLatex($type); + return new FactureLatex; case 'location': - return new LocationLatex($type); + return new LocationLatex; case 'enveloppe_recto': - return new EnveloppeRectoLatex($type); + return new EnveloppeRectoLatex; case 'enveloppe_verso': - return new EnveloppeVersoLatex($type); + return new EnveloppeVersoLatex; // pas de document case 'cesu': return null; @@ -41,10 +41,8 @@ abstract class Latex } } - public function __construct(string $type) - { - $this->type = $type; - } + //~ public function __construct() + //~ {} public function makeLatex() { @@ -104,10 +102,8 @@ abstract class PrestaLatex extends Latex { protected $Date; // sera un type "Dates" - //~ public function __construct(string $type) - //~ { - //~ $this->type = $type; - //~ } + //~ public function __construct() + //~ {} public function setData(Object $Object): self { @@ -158,17 +154,32 @@ abstract class PrestaLatex extends Latex } } class DevisLatex extends PrestaLatex // extends Latex -{} +{ + public function __construct() + { + $this->type = 'devis'; + } +} class FactureLatex extends PrestaLatex // extends Latex -{} +{ + public function __construct() + { + $this->type = 'facture'; + } +} class LocationLatex extends PrestaLatex // extends Latex -{} +{ + public function __construct() + { + $this->type = 'location'; + } +} class EnveloppeRectoLatex extends Latex { - public function __construct(string $type) + public function __construct() { - $this->type = $type; + $this->type = 'enveloppe_recto'; $this->latex_path = Config::$latex_path . 'enveloppes_recto/'; $this->pdf_path = Config::$pdf_path . 'enveloppes_recto/'; } @@ -188,9 +199,9 @@ class EnveloppeRectoLatex extends Latex } class EnveloppeVersoLatex extends Latex { - public function __construct(string $type) + public function __construct() { - $this->type = $type; + $this->type = 'enveloppe_verso'; $this->file_name = 'enveloppe_verso.tex'; $this->latex_path = Config::$latex_path; $this->pdf_path = Config::$pdf_path; diff --git a/src/functions.php b/src/functions.php index f78b23d..c847403 100644 --- a/src/functions.php +++ b/src/functions.php @@ -7,9 +7,8 @@ // commande pour lancer une application graphique en ouvrant un fichier function windowAppCommand(string $app, string $path = ''): string { - // attention, la syntaxe utilisée est la plus simple: "app fichier" - // ça fonctionne avec les logiciels choisis: gimp, scribus - // mais ça pourrait ne pas convenir pour d'autres + // attention, ne supporte que la syntaxe la plus simple avec un seul paramètre:"app fichier" + // fonctionne avec choisis: gimp, scribus, sqlite, "l'explorateur de fichiers par défaut" $command = 'nohup ' . $app; // détache l'appli du script PHP if($path !== '') @@ -49,65 +48,36 @@ function enterCustomer($Client): bool function makeObjectClient() { // fenêtres - //~ $QuestionNouveauClient = new ZenityQuestion(ZenitySetup::$question_nouveau_client_text); $RechercheClient = new zenityEntry(ZenitySetup::$recherche_client_text); $ResultatsRechercheClient = new ZenityList(ZenitySetup::$resultats_recherche_client_text, []); - //~ $NouveauClient = new ZenityForms(ZenitySetup::$nouveau_client_text, ZenitySetup::$nouveau_client_entrees); $Client = new Clients; - // est ce que le client est déjà dans la base? - //~ $client_inconnu = true; - //~ if(exec($QuestionNouveauClient->get()) == '0') // $? = 0 signifie oui, double égal == pour le transtypage - //~ { - //~ echo "choix: recherche d'une client\n"; - $input = exec($RechercheClient->get()); - if($input == '') - { - echo "debug: recherche annulée ou saisie vide\n"; - return 0; - } - - echo "debug: recherche effectuée\n"; - $ResultatsRechercheClient->setListRows(searchCustomer($input, $Client), $Client->getTable()); // recherche silencieuse - - // sélection parmi les résultats - $input = exec($ResultatsRechercheClient->get()); // renvoie l'ID de la table 'clients' - $ResultatsRechercheClient->cleanCommand(); - - if($input == '') - { - echo "debug: client pas trouvé ou pas sélectionné\n"; - return 0; - } - - echo "debug: client sélectionné\n"; - $Client->hydrate($Client->findById($input)); - //~ $client_inconnu = false; - //~ } - //~ else - //~ { - //~ echo "choix: nouveau client\n"; - //~ } + $input = exec($RechercheClient->get()); + if($input == '') + { + echo "debug: recherche annulée ou saisie vide\n"; + return 0; + } + + echo "debug: recherche effectuée\n"; + $ResultatsRechercheClient->setListRows( + searchCustomer($input, $Client), + count(StructTablesDB::$structureOfTables[$Client->getTable()])); // 2è paramètre = nombre de colonnes + + // sélection parmi les résultats + $input = exec($ResultatsRechercheClient->get()); // renvoie l'ID de la table 'clients' + $ResultatsRechercheClient->cleanCommand(); + + if($input == '') + { + echo "debug: client pas trouvé ou pas sélectionné\n"; + return 0; + } - // on n'a pas cherché OU on n'a pas trouvé - //~ if($client_inconnu) - //~ { - //~ $input = exec($NouveauClient->get()); - //~ if($input == '') - //~ { - //~ echo "debug: annulation lors de l'enregistrement d'un nouveau client\n"; - //~ return 0; - //~ } - //~ if(!$Client->hydrateFromForm($input)) - //~ { - //~ // messages d'erreur dans hydrateFromForm() - //~ return 0; - //~ } - //~ unset($input); - //~ $Client->create(); - //~ $Client->setID(); // sans paramètre, exécute un $this->db->lastInsertId() - //~ } + echo "debug: client sélectionné\n"; + $Client->setID($input); + $Client->hydrate($Client->findById()); return $Client; } @@ -121,10 +91,57 @@ function searchCustomer(string $input, Clients $Client): array { $input_array = explode(' ', $input); // si plusieurs mot, on les recherche tous l'un après l'autre $result = $Client->findByKeywords($input_array, 'prenom_nom'); // on obtient un tableau à deux dimensions avec les entrées trouvées + //var_dump($result); return($result); } +function getServices(Clients $Client) +{ + echo "debug: recherche d'une prestation\n"; + + // recherche dans la table 'prestations' avec 'ID_client' les ID des prestas + $Presta = new Prestations($Client->getID()); + $IDs = $Presta->getIDsByIdClient(); + unset($Presta); + + // mettres toutes les données dans un tableau + $PrestaList = []; + foreach($IDs as $id) + { + $PrestaList[$id] = new Prestations($Client->getID()); // renseigne 'ID_client' + $PrestaList[$id]->setID($id); // ID de la prestation = clé du tableau + $PrestaList[$id]->hydrate($PrestaList[$id]->findById()); // données copiés de la table à l'objet + } + + // fenêtre + $entrees = []; + foreach($PrestaList as $Presta) + { + $id = $Presta->getID(); + $entrees[$id][] = $id; + $Date = new Dates((int)$Presta->getDate()); // envoi du timestamp, (int) est là par sécurité + $entrees[$id][] = $Date->getDate(); + $entrees[$id][] = $Presta->getTypePresta(); + $entrees[$id][] = $Presta->getCodePresta(); + } + $ResultatsRecherchePresta = new ZenityList(ZenitySetup::$resultats_recherche_presta_text, []); + $ResultatsRecherchePresta->setListRows($entrees, 4); + + // choix de l'utilisateur + $input = exec($ResultatsRecherchePresta->get()); // $input est l'ID de la prestation + if($input == '') + { + echo "debug: recherche annulée ou saisie vide\n"; + return 0; + } + else + { + return $PrestaList[$input]; + } +} + + function makeTexAndPdf(Object $Object) { if(get_class($Object) !== 'EnveloppeVersoLatex') @@ -132,72 +149,6 @@ function makeTexAndPdf(Object $Object) makeFolder($Object->getLatexPath()); makeFolder($Object->getPdfPath()); } - makeFile($Object->getLatexPath(), $Object->getFileName(), $Object->getLatex()); - latexToPdf($Object->getLatexPath(), $Object->getFileName(), $Object->getPdfPath()); + makeFile($Object->getLatexPath(), $Object->getFileName(), $Object->getLatex()); // fichier .tex + latexToPdf($Object->getLatexPath(), $Object->getFileName(), $Object->getPdfPath()); // fichier .pdf avec pdflatex } - -//~ function makeLatexAndPdfDocuments(Clients $Client = null, Prestations $Presta = null, $PrestaDetails = null) -//~ { - //~ $latex = ''; - //~ $year = ''; - //~ $data = []; - //~ $latex_path = Config::$latex_path; - //~ $pdf_path = Config::$pdf_path; - - //~ // verso d'une enveloppe - //~ $latex = makeLatex('enveloppe_verso'); // pas de données transmises, elles sont dans la classe Config - //~ $file_name = 'enveloppe_verso.tex'; - //~ makeFile($latex_path, $file_name, $latex); - - //~ latexToPdf($latex_path, $file_name, $pdf_path); - - - //~ if($Client !== null) - //~ { - //~ $data = $Client->getAll(); - - //~ // recto d'une enveloppe - //~ $latex_recto_path = $latex_path . 'enveloppes_recto/'; - //~ $pdf_recto_path = $pdf_path . 'enveloppes_recto/'; - //~ $data['code_postal_espaces'] = implode(' \ ', str_split($data['code_postal'])); // code postal avec 2 espaces entre chaque chiffre: 2 \ 9 \ 0 \ 0 \ 0 - - //~ $latex = makeLatex('enveloppe_recto', $data); // injection des variables - //~ $file_name = $Client->getCodeClient() . '.tex'; - //~ makeFolder($latex_recto_path); - //~ makeFile($latex_recto_path, $file_name, $latex); - - //~ makeFolder($pdf_recto_path); - //~ latexToPdf($latex_recto_path, $file_name, $pdf_recto_path); - - //~ // facture, devis, location - //~ if($Presta !== null && $PrestaDetails !== null) - //~ { - //~ $type = $Presta->getTypePresta(); - //~ $file_name = $type . '.tex'; - //~ if($type === 'facture' || $type === 'devis' || $type === 'location') - //~ { - //~ $data = array_merge($data, $Presta->getAll()); - //~ $Date = new Dates($Presta->getDate()); // entrée = timestamp (doit être un "int"!!) - //~ $year = $Date->getYear(); - //~ $latex_year_path = $latex_path . $year . '/'; // un sous-dossier par année - //~ $pdf_year_path = $pdf_path . $year . '/'; - //~ $data = array_merge($data, $PrestaDetails->getAll()); - - //~ $latex = makeLatex($type, $data, $Date); // injection des variables - //~ $file_name = $Presta->getCodePresta() . '.tex'; - //~ makeFolder($latex_year_path); - //~ makeFile($latex_year_path, $file_name, $latex); - - //~ makeFolder($pdf_year_path); - //~ latexToPdf($latex_year_path, $file_name, $pdf_year_path); - //~ } - //~ elseif($type === 'cesu' || $type === 'non_vendue') - //~ {} // pas de document - //~ else - //~ { - //~ echo "debug: erreur génération latex, type de prestation \n"; - //~ return 0; - //~ } - //~ } - //~ } -//~ } diff --git a/src/main.php b/src/main.php index a0b973b..303e1ef 100755 --- a/src/main.php +++ b/src/main.php @@ -37,14 +37,13 @@ require('model/Model.php'); // class Model extends DB, requêtes SQL require('model/StructTablesDB.php'); Model::createTables(); -//~ require('model/traits.php'); require('model/Clients.php'); require('model/Prestations.php'); require('model/DevisFactures.php'); require('model/CESU.php'); require('model/Locations.php'); -require('view/Zenity.php'); // commande système zenity +require('view/Zenity.php'); // générer ler commandes système zenity require('view/ZenitySetup.php'); // texte dans les fenêtres ET instanciation (un objet = une commande) require('Latex.php'); // générer le code LaTeX diff --git a/src/model/CESU.php b/src/model/CESU.php index dbb4023..a8aa1e8 100644 --- a/src/model/CESU.php +++ b/src/model/CESU.php @@ -6,18 +6,26 @@ class CESU extends Model //~ const TABLE = 'cesu'; // lecture des données ou hydratation + protected $ID; protected $ID_presta; protected $taches; protected $duree_travail; protected $salaire; - //~ use ModelChildren; - public function __construct() { $this->table = strtolower(__CLASS__); // cesu } + public function getAllWithWindowFields(): array // différent de Model::getAll() qui retourne get_object_vars($this) + { + return [ + "Numéro CESU:" => $this->ID, + "Tâche effectuée:" => $this->taches, + "Durée du travail:" => $this->duree_travail, + "Salaire:" => $this->salaire]; + } + // setters public function setIDPresta(int $value) { diff --git a/src/model/Clients.php b/src/model/Clients.php index f36acc1..6a4dcf5 100644 --- a/src/model/Clients.php +++ b/src/model/Clients.php @@ -14,8 +14,6 @@ class Clients extends Model protected $courriel; protected $apropos; protected $type = 'prospect'; - - //~ use ModelChildren; public function __construct() { @@ -31,6 +29,52 @@ class Clients extends Model { return $this->code_client; } + public function getAllWithWindowFields(): array // différent de Model::getAll() qui retourne get_object_vars($this) + { + return [ + "Prénom Nom:" => $this->prenom_nom, + "Code client (J.C.Dusse):" => $this->code_client, + "Adresse:" => $this->adresse, + "Code postal:" => $this->code_postal, + "Ville:" => $this->ville, + "Telephone:" => $this->telephone, + "Courriel:" => $this->courriel, + "À propos:" => $this->apropos, + "Client ou Prospect?" => $this->type]; + } + public function getSetterAndSet(string $entry, string $input) + { + switch($entry) + { + case "Prénom Nom:": + $this->setPrenomNom($input); + break; + case "Code client (J.C.Dusse):": + $this->setCodeClient($input); + break; + case "Adresse:": + $this->setAdresse($input); + break; + case "Code postal:": + $this->setCodePostal($input); + break; + case "Ville:": + $this->setVille($input); + break; + case "Telephone:": + $this->setTelephone($input); + break; + case "Courriel:": + $this->setCourriel($input); + break; + case "À propos:": + $this->setApropos($input); + break; + case "Client ou Prospect?": + $this->setType($input); + break; + } + } // setters public function setPrenomNom($value) @@ -58,7 +102,6 @@ class Clients extends Model $this->ville = (string) $value; return $this; } - public function setTelephone($value) // chaine parce que zenity renvoie une chaine et parce qu'on garde le 0 au début { if(is_numeric($value)) diff --git a/src/model/DevisFactures.php b/src/model/DevisFactures.php index c9b7d18..5769842 100644 --- a/src/model/DevisFactures.php +++ b/src/model/DevisFactures.php @@ -22,12 +22,42 @@ class DevisFactures extends Model protected $validite_devis; protected $signature_devis; - //~ use ModelChildren; - public function __construct(string $table) { $this->table = $table; // deux tables séparées devis et factures } + public function getAllWithWindowFields(): array // différent de Model::getAll() qui retourne get_object_vars($this) + { + $champs_communs = [ + "Tâches:" => $this->taches, + "PC:" => $this->machine, + "OS:" => $this->OS, + "Données:" => $this->donnees, + "Clés de licences:" => $this->cles_licences, + "Total Main d'oeuvre:" => $this->total_main_d_oeuvre, + "Pièces" => $this->pieces, + "Total des pièces" => $this->total_pieces, + "Déplacement" => $this->deplacement, + "Total HT" => $this->total_HT]; + + if($this->table === 'factures') + { + return ["Numéro facture:" => $this->ID] + $champs_communs; + } + elseif($this->table === 'devis') + { + $champs_devis = [ + "Delai de livraison" => $this->delai_livraison, + "Durée de validité" => $this->validite_devis, + "Devis signé?" => $this->signature_devis]; + + return ["Numéro devis:" => $this->ID] + $champs_communs + $champs_devis; + } + else + { + return []; + } + } // setters public function setIDPresta(int $value) diff --git a/src/model/Locations.php b/src/model/Locations.php index b669e7c..e3c9507 100644 --- a/src/model/Locations.php +++ b/src/model/Locations.php @@ -4,6 +4,7 @@ class Locations extends Model { // lecture des données ou hydratation + protected $ID; protected $ID_presta; protected $designation; protected $modele_description; @@ -15,13 +16,26 @@ class Locations extends Model protected $loyers_payes; protected $caution; - //~ use ModelChildren; - public function __construct() { $this->table = strtolower(__CLASS__); // locations } + public function getAllWithWindowFields(): array // différent de Model::getAll() qui retourne get_object_vars($this) + { + return [ + "Numéro location:" => $this->ID, + "Désignation:" => $this->designation, + "Description du modèle:" => $this->modele_description, + "Valeur:" => $this->valeur, + "État des lieux de début:" => $this->etat_des_lieux_debut, + "État des lieux de fin:" => $this->etat_des_lieux_fin, + "Durée de la location:" => $this->duree_location, + "Loyer Mensuel" => $this->loyer_mensuel, + "Loyers Payés" => $this->loyers_payes, + "Caution" => $this->caution]; + } + // setters public function setIDPresta(int $value) { diff --git a/src/model/Model.php b/src/model/Model.php index 3fb3bdf..4c4a80c 100644 --- a/src/model/Model.php +++ b/src/model/Model.php @@ -19,7 +19,7 @@ abstract class Model extends DB return $this->table; } - public function getAll(): array + public function getAll(): array // à améliorer pour ne pas renvoyer $db et $table { return get_object_vars($this); // retourne les propriétés de l'objet } @@ -29,7 +29,7 @@ abstract class Model extends DB { if($value === 0) { - $this->ID = $this->db->lastInsertId(); // méthode de PDO (attention ne gère pas la concurence) + $this->ID = $this->db->lastInsertId(); // méthode de PDO, attention lastInsertId() ne gère pas la concurence } else { @@ -183,9 +183,22 @@ abstract class Model extends DB return($this->execQuery('SELECT * FROM ' . $this->table)->fetchAll()); // fonctionne aussi sans le point virgule dans le SQL!! } - public function findById(int $id) // obtenir une entrée avec son ID + public function findById() // obtenir une entrée avec son ID { - return($this->execQuery('SELECT * FROM ' . $this->table . ' WHERE id = ' . $id)->fetch()); + return($this->execQuery('SELECT * FROM ' . $this->table . ' WHERE id = ' . $this->ID)->fetch()); + } + public function getDetailsByIdPresta() + { + if($this->table == 'prestations') + { + // à l'occaz, rendre abstraite la classe Prestations + echo 'erreur: ne pas appeler Model::getDetailsByIdPresta() si la table ciblée est "prestations".'; + return 0; + } + else + { + return $this->execQuery('SELECT * FROM ' . $this->table . ' WHERE id_Presta = ' . $this->ID_presta)->fetch(); // type array + } } protected function find(array $criteria): array // obtenir une entrée avec un tableau associatif 'champ' => 'valeur' @@ -207,7 +220,7 @@ abstract class Model extends DB // update UPDATE - public function update(int $id) // utiliser plutôt $this->ID ? + public function update() { $fields = []; $values = []; @@ -219,7 +232,7 @@ abstract class Model extends DB $values[] = $value; } } - $values[] = $id; // cette syntaxe ajoute une valeur au tableau + $values[] = $this->ID; // cette syntaxe ajoute une valeur au tableau $field_list = implode(', ', $fields); // UPDATE annonces SET titre = ?, description = ?, actif = ? WHERE id = ? diff --git a/src/model/Prestations.php b/src/model/Prestations.php index 22865df..fe09133 100644 --- a/src/model/Prestations.php +++ b/src/model/Prestations.php @@ -11,8 +11,6 @@ class Prestations extends Model protected $type_presta = ''; protected $mode_paiement = ''; protected $commentaires = ''; - - //~ use ModelChildren; public function __construct(int $ID_client) { @@ -29,6 +27,16 @@ class Prestations extends Model { return $this->ID_client; } + public function getIDsByIdClient() // obtenir une entrée avec son ID_client + { + $IDs = $this->execQuery('SELECT id FROM ' . $this->table . ' WHERE id_client = ' . $this->ID_client)->fetchAll(); + // changer le tableau de tableaux en tableau simple + for($i = 0; $i < count($IDs); $i++) + { + $IDs[$i] = $IDs[$i]['ID']; + } + return($IDs); + } public function getCodePresta(): string { return $this->code_presta; @@ -41,6 +49,18 @@ class Prestations extends Model { return $this->type_presta; } + public function getAllWithWindowFields(): array // différent de Model::getAll() qui retourne get_object_vars($this) + { + $code_presta_tableau = explode('-', $this->code_presta); + $Date = new Dates($this->date); + + return [ + "Numéro prestation:" => end($code_presta_tableau), // dernière case + "Date:" => $Date->getDate(), + "Type de Presta:" => $this->type_presta, + "Mode de paiement:" => $this->mode_paiement, + "Commentaires:" => $this->commentaires]; + } // setters //~ public function setID() -> dans le trait ModelChildren @@ -81,7 +101,7 @@ class Prestations extends Model return $this; } - // code client = année-mois-jour-codeclient-combientièmefois + // code client = année-mois-jour-codeclient-typedepresta-combientièmefois public function makeCodePresta(Dates $Date, string $code_client) { // on récupère un tableau contenant toutes les prestations d'un client tous types confondus (devis, facture, cesu, location, enregistrement sans vente) diff --git a/src/model/StructTablesDB.php b/src/model/StructTablesDB.php index 769d502..679adde 100644 --- a/src/model/StructTablesDB.php +++ b/src/model/StructTablesDB.php @@ -11,7 +11,7 @@ class StructTablesDB // les tables devis_factures, cesu et locations sont liées à la table prestations 'clients' => ['ID' => 'INTEGER', 'prenom_nom' => 'TEXT', 'code_client' => 'TEXT', 'adresse' => 'TEXT', 'code_postal' => 'TEXT', 'ville' => 'TEXT', 'telephone' => 'TEXT', 'courriel' => 'TEXT', 'apropos' => 'TEXT', 'type' => 'TEXT DEFAULT prospect'], 'prestations' => ['ID' => 'INTEGER', 'ID_client' => 'INTEGER', 'code_presta' => 'TEXT', 'date' => 'INTEGER', 'type_presta' => 'TEXT', 'mode_paiement' => 'TEXT', 'commentaires' => 'TEXT'], - 'devis' => ['ID' => 'INTEGER', 'ID_presta' => 'INTEGER', 'taches' => 'TEXT', 'total_main_d_oeuvre' => 'REAL', 'pieces' => 'TEXT', 'total_pieces' => 'REAL', 'deplacement' => 'REAL', 'total_HT' => 'REAL', 'delai_livraison' => 'TEXT', 'validite_devis' => 'TEXT', 'signature_devis' => 'TEXT'], + 'devis' => ['ID' => 'INTEGER', 'ID_presta' => 'INTEGER', 'taches' => 'TEXT', 'total_main_d_oeuvre' => 'REAL', 'pieces' => 'TEXT', 'total_pieces' => 'REAL', 'deplacement' => 'REAL', 'total_HT' => 'REAL', 'delai_livraison' => 'TEXT', 'validite_devis' => 'TEXT', 'signature_devis' => 'TEXT DEFAULT non'], 'factures' => ['ID' => 'INTEGER', 'ID_presta' => 'INTEGER', 'taches' => 'TEXT', 'machine' => 'TEXT', 'OS' => 'TEXT', 'donnees' => 'TEXT', 'cles_licences' => 'TEXT', 'total_main_d_oeuvre' => 'REAL', 'pieces' => 'TEXT', 'total_pieces' => 'REAL', 'deplacement' => 'REAL', 'total_HT' => 'REAL'], 'cesu' => ['ID' => 'INTEGER', 'ID_presta' => 'INTEGER', 'taches' => 'TEXT', 'duree_travail' => 'TEXT', 'salaire' => 'REAL'], 'locations' => ['ID' => 'INTEGER', 'ID_presta' => 'INTEGER', 'designation' => 'TEXT', 'modele_description' => 'TEXT', 'valeur' => 'REAL', 'etat_des_lieux_debut' => 'TEXT', 'etat_des_lieux_fin' => 'TEXT', 'duree_location' => 'TEXT', 'loyer_mensuel' => 'REAL', 'loyers_payes' => 'INTEGER', 'caution' => 'INTEGER'] diff --git a/src/model/traits.php b/src/model/traits.php deleted file mode 100644 index 88451d9..0000000 --- a/src/model/traits.php +++ /dev/null @@ -1,13 +0,0 @@ -table = strtolower(__CLASS__); - //~ echo "TABLE = " . $this->table . "\n"; - //~ } -} diff --git a/src/sections/1_customer.php b/src/sections/1_customer.php index cdec448..ca20d08 100644 --- a/src/sections/1_customer.php +++ b/src/sections/1_customer.php @@ -12,29 +12,36 @@ function newCustomer(): array // -- partie 1: client ou prospect? -- $Client = new Clients; - if(exec($TypeDeClient->get()) === 'Client') + $choix_niv2 = exec($TypeDeClient->get()); + if($choix_niv2 === ZenitySetup::$type_client_entrees[0]) { + echo "choix: " . $choix_niv2 . "\n"; $Client->setType('client'); } - //~ else // - //~ { - //~ $Client->setType('prospect'); - //~ } + elseif($choix_niv2 === ZenitySetup::$type_client_entrees[1]) + { + echo "choix: " . $choix_niv2 . "\n"; + $Client->setType('prospect'); + } + else + { + return [0, null]; // menu principal + } // -- partie 2: saisie des infos -- if(enterCustomer($Client)) { // -- partie 3: on fait quoi maintenant -- - $choix_niv2 = exec($FinSection1->get()); - if($choix_niv2 === ZenitySetup::$fin_section_1_entrees[0]) + $choix_niv3 = exec($FinSection1->get()); + if($choix_niv3 === ZenitySetup::$fin_section_1_entrees[0]) { return [2, $Client]; // section 2: newService() } - elseif($choix_niv2 === ZenitySetup::$fin_section_1_entrees[1]) + elseif($choix_niv3 === ZenitySetup::$fin_section_1_entrees[1]) { return [3, $Client]; // section 3: modifyData() } - elseif($choix_niv2 === ZenitySetup::$fin_section_1_entrees[2]) + elseif($choix_niv3 === ZenitySetup::$fin_section_1_entrees[2]) { return [1, null]; // relancer section 1: newCustomer() } diff --git a/src/sections/2_service.php b/src/sections/2_service.php index 8109b31..2680db6 100644 --- a/src/sections/2_service.php +++ b/src/sections/2_service.php @@ -13,7 +13,7 @@ function newService($Client): array // $Client est un Client ou null $FormulaireCesu = new ZenityForms(ZenitySetup::$formulaire_text, ZenitySetup::$formulaire_cesu_entrees); $FormulaireLocation = new ZenityForms(ZenitySetup::$formulaire_text, ZenitySetup::$formulaire_location_entrees); $CommentairePrestation = new ZenityEntry(ZenitySetup::$commentaire_prestation_text); - $Recapitulatif = new ZenityList(ZenitySetup::$recapitulatif_text, ZenitySetup::$recapitulatif_entrees); // tableau à multiples colonnes + //$Recapitulatif = new ZenityList(ZenitySetup::$recapitulatif_text, ZenitySetup::$recapitulatif_entrees); // tableau à multiples colonnes //$QuestionModifierPrestation = new ZenityQuestion(ZenitySetup::$question_modification_text); $FinSection2 = new ZenityList(ZenitySetup::$fin_section_2_text, ZenitySetup::$fin_section_2_entrees); @@ -32,7 +32,7 @@ function newService($Client): array // $Client est un Client ou null // -- partie 2: la prestation -- - // niveau 2: type comptable d'enregistrement: devis, facture, cesu, location ou pas de prestation + // niveau 2: type comptable d'enregistrement: devis, facture, cesu, location ou prestation non vendue $choix_niv2 = exec($MenuEnregistrement->get()); if($choix_niv2 === '') { @@ -40,9 +40,6 @@ function newService($Client): array // $Client est un Client ou null return [0, null]; // menu principal } - $Presta = new Prestations($Client->getID()); - - // détail de la prestation // calendrier - étape 1/3 $Date = new Dates(exec($Calendrier->get())); // exec() renvoie soit une chaîne soit un false if($Date->getDate() == '') // on n'a pas cliqué sur "annuler" @@ -51,32 +48,56 @@ function newService($Client): array // $Client est un Client ou null return [0, null]; // menu principal } + $Presta = new Prestations($Client->getID()); $Presta->setDate($Date->getTimestamp()); // un entier pour la BDD + // formulaire - étape 2/3 switch($choix_niv2) { - // formulaire - étape 2/3 - //~ case ZenitySetup::$menu_enregistrement_entrees[0]: // "Devis" - //~ $PrestaDetails = new DevisFactures('devis'); - //~ $Presta->setTypePresta('devis'); - //~ $input = exec($FormulaireDevis->get()); - //~ break; - case ZenitySetup::$menu_enregistrement_entrees[0]: // "Facture" - $PrestaDetails = new DevisFactures('factures'); // 'factures' est le nom de la table, pas le type de presta + // comparaison retour de $MenuEnregistrement->get() avec les noms des entrées du même menu + case ZenitySetup::$menu_enregistrement_entrees[0]: // "Devis" + $PrestaDetails = new DevisFactures('devis'); + $Presta->setTypePresta('devis'); + $input = exec($FormulaireDevis->get()); + break; + case ZenitySetup::$menu_enregistrement_entrees[1]: // "Facture" + $PrestaDetails = new DevisFactures('factures'); $Presta->setTypePresta('facture'); $input =exec($FormulaireFacture->get()); break; - case ZenitySetup::$menu_enregistrement_entrees[1]: // "CESU" + + //~ case ZenitySetup::$menu_enregistrement_entrees[2]: // "Facture à partir d'un devis" + //~ $PrestaDetails = new DevisFactures('facture'); + //~ $Presta->setTypePresta('facture'); + // 1 choix d'un devis + //makeObjectService(); // liste les devis existants pour ce client + // + // 2 hydratation + // + // 3 préremplissage du formulaire + //$FormulaireFacture->set($Data) + // + // et plus loin: + // 4 édition de la facture + //$input = exec($FormulaireFacture->get()); + // + // 5 hydratation avec les changements + // + //echo $FormulaireFacture->get() . "\n"; + //~ $input = exec($FormulaireFacture->get()); + //~ break; + + case ZenitySetup::$menu_enregistrement_entrees[3]: // "CESU" $PrestaDetails = new CESU(); $Presta->setTypePresta('cesu'); $input = exec($FormulaireCesu->get()); break; - case ZenitySetup::$menu_enregistrement_entrees[2]: // "Location" + case ZenitySetup::$menu_enregistrement_entrees[4]: // "Location" $PrestaDetails = new Locations(); $Presta->setTypePresta('location'); $input = exec($FormulaireLocation->get()); break; - case ZenitySetup::$menu_enregistrement_entrees[3]: // "Prestation non vendue" + case ZenitySetup::$menu_enregistrement_entrees[5]: // "Prestation non vendue" $Presta->setTypePresta('non_vendue'); break; default: // inutile normallement, cas déjà géré avant @@ -86,7 +107,8 @@ function newService($Client): array // $Client est un Client ou null $Presta->makeCodePresta($Date, $Client->getCodeClient()); // d'un objet à l'autre - if($choix_niv2 != ZenitySetup::$menu_enregistrement_entrees[3]) // si presta non vendue, saut étape 3/3 + // hydratation ou saut étape 3/3 si presta non vendu + if($choix_niv2 != ZenitySetup::$menu_enregistrement_entrees[5]) { if($input == '') // annulation { @@ -154,9 +176,9 @@ function newService($Client): array // $Client est un Client ou null //~ {} // fabrique d'objets (sans connaître les noms des classes) - $EnveloppeRecto = Latex::makeLatexSubClass('enveloppe_recto'); - $EnveloppeVerso = Latex::makeLatexSubClass('enveloppe_verso'); - $DocumentPresta = Latex::makeLatexSubClass($Presta->getTypePresta()); // $type = facture, devis, location + $EnveloppeRecto = Latex::makeInstance('enveloppe_recto'); + $EnveloppeVerso = Latex::makeInstance('enveloppe_verso'); + $DocumentPresta = Latex::makeInstance($Presta->getTypePresta()); // $type = facture, devis, location // génération du latex $EnveloppeRecto->setData($Client); diff --git a/src/sections/3_modify_data.php b/src/sections/3_modify_data.php index cc95468..e16a58f 100644 --- a/src/sections/3_modify_data.php +++ b/src/sections/3_modify_data.php @@ -3,7 +3,175 @@ // // -- SECTION 3: Modifier un client, un prospect, une prestation, un devis -- -function modifyData($Objet): array +function modifyData($Client): array { + // -- partie 1: rechercher un client -- + if($Client == null || get_class($Client) !== 'Clients') // étape sautable + { + $Client = makeObjectClient(); // = 0 ou type "Clients" + if(!is_object($Client) || !get_class($Client) == 'Clients') + { + echo "debug: annulation sélection client\n"; + return [0, null]; // menu principal + } + } + + + // -- partie 2: modifier un client -- + + // fenêtre $ModificationClient + $ModificationClientMenu = new ZenityList(ZenitySetup::$modification_client['text'], []); + $entrees = []; + $i = 0; + $client_data = $Client->getAllWithWindowFields(); + //var_dump($client_data); + foreach($client_data as $key => $value) + { + $entrees[$i][] = $key; + $entrees[$i][] = $value; + $i++; + } + $entrees[$i][] = ZenitySetup::$modification_client['service']; + $entrees[$i][] = ''; + $i++; + $entrees[$i][] = ZenitySetup::$modification_client['return']; + $entrees[$i][] = ''; + $ModificationClientMenu->setListRows($entrees, 2, 2.5); + + // modifier une valeur + $choix_niv2 = exec($ModificationClientMenu->get()); + if($choix_niv2 === ZenitySetup::$modification_client['service']) // ne pas modifier le client mais une prestation + { + echo "choix: modifier une prestation\n"; + // on passe à la suite + } + elseif($choix_niv2 === ZenitySetup::$modification_client['return']) // annuler + { + echo "choix: retour au menu principal\n"; + return [0, null]; // menu principal + } + elseif($choix_niv2 === "Client ou Prospect?") // modifier le client + { + echo "choix: modifier" . $choix_niv2 . "\n"; + $TypeDeClient = new ZenityList(ZenitySetup::$type_client_text, ZenitySetup::$type_client_entrees); + $input = exec($TypeDeClient->get()); + if($input === ZenitySetup::$type_client_entrees[0]) + { + echo "choix: " . $input . "\n"; + $Client->setType('client'); + } + elseif($input === ZenitySetup::$type_client_entrees[1]) + { + echo "choix: " . $input . "\n"; + $Client->setType('prospect'); + } + else + { + echo "choix: annulation\n"; + } + $Client->update(); + return [3, $Client]; // menu précédent + } + elseif(is_string($choix_niv2) && $choix_niv2 != '') // modifier le client + { + echo "choix: modifier" . $choix_niv2 . "\n"; + $ModificationClient = new ZenityEntry($choix_niv2); + $input = exec($ModificationClient->get()); + if(is_string($input) && $input != '') + { + $Client->getSetterAndSet($choix_niv2, $input); + $Client->update(); + + // mettre à jour les documents + + } + else + { + echo "choix: annulation\n"; + } + return [3, $Client]; // menu précédent + } + else // annuler + { + echo "annulation: retour au menu principal\n"; + return [0, null]; // menu principal + } + + + // -- partie 3: rechercher une prestation -- + $Presta = getServices($Client); // = 0 ou type "Prestations" + if(!is_object($Presta) || !get_class($Presta) == 'Prestations') + { + echo "debug: annulation sélection client\n"; + return [3, $Client]; // menu précédent + } + + + // -- partie 4: modifier une prestation -- + + // fenêtre $ModificationPresta + $ModificationPrestaMenu = new ZenityList(ZenitySetup::$modification_presta['text'], []); + $entrees = []; + $i = 0; + + $presta_data = $Presta->getAllWithWindowFields(); + var_dump($presta_data); + foreach($presta_data as $key => $value) + { + $entrees[$i][] = $key; + $entrees[$i][] = $value; + $i++; + } + + // infos des sous-tables 'facture', 'devis', etc + switch($Presta->getTypePresta()) + { + case 'facture': + $PrestaDetails = new DevisFactures('factures'); + break; + + case 'devis': + $PrestaDetails = new DevisFactures('devis'); + break; + case 'cesu': + $PrestaDetails = new CESU(); + break; + case 'location': + $PrestaDetails = new Locations(); + break; + } + if(isset($PrestaDetails)) + { + $PrestaDetails->setIDPresta($Presta->getID()); + $PrestaDetails->hydrate($PrestaDetails->getDetailsByIdPresta()); + $presta_data = $PrestaDetails->getAllWithWindowFields(); + var_dump($presta_data); + foreach($presta_data as $key => $value) + { + $entrees[$i][] = $key; + $entrees[$i][] = $value; + $i++; + } + } + + if($Presta->getTypePresta() === 'devis') + { + $entrees[$i][] = ZenitySetup::$modification_presta['devis_facture']; // option changer le devis en facture + $entrees[$i][] = ''; + $i++; + } + $entrees[$i][] = ZenitySetup::$modification_presta['service']; + $entrees[$i][] = ''; + $i++; + $entrees[$i][] = ZenitySetup::$modification_presta['return']; + $entrees[$i][] = ''; + $ModificationPrestaMenu->setListRows($entrees, 2, 2.5); + + // modifier une valeur + $choix_niv3 = exec($ModificationPrestaMenu->get()); + var_dump($choix_niv3); + + // si changement de type de prestation autre que de devis à facture, ça devient compliqué! + return [0, null]; // menu principal } diff --git a/src/sections/4_get_document.php b/src/sections/4_get_document.php index e28e526..baad117 100644 --- a/src/sections/4_get_document.php +++ b/src/sections/4_get_document.php @@ -3,7 +3,7 @@ // // -- SECTION 4: Afficher, imprimer un document -- -function getDocument($Objet): array +function getOrPrintDocument(): array { $MenuDocuments = new ZenityList(ZenitySetup::$menu_documents_text, ZenitySetup::$menu_documents_entrees); diff --git a/src/sections/main_loop.php b/src/sections/main_loop.php index 7dd1af0..f68c251 100644 --- a/src/sections/main_loop.php +++ b/src/sections/main_loop.php @@ -6,14 +6,15 @@ require('sections/1_customer.php'); require('sections/2_service.php'); require('sections/3_modify_data.php'); +require('sections/4_get_document.php'); $main_loop = true; -$returned = [0, null]; // [code de retour, éventuelles données] +$section = [0, null]; // [code de retour, éventuelles données] while($main_loop) { // -- MENU PRINCIPAL (niveau 1) -- - if($returned[0] === 0) + if($section[0] === 0) { echo("Menu principal\n"); $MenuPrincipal = new ZenityList(ZenitySetup::$menu_principal_text, ZenitySetup::$menu_principal_entrees); @@ -24,44 +25,45 @@ while($main_loop) $choix_niv1 = ''; } - + // comparaison du retour de $MenuPrincipal->get() avec les noms des entrées du même menu + // -- SECTION 1: Clients et prospects -- - if($choix_niv1 === ZenitySetup::$menu_principal_entrees[0] || $returned[0] === 1) + if($choix_niv1 === ZenitySetup::$menu_principal_entrees[0] || $section[0] === 1) { echo("choix: ". ZenitySetup::$menu_principal_entrees[0] . "\n"); - $returned = newCustomer(); + $section = newCustomer(); } // -- SECTION 2: Prestations et devis -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[1] || $returned[0] === 2) + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[1] || $section[0] === 2) { echo("choix: ". ZenitySetup::$menu_principal_entrees[1] . "\n"); - $returned = newService($returned[1]); // $returned[1] vaut un type Clients ou null + $section = newService($section[1]); // $section[1] vaut un type Clients ou null } // -- SECTION 3: Modifier un enregistrement -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[2] || $returned[0] === 3) + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[2] || $section[0] === 3) { echo("choix: " . ZenitySetup::$menu_principal_entrees[2] . "\n"); - $returned = modifyData(); + $section = modifyData($section[1]); // $section[1] vaut un type Clients ou null } // -- SECTION 4: Consulter, Imprimer un document -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[3] || $returned[0] === 4) // = Imprimer un document + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[3] || $section[0] === 4) // = Imprimer un document { echo("choix: ". ZenitySetup::$menu_principal_entrees[3] . "\n"); - $returned = getDocument(); + $section = getOrPrintDocument(); } // -- SECTION 5: Consulter/analyser les données -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[4] || $returned[0] === 5) + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[4] || $section[0] === 5) { echo("choix: ". ZenitySetup::$menu_principal_entrees[4] . "\n"); // quel affichage? des tableaux avec zenity? LaTeX? une page web? un autre outil servant à faire des tableaux et graphiques } // -- SECTION 6: Supports de communication -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[5] || $returned[0] === 6) // = Communication + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[5] || $section[0] === 6) // = Communication { echo("choix: ". ZenitySetup::$menu_principal_entrees[5] . "\n"); $MenuCommunication = new ZenityList(ZenitySetup::$menu_communication_text, ZenitySetup::$menu_communication_entrees); @@ -85,7 +87,7 @@ while($main_loop) } // -- SECTION 7: BDD -- - elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[6] || $returned[0] === 7) // = Base de données + elseif($choix_niv1 === ZenitySetup::$menu_principal_entrees[6] || $section[0] === 7) // = Base de données { echo("choix: ". ZenitySetup::$menu_principal_entrees[6] . "\n"); if($sqlitebrowser_enable) diff --git a/src/view/Zenity.php b/src/view/Zenity.php index 73c8c3e..858dfa9 100644 --- a/src/view/Zenity.php +++ b/src/view/Zenity.php @@ -26,6 +26,11 @@ abstract class ZenityCmd { return($this->command); } + // pré-remlir un formulaire zenity --forms + //~ public function formSet() + //~ { + //~ return($this->command); + //~ } } @@ -37,7 +42,7 @@ class ZenityList extends ZenityCmd { $this->command_type = ' --list'; parent::__construct($text, $rows); - $this->columns = 1; + //$this->columns = 1; $this->command .= ' --hide-header'; // ligne inutile, il y a déjà le --text if($this->rows !== []) { @@ -48,25 +53,29 @@ class ZenityList extends ZenityCmd } } - // cas ou $this->rows est renseignée après - // la valeur de $columns n'est plus nécessairement celle apr défaut - public function setListRows(array $rows, string $table) + // cas ou $this->rows n'est pas renseignée dans le constructeur + // la valeur de $columns n'est plus nécessairement celle par défaut + public function setListRows(array $rows, int $columns, float $widen = 1) { + // si 1 colonne, $rows = tableau à une dimension + // si 2 colonnes ou plus, $rows = tableau à deux dimensions $this->rows = $rows; - $this->columns = count(StructTablesDB::$structureOfTables[$table]); - $this->width = 800; + $this->columns = $columns; + + $this->width = $this->columns * 120 * $widen; + $this->height = 91 + count($this->rows) * 25; // inclut les lignes et la barre de défilement horizontale + $this->command .= ' --width=' . $this->width; - $this->height = 80 + count($this->rows) * 25; $this->command .= ' --height=' . $this->height; $this->fillZenityList(); } - // noter que la syntaxe de zentity --list est déroutante! + // noter que la syntaxe de zenity --list est déroutante! // le remplissage est horizontal et le nombre de colonne dépend du nombre d'occurence de --column="" public function fillZenityList() { $output = ''; - if($this->columns === 1) + if($this->columns === 1) // $this->rows doit être un tableau { $output .= ' --column=""'; // remplissage vertical @@ -75,7 +84,8 @@ class ZenityList extends ZenityCmd $output .= ' "' . $one_row . '"'; // forme: ' "choix 1" "choix 2"' } } - elseif($this->columns >= 2) // marche quelque soit le nombre de colonnes + elseif($this->columns >= 2) // $this->rows doit être un tableau à deux dimensions + // marche quelque soit le nombre de colonnes { for($i = 0; $i < $this->columns; $i++) { diff --git a/src/view/ZenitySetup.php b/src/view/ZenitySetup.php index d0b785c..32648ea 100644 --- a/src/view/ZenitySetup.php +++ b/src/view/ZenitySetup.php @@ -1,10 +1,14 @@ 'Modifier une information concernant un client', + //'lignes' => ["Prénom Nom:", "Code client (J.C.Dusse):", "Adresse:", "Code postal:", "Ville:", "Telephone:", "Courriel:", "À propos:", "Client ou Prospect?"], // inutile, obtenu par $Client->getAll() + 'service' => "Modifier une prestation", + 'return' => "Retour menu principal"]; + static public $modification_presta = ['text' => 'Modifier une prestation', + 'devis_facture' => "Changer ce devis en facture", + 'service' => "Modifier une autre prestation", + 'return' => "Retour menu précédent"]; + + //~ static public $recapitulatif_text = "voici toutes les informations enregistrées"; + //~ static public $recapitulatif_entrees = []; + //static public $question_modification_text = "Prestation enregistrée. Modifier les informations?"; static public $fin_section_1_text = "Client enregistré"; -- cgit v1.2.3