diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 8567c28cf..a50bfad9c 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -901,12 +901,12 @@ paths: security: - api_key: [] - user_id: [] - /project: - get: - summary: Metadata about all projects + /project: + get: + summary: Metadata about all projects (with Maitrise entries) tags: - "Project" - description: 'Returns the metadata about projects in the system, but not members of the projects.' + description: 'Returns the metadata about projects in the system, including Maitrise (type, bureau, email) entries for each project.' produces: - "application/json" operationId: "GetProjectList" @@ -917,8 +917,8 @@ paths: description: "successful operation" schema: type: "array" - items: - $ref: "#/definitions/Project" + items: + $ref: "#/definitions/Project" security: - api_key: [] - user_id: [] @@ -1063,7 +1063,7 @@ paths: security: - api_key: [] - user_id: [] -definitions: +definitions: Audit: type: "object" properties: @@ -1617,27 +1617,43 @@ definitions: Disabled: type: boolean format: "User has been disabled" - Project: - type: "object" - properties: - ProjectID: - type: "integer" - format: "keyValue" - ProjectName: - type: "string" - format: "Freeform text" - ProjectSponsor: - type: "string" - format: "Freeform text" - ProjectStartDate: - type: "string" - format: "Datestamp" - ProjectEndDate: - type: "string" - format: "Datestamp" - ProjectActualEndDate: - type: "string" - format: "Datestamp" + Project: + type: "object" + properties: + ProjectID: + type: "integer" + format: "keyValue" + ProjectName: + type: "string" + format: "Freeform text" + ProjectSponsor: + type: "string" + format: "Freeform text" + ProjectStartDate: + type: "string" + format: "Datestamp" + ProjectEndDate: + type: "string" + format: "Datestamp" + ProjectActualEndDate: + type: "string" + format: "Datestamp" + Maitrise: + type: "array" + items: + $ref: "#/definitions/ProjectMaitrise" + ProjectMaitrise: + type: "object" + properties: + MaitriseName: + type: "string" + format: "Freeform text" + BureauName: + type: "string" + format: "Freeform text" + BureauEmail: + type: "string" + format: "EmailAddress" PowerPort: type: "object" properties: diff --git a/api/v1/getRoutes.php b/api/v1/getRoutes.php index 5d0b22043..30be21e8c 100644 --- a/api/v1/getRoutes.php +++ b/api/v1/getRoutes.php @@ -706,11 +706,47 @@ // $app->get( '/project', function(Request $request, Response $response) { - $r['error']=false; - $r['errorcode']=200; - $r['project']=Projects::getProjectList(); + $r['error']=false; + $r['errorcode']=200; + + $list = Projects::getProjectList(); + + // Fetch Maitrise rows for each project + global $dbh; + $st = null; + try{ + $st = $dbh->prepare("SELECT mt.MaitriseName, pm.BureauName, pm.BureauEmail + FROM fac_ProjectMaitrise pm + LEFT JOIN fac_MaitriseType mt ON mt.MaitriseTypeID = pm.MaitriseTypeID + WHERE pm.ProjectID = :pid + ORDER BY pm.ProjectMaitriseID ASC"); + }catch(Exception $e){ + $st = null; // If tables don't exist, just omit maitrise info + } + + $projects = array(); + foreach($list as $p){ + $tmp = array(); + foreach($p as $prop=>$val){ + $tmp[$prop] = $val; + } + // Attach maitrise info if available + $tmp['Maitrise'] = array(); + if($st){ + try{ + $st->execute(array(':pid'=>$p->ProjectID)); + $tmp['Maitrise'] = $st->fetchAll(PDO::FETCH_ASSOC); + }catch(Exception $e){ + // ignore errors and leave Maitrise empty + $tmp['Maitrise'] = array(); + } + } + $projects[] = $tmp; + } + + $r['project'] = $projects; - return $response->withJson( $r, $r['errorcode'] ); + return $response->withJson( $r, $r['errorcode'] ); }); // diff --git a/classes/MaitriseType.class.php b/classes/MaitriseType.class.php new file mode 100644 index 000000000..e929c4dc3 --- /dev/null +++ b/classes/MaitriseType.class.php @@ -0,0 +1,36 @@ +prepare("SELECT * FROM fac_MaitriseType ORDER BY MaitriseName ASC;"); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_CLASS,"MaitriseType"); + } + + static function Insert($name){ + global $dbh; + $stmt=$dbh->prepare("INSERT INTO fac_MaitriseType SET MaitriseName=:name;"); + return $stmt->execute(array(":name"=>$name)); + } + + static function Delete($id){ + global $dbh; + $stmt=$dbh->prepare("DELETE FROM fac_MaitriseType WHERE MaitriseTypeID=:id;"); + return $stmt->execute(array(":id"=>$id)); + } + + static function Update($id,$name){ + global $dbh; + $stmt=$dbh->prepare("UPDATE fac_MaitriseType SET MaitriseName=:name WHERE MaitriseTypeID=:id;"); + return $stmt->execute(array(":id"=>intval($id), ":name"=>$name)); + } +} +?> diff --git a/create.sql b/create.sql index 3144611dc..5ddc1c6a2 100644 --- a/create.sql +++ b/create.sql @@ -1111,6 +1111,32 @@ CREATE TABLE fac_ProjectMembership ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- +-- Maitrise types and project linkage +-- + +DROP TABLE IF EXISTS fac_MaitriseType; +CREATE TABLE fac_MaitriseType ( + MaitriseTypeID INT AUTO_INCREMENT PRIMARY KEY, + MaitriseName VARCHAR(100) UNIQUE NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO fac_MaitriseType (MaitriseName) +VALUES +('CUSTOMER'), +('PROD ES'); + +DROP TABLE IF EXISTS fac_ProjectMaitrise; +CREATE TABLE fac_ProjectMaitrise ( + ProjectMaitriseID INT AUTO_INCREMENT PRIMARY KEY, + ProjectID INT NOT NULL, + MaitriseTypeID INT NOT NULL, + BureauName VARCHAR(100) NOT NULL, + BureauEmail VARCHAR(255) NULL DEFAULT NULL, + KEY (ProjectID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- -- Tables for tracking how things leave -- diff --git a/css/inventory.php b/css/inventory.php index 0b4dd74c7..87bed2deb 100644 --- a/css/inventory.php +++ b/css/inventory.php @@ -520,6 +520,16 @@ padding: 10px; margin-bottom: 8px; } + +/* Maitrise Types modal spacing */ +#maitrisetypesmodal { padding: 10px; } +#maitrisetypesmodal table { border-collapse: separate; border-spacing: 0 8px; } +#maitrisetypesmodal th, #maitrisetypesmodal td { padding: 6px 10px; vertical-align: middle; } +#maitrisetypesmodal input[type="text"], +#maitrisetypesmodal input[type="email"], +#maitrisetypesmodal select { padding: 6px 8px; margin: 4px 6px 4px 0; } +#maitrisetypesmodal button { margin: 4px 6px; } +#maitrisetypesmodal .note { margin-top: 10px; display: block; } #infopanel fieldset button, #infopanel fieldset input[type=submit], #infopanel fieldset input[type=button],.reports fieldset button, .reports fieldset input[type=submit], .reports fieldset input[type=button] {width: 100%;} #infopanel legend, .device legend, .reports legend {border: 1px ParameterArray['HeaderColor']; ?> solid;background-color: white;} div.cabinet { @@ -1131,6 +1141,11 @@ #pandn.table .custom-combobox a {margin: 0; vertical-align: top; border-left: 0px; border-right: 2px; position: absolute;} div#pandn.table div[id^="ppn"] { min-width: 200px; } +/* Project Manager: align combobox toggle with input */ +#projectid + span.custom-combobox { position: relative; display: inline-block; width: 100%; vertical-align: middle; } +#projectid + span.custom-combobox input { width: calc(100% - 24px) !important; } +#projectid + span.custom-combobox a.custom-combobox-toggle { position: absolute; right: 0; top: 0; bottom: 0; width: 24px; padding: 0; } + #olog > div:first-child { border-bottom: 2px solid black; } #olog > div > div:first-child { width: 100px; padding-right: 5px; white-space: nowrap; } #olog > div:first-child > div:first-child { border-right: 0 none; } diff --git a/db-23.04-to-25.01.sql b/db-23.04-to-25.01.sql index 6f1c63bcc..60a81bedd 100644 --- a/db-23.04-to-25.01.sql +++ b/db-23.04-to-25.01.sql @@ -111,4 +111,29 @@ ALTER TABLE fac_TemplatePowerPorts ADD COLUMN ConnectorID int(11) DEFAULT NULL A ALTER TABLE fac_TemplatePowerPorts ADD COLUMN PhaseID int(11) DEFAULT NULL AFTER ConnectorID; ALTER TABLE fac_TemplatePowerPorts ADD COLUMN VoltageID int(11) DEFAULT NULL AFTER PhaseID; +-- +-- Maitrise types and project linkage +-- + +DROP TABLE IF EXISTS fac_MaitriseType; +CREATE TABLE fac_MaitriseType ( + MaitriseTypeID INT AUTO_INCREMENT PRIMARY KEY, + MaitriseName VARCHAR(100) UNIQUE NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO fac_MaitriseType (MaitriseName) +VALUES +('CUSTOMER'), +('PROD ES'); + +DROP TABLE IF EXISTS fac_ProjectMaitrise; +CREATE TABLE fac_ProjectMaitrise ( + ProjectMaitriseID INT AUTO_INCREMENT PRIMARY KEY, + ProjectID INT NOT NULL, + MaitriseTypeID INT NOT NULL, + BureauName VARCHAR(100) NOT NULL, + BureauEmail VARCHAR(255) NULL DEFAULT NULL, + KEY (ProjectID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + UPDATE fac_Config set Value="25.01" WHERE Parameter="Version"; diff --git a/project_mgr.php b/project_mgr.php index 3257d211f..0b086662d 100644 --- a/project_mgr.php +++ b/project_mgr.php @@ -1,6 +1,7 @@ ContactAdmin){ + if($_POST['mt_action']==='AddType' && isset($_POST['newmaitrise'])){ + $name=trim($_POST['newmaitrise']); + if($name!==''){ + MaitriseType::Insert($name); + } + } + if($_POST['mt_action']==='DeleteType' && isset($_POST['deletetypeid'])){ + $id=intval($_POST['deletetypeid']); + if($id>0){ + MaitriseType::Delete($id); + } + } + if($_POST['mt_action']==='UpdateType' && isset($_POST['updatetypeid'])){ + $id=intval($_POST['updatetypeid']); + $name=isset($_POST['updatename'])?trim($_POST['updatename']):''; + if($id>0 && $name!==''){ + MaitriseType::Update($id,$name); + } + } + // Refresh to avoid resubmission + header('Location: '.redirect('project_mgr.php'.(isset($_REQUEST['projectid'])?'?projectid='.(int)$_REQUEST['projectid']:''))); + exit; + } + if(isset($_REQUEST['projectid'])&&($_REQUEST['projectid']>0)){ $project->ProjectID=(isset($_POST['projectid']) ? $_POST['projectid'] : $_GET['projectid']); $project = Projects::getProject( $project->ProjectID ); @@ -40,11 +70,16 @@ if($project->ProjectName!=''){ if($_POST['action']=='Create'){ $project->createProject(); - + // Save Maitrise rows for the new project + if($project->ProjectID>0){ + saveProjectMaitriseRows($project->ProjectID); + } header('Location: '.redirect("project_mgr.php?projectid=$project->ProjectID")); exit; }else{ $project->updateProject(); + // Save Maitrise rows for existing project + saveProjectMaitriseRows($project->ProjectID); } } // Refresh object @@ -53,7 +88,92 @@ $projectList=Projects::getProjectList(); + // Helper to check if a table exists + function tableExists($name){ + global $dbh; + try{ + $st=$dbh->prepare("SHOW TABLES LIKE :t"); + $st->execute(array(":t"=>$name)); + return ($st->rowCount()>0); + }catch(Exception $e){ return false; } + } + + // Fetch Maitrise types and existing rows for this project + $maitriseTypes = array(); + if(function_exists('tableExists') && tableExists('fac_MaitriseType')){ + $maitriseTypes = MaitriseType::GetAll(); + } + $projectMaitriseRows = array(); + if(isset($project->ProjectID) && $project->ProjectID>0 && tableExists('fac_ProjectMaitrise')){ + $st=$dbh->prepare("SELECT pm.ProjectMaitriseID, pm.MaitriseTypeID, pm.BureauName, pm.BureauEmail FROM fac_ProjectMaitrise pm WHERE pm.ProjectID=:pid ORDER BY pm.ProjectMaitriseID ASC"); + $st->execute(array(":pid"=>$project->ProjectID)); + $projectMaitriseRows=$st->fetchAll(PDO::FETCH_ASSOC); + } + + // Helper function to save Maitrise rows + function saveProjectMaitriseRows($projectID){ + global $dbh; + global $maitriseEmailWarnings; + // Ensure required table exists; if not, skip gracefully + try{ + $chk=$dbh->query("SHOW TABLES LIKE 'fac_ProjectMaitrise'"); + if(!$chk || $chk->rowCount()==0){ + return; // table not present yet + } + }catch(Exception $ex){ + return; + } + $types = isset($_POST['maitrise_type']) && is_array($_POST['maitrise_type']) ? $_POST['maitrise_type'] : array(); + $names = isset($_POST['maitrise_bureau']) && is_array($_POST['maitrise_bureau']) ? $_POST['maitrise_bureau'] : array(); + $emails = isset($_POST['maitrise_email']) && is_array($_POST['maitrise_email']) ? $_POST['maitrise_email'] : array(); + // Clear existing + $del=$dbh->prepare("DELETE FROM fac_ProjectMaitrise WHERE ProjectID=:pid"); + $del->execute(array(":pid"=>$projectID)); + // Insert new + $ins=$dbh->prepare("INSERT INTO fac_ProjectMaitrise SET ProjectID=:pid, MaitriseTypeID=:mtid, BureauName=:bname, BureauEmail=:bemail"); + for($i=0;$i0 && $bname!==''){ + $bemailParam = null; + if($bemail!==''){ + $valid = false; + if(function_exists('filter_var') && defined('FILTER_VALIDATE_EMAIL')){ + $valid = (bool)filter_var($bemail, FILTER_VALIDATE_EMAIL); + }else{ + $valid = (bool)preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $bemail); + } + if($valid){ + $bemailParam = $bemail; + }else{ + $maitriseEmailWarnings[] = array('email'=>$bemail,'name'=>$bname); + $bemailParam = null; + } + } + try{ + $ins->execute(array(":pid"=>$projectID, ":mtid"=>$mtid, ":bname"=>$bname, ":bemail"=>$bemailParam)); + }catch(Exception $ex){ + $maitriseEmailWarnings[] = array('email'=>$bemail,'name'=>$bname); + } + } + } + } + $title = __("openDCIM Project Information" ); + + // Prepare warnings HTML to be injected in form output + $maitriseEmailWarningsHtml = ''; + if(!empty($maitriseEmailWarnings)){ + $maitriseEmailWarningsHtml .= '
'; + foreach($maitriseEmailWarnings as $w){ + $msg = sprintf(__('Email "%s" for "%s" is invalid and was not saved.'), $w['email'], $w['name']); + $maitriseEmailWarningsHtml .= '
'.htmlspecialchars($msg).'
'; + } + $maitriseEmailWarningsHtml .= '
'; + } ?> @@ -75,6 +195,7 @@ + diff --git a/readme_Project_mgr.me b/readme_Project_mgr.me new file mode 100644 index 000000000..a8b978f2d --- /dev/null +++ b/readme_Project_mgr.me @@ -0,0 +1,61 @@ +Vue D’Ensemble + +Page de gestion des projets: créer, modifier, supprimer un projet et gérer ses “Maitrise” (bureaux/types), ainsi que l’association de matériels (devices/cabinets). +Accès via Project dans le menu; la liste est filtrable (recherche intégrée). +Accès + +Permissions d’écriture requises pour créer/modifier des projets. +Les actions “Gérer les types de Maitrise” sont réservées aux administrateurs de contacts. +Sélection Du Projet + +Sélectionner un projet dans la liste en haut; le changement charge automatiquement le projet choisi. +Option “New Project” pour créer un projet. +Champs Projet + +Project Name: obligatoire. +Project Sponsor: optionnel. +Project Start Date, Expiration Date, Actual End Date: optionnels; format de date YYYY-MM-DD avec sélecteur calendrier. +Les validations de formulaire guident la saisie (ex. nom requis). +Actions + +Create: enregistre un nouveau projet et recharge la page du projet créé. +Update: sauvegarde les modifications du projet courant et des lignes “Maitrise”. +Delete: ouvre une confirmation. Supprime le projet; les devices sont dés-associés, pas supprimés. Action irréversible. +Maitrise (Bureaux) + +Section “Maitrise Entries”: ajoutez une ou plusieurs lignes. +Par ligne: +Type: sélectionner un type de Maitrise. +Bureau Name: nom du bureau, obligatoire. +Email: email du bureau, optionnel. +Boutons: +Add Maitrise: ajoute une nouvelle ligne vide. +Remove: supprime la ligne correspondante. +Règles Email + +Non obligatoire: si laissé vide, l’email est enregistré à NULL. +Si saisi, le format est vérifié: +Valide: enregistré tel quel. +Invalide: non enregistré (remplacé par NULL) et un avertissement s’affiche en haut du formulaire. +L’enregistrement des autres champs n’est pas bloqué par un email manquant/invalide. +Types De Maitrise (Admin) + +Bouton “Manage Maitrise Types”: +Modifier/Supprimer des types existants (sauvegarde immédiate). +Ajouter un nouveau type. +Dans les listes Type, l’option “More” ouvre également la gestion des types. +Association D’Éléments + +Assign Devices / Assign Cabinets: ouvre un panneau (iframe) pour associer/dissocier des équipements ou armoires au projet courant. +Pendant l’assignation, les champs du formulaire principal sont mis en lecture seule. +Conseils & Dépannage + +Champs requis: renseigner “Project Name” pour créer/mettre à jour. +Dates: utiliser le sélecteur pour éviter des formats invalides. +Email: laissez vide si vous n’en avez pas; en cas de message d’avertissement, corrigez le format si nécessaire. +Suppression: vérifiez bien le contenu du projet; l’action ne peut pas être annulée. +Références Fonctionnelles + +Email optionnel et avertissements: project_mgr.php:144, project_mgr.php:147, project_mgr.php:172. +Insertion des avertissements dans le formulaire: project_mgr.php:168, project_mgr.php:173, project_mgr.php:260. +Champs et actions de base du projet (formulaire): project_mgr.php:259, project_mgr.php:260, project_mgr.php:383. \ No newline at end of file