Compare commits

...

222 Commits

Author SHA1 Message Date
solocla 7b6d85766d change migration 2026-07-23 12:18:56 +02:00
solocla b2a79aec1b multiple analysis on same parts 2026-07-23 12:13:28 +02:00
solocla a300932421 fixed field object search ranking 2026-07-22 14:04:50 +02:00
solocla df1ddce0a6 fixed buyer in template 2026-07-22 12:07:20 +02:00
solocla 0277fb5262 warm cache run 2026-07-22 12:03:39 +02:00
solocla 0407bcdcc4 Add row included in clone and navigation parts 2026-07-22 09:33:46 +02:00
solocla b1810211d5 automatic date on part from ConsegnaRichiesta 2026-07-22 09:00:08 +02:00
solocla 48d775f4d5 sample unlock date time copy paste on arrival 2026-07-22 08:47:59 +02:00
solocla bd9bd52c10 fixed modal analysis name matrix 2026-07-20 15:48:29 +02:00
solocla 7db96a1f9c fixed matrix in analysis 2026-07-20 15:18:29 +02:00
solocla 699bffaa78 ranking custom field 2026-07-20 11:32:22 +02:00
RMubarakzyanov 69bb029d9d union ids 2026-07-19 14:23:23 +03:00
RMubarakzyanov c3f3e824dc server side filtering and ui-fixes 2026-07-19 13:05:00 +03:00
solocla 05356fc98b readded clona parti 2026-07-17 13:58:50 +02:00
solocla 232f8244bf fixed dirty rows 2026-07-17 11:01:51 +02:00
solocla 1107e003f8 update stats monthly 2026-07-17 10:53:31 +02:00
solocla c2fd2dc470 fixed export and save to selection 2026-07-17 10:46:33 +02:00
solocla 86b1c8420d fixed rebuild grid 2026-07-17 09:30:55 +02:00
solocla 1b5dd04740 Merge branch 'salvataggio-manuale-parts' into filter 2026-07-17 09:20:09 +02:00
solocla ff676376ab fixed filter 2026-07-17 09:19:23 +02:00
solocla 1a47d5649a filter on imported 2026-07-17 09:17:44 +02:00
solocla 66ab44142b fixed save parts with color button 2026-07-16 15:01:40 +02:00
solocla 05f5722ebd navbar update 2026-07-16 14:43:37 +02:00
solocla 546b4ced69 update stats e nota template 2026-07-16 14:34:09 +02:00
solocla d256370712 asterisc analysis 2026-07-16 13:53:53 +02:00
solocla be450f9d85 parts with save button 2026-07-15 16:59:27 +02:00
solocla 2a6eddeb44 fixed import insert 2026-07-15 15:36:30 +02:00
solocla 4d447a5c0d fixed accettatore 2026-07-15 12:08:26 +02:00
solocla c6168dd5d4 Ripristina search_clienti.php sovrascritto per errore 2026-07-15 11:12:59 +02:00
solocla 1c2ea459ca navbar update 2026-07-15 10:29:17 +02:00
solocla 7183189343 Pagina statistiche export LIMS 2026-07-15 10:24:04 +02:00
RMubarakzyanov 1bb9928b24 warm cache for analysis 2026-07-13 19:57:28 +03:00
solocla 173aef51c6 fixed popup 2026-07-11 19:58:10 +02:00
solocla a94eac9fb4 popup dashboard 2026-07-11 19:49:14 +02:00
solocla 51cfd0a434 scroll bar blocked imported 2026-07-11 09:03:17 +02:00
solocla def2715d38 lesillaroutine 2026-07-10 16:38:06 +02:00
solocla 68f344bc06 ranking search matrici 2026-07-10 11:02:45 +02:00
solocla e3f7637b64 changed search matrici strategy 2026-07-10 09:10:06 +02:00
solocla f4ea06fb93 added note field on template and export 2026-07-10 08:28:46 +02:00
solocla 2ab667df22 added fkag clone parts note 2026-07-06 17:06:17 +02:00
solocla 485748859e fixed search clients 2026-07-06 15:54:20 +02:00
solocla 7b961d5818 fixed search client 2026-07-06 15:32:15 +02:00
solocla 500e40782d phinx install 2026-07-04 18:13:21 +02:00
solocla b19963808e favourites button 2026-07-04 15:54:34 +02:00
solocla 8a5859ce46 added free text 2026-07-04 15:46:11 +02:00
solocla 3d2c0c9799 fixed marker 2026-07-04 15:23:49 +02:00
solocla 0130231c64 various fixing 2026-07-04 15:18:48 +02:00
solocla a99810d61b during clone pars ask to clone analysis related 2026-07-04 14:46:44 +02:00
solocla 7473973a34 disabled ClienteFatturazione temporary 2026-07-04 14:28:03 +02:00
solocla 1947730e34 fixed warm cache 2026-07-03 16:48:16 +02:00
solocla e3b1a46482 clienteAnalisi added to validation 2026-07-03 15:52:37 +02:00
solocla 0e16812e9a added Fatturazione in export and fixed attivo inattivo, added desxription to parts 2026-07-03 14:46:04 +02:00
solocla 40dd23e658 fixed headscript 2026-07-02 15:53:38 +02:00
solocla bd47f4f715 fixed topbar2 2026-07-02 15:38:26 +02:00
solocla 042b4698fb fixed topbar 2026-07-02 15:37:56 +02:00
solocla 614bc4feba migration fatturazione script 2026-07-02 15:37:11 +02:00
solocla bef600ae11 added fatturazione avanzata to template and imported 2026-07-02 15:20:55 +02:00
solocla 1476160a38 test enivornment badge 2026-07-02 10:27:28 +02:00
solocla 2c7ddbcfeb fixed flag main 2026-07-01 12:19:11 +02:00
solocla 6ad10c4d8f fixed validation per dup0licate parts, client inactive, fixed flags scheme, update schema json 2026-07-01 11:43:17 +02:00
RMubarakzyanov e0654b4847 fix annotated photos 2026-06-29 22:06:27 +03:00
RMubarakzyanov fda3d66d1f fix ghost columns v2 2026-06-22 22:30:02 +03:00
RMubarakzyanov ff61456d91 fix ghost columns 2026-06-22 20:43:52 +03:00
solocla 089886cb9a fixed button 2026-06-22 15:39:28 +02:00
solocla 4dc010d80e added users admin and roles on menu 2026-06-18 10:54:44 +02:00
solocla 83c6157b10 validation excldugin id 189 tested component 2026-06-16 08:33:32 +02:00
RMubarakzyanov 4dd7b89c22 added mandatory field checking 2026-06-11 13:41:31 +03:00
RMubarakzyanov dec42b4442 Merge branch 'main' into feature/analysis 2026-06-11 09:37:01 +03:00
solocla dab8d9aebf fixed mapping for json 2026-06-08 08:47:17 +02:00
solocla 375a10a678 filter analysis web 2026-06-08 07:40:05 +02:00
solocla 15990be884 fxied column order LIMS 2026-06-04 16:48:22 +02:00
RMubarakzyanov c3a6dd73b6 import backoff 2026-05-28 23:59:15 +03:00
solocla 44ed1186e0 added richmento pelletteria routine 2026-05-28 13:50:52 +02:00
solocla 9050cb1006 routine burberry 2026-05-26 12:15:45 +02:00
solocla e6820fdb62 added order column 2026-05-25 10:59:58 +02:00
solocla 5da37a7836 paulshark routine 2026-05-22 12:27:53 +02:00
solocla c5f27cb69a routine fendi 2026-05-21 10:11:53 +02:00
solocla 1d81d6c996 fixed import update 2026-05-20 18:47:41 +02:00
solocla 0c72dbf5ae sort first with exactly phrase 2026-05-20 17:01:07 +02:00
solocla 8455be04e1 remove hidden from xls import to avoid big file dimension 2026-05-20 16:47:18 +02:00
solocla e42d1b9c51 fix limit dropdown 2026-05-19 16:20:55 +02:00
solocla 3e69e3c322 fixed routine valentino 2026-05-19 11:23:26 +02:00
solocla 0eb4f7a2ad valentino routine 2026-05-19 11:12:18 +02:00
solocla 4f2cfc1930 fix render gird for doppi apici 2026-05-18 15:38:43 +02:00
solocla df075dd76a main field show in imported 2026-05-14 10:14:59 +02:00
solocla 6460454201 fixed main field 2026-05-14 10:00:47 +02:00
solocla 574ddbbd32 fixed propagation 2026-05-13 15:12:13 +02:00
solocla 41f414db5c 2 main 2026-05-13 14:58:13 +02:00
solocla 4a863e8c16 fixed double scroll bar parts dropdown 2026-05-13 14:26:11 +02:00
solocla b431f1d4e9 added top scrollbar imported 2026-05-13 14:22:43 +02:00
solocla f97b52f158 skip empty parts during clone and LIMS export 2026-05-13 11:54:28 +02:00
solocla 836fc055ec cliente reponsabile change based on client 2026-05-12 15:48:35 +02:00
solocla e8dd585df4 added arrows to jump parts 2026-05-12 09:37:52 +02:00
solocla 198b8c08ad Revert "update scheme different obbligatorioweb"
This reverts commit a3eb0f0a57.
2026-05-11 14:47:42 +02:00
solocla 28c467d55e fixed order import 2026-05-11 14:24:35 +02:00
solocla 56eee99a67 change template and import o use also sheet number of XLS 2026-05-09 15:39:43 +02:00
solocla f514b3d2c7 added sheet and config API in insert edit template 2026-05-09 09:44:52 +02:00
solocla a3eb0f0a57 update scheme different obbligatorioweb 2026-05-09 09:30:59 +02:00
solocla aa355905d7 update schema 2026-05-08 11:10:21 +02:00
solocla b38f3e1240 Stop tracking generated schema response file 2026-05-06 16:52:53 +02:00
solocla cbd0c5b68a annotation color and clone templates 2026-05-06 16:44:42 +02:00
solocla f3e5cb4ffd fixed capitolato riferimento 2026-05-05 09:37:18 +02:00
solocla 0f0c6a04b7 fixed Mix 2026-05-05 09:07:14 +02:00
solocla 813bd66f96 scroll table parts 2026-05-04 12:10:12 +02:00
solocla ce00247d1c edit templtae lingue e colori 2026-05-04 11:21:30 +02:00
solocla 3a7dd266c8 fixed quotation 2026-04-29 12:16:44 +02:00
solocla ba8dc4c721 added get 2026-04-29 08:38:16 +02:00
RMubarakzyanov cfbbc36116 Fix TZ Issue 2026-04-28 12:53:21 +03:00
solocla 50d578eea1 commesssa on commessaweb 2026-04-27 16:02:50 +02:00
solocla 6b0d2aa9b9 fixed color and import dahboard 2026-04-27 14:34:45 +02:00
RMubarakzyanov 67bbd9bbbb export analyses 2026-04-21 00:09:59 +03:00
solocla fa7997c980 tested component fixed 2026-04-19 17:38:35 +02:00
solocla 66be442eb6 scrollbar import page 2026-04-17 15:00:17 +02:00
solocla 19a2d6e3f7 scrollbar also top 2026-04-17 10:47:41 +02:00
solocla a15ab08576 Merge branch 'main' of http://192.168.1.93:8418/solocla/trf_certest 2026-04-17 08:43:32 +02:00
solocla f71e8a56b5 cloneparts 2026-04-17 08:43:05 +02:00
RMubarakzyanov cb38bfb75a fix resize on tolims page 2026-04-16 11:39:00 +03:00
solocla 28a708dad3 removed limit admin 2026-04-15 16:19:39 +02:00
solocla 6b9cf20ab9 fixed resize and analysis 2026-04-15 14:37:49 +02:00
solocla d40fc7d177 update gitignore 2026-04-15 08:49:06 +02:00
RMubarakzyanov b812563023 Merge branch 'main' into feature/milestone3 2026-04-08 09:24:30 +03:00
solocla 39a821357e analysis complete 2026-04-03 11:15:33 +02:00
RMubarakzyanov 53c223ea5f merge 2026-04-02 17:01:22 +03:00
RMubarakzyanov 9775a12d4a Merge branch 'main' into feature/milestone3
# Conflicts:
#	public/userarea/import_insert.php
#	public/userarea/mapping_template_xls_scheme2.php
#	public/userarea/process_import_xls2.php
2026-04-02 12:29:17 +03:00
solocla 7dfb935e33 new mocnler routine (3 steps) 2026-04-02 09:49:09 +02:00
RMubarakzyanov abb4200215 Fix import 2026-03-31 17:15:43 +03:00
RMubarakzyanov 0be7109df4 Fix header finder 2026-03-31 16:33:08 +03:00
solocla 578671e013 added analysis by client and matrici 2026-03-31 14:08:51 +02:00
RMubarakzyanov d24836e2b1 matrici cache 2026-03-31 14:25:10 +03:00
RMubarakzyanov d983659000 transfer auto-detect header 2026-03-31 13:41:31 +03:00
RMubarakzyanov 7c5aa7734f hide form 2026-03-30 18:19:16 +03:00
RMubarakzyanov 4f0dbc7e91 cache and view improvements 2026-03-30 16:28:20 +03:00
RMubarakzyanov fb09f033ae removed htmlspecialchars on import && savings 2026-03-30 13:24:18 +03:00
solocla 8bae2d7008 analysis 2026-03-30 11:31:25 +02:00
RMubarakzyanov 7463fc6726 change links 2026-03-30 08:53:14 +03:00
RMubarakzyanov 0bc2ff7e9d fix autodetect 2026-03-29 21:47:23 +03:00
RMubarakzyanov aa1c32b7ed autodetect mapping 2026-03-29 21:07:16 +03:00
RMubarakzyanov c573a46318 tolims filter, pagination 2026-03-29 17:13:54 +03:00
solocla bf18a904bd update API template 2026-03-28 10:33:28 +01:00
solocla b3ce489348 api change dahsboard 2026-03-28 10:28:25 +01:00
solocla 08b89e01cc Stop tracking customfield_values_response.json 2026-03-28 10:28:08 +01:00
RMubarakzyanov 3e66d67dc5 remove htmlspecialchars from saving 2026-03-27 20:55:54 +03:00
solocla 490786731a moncler supplier fabric routine 2026-03-27 15:54:22 +01:00
solocla e96f538a47 added multiple column option template with color 2026-03-27 15:36:57 +01:00
solocla cc96ecb67f order parts by part_number 2026-03-27 15:18:44 +01:00
solocla 4cf03ae742 fixed routine 2026-03-27 09:36:06 +01:00
solocla 7a486e9dcf update moncler routine 2026-03-27 09:24:46 +01:00
RMubarakzyanov 2a7b1fae17 imported && tolims 2026-03-26 16:05:17 +03:00
solocla a05d9fed2b scheme alphabetical order 2026-03-26 11:47:02 +01:00
solocla 65170a0a7c fixed 285 export to LIMS and header import 2026-03-26 11:42:52 +01:00
solocla 223688c372 added select 2 on capitolato 2026-03-25 14:23:44 +01:00
solocla b562eb4033 fixed ordine alfabetico capitolato 2026-03-25 14:20:45 +01:00
solocla 0645a0c675 update profile 2026-03-25 11:02:27 +01:00
solocla a02a6b2c4c added clienteAnalisi on query export 2026-03-22 15:11:00 +01:00
RMubarakzyanov 45dd8d6907 Link photos to .01 Campioni 2026-03-21 16:18:27 +03:00
RMubarakzyanov 755f6812d4 Campione custom fields (Tested Component + parts), per-part ConsegnaRichiesta with validation 2026-03-20 23:31:31 +03:00
RMubarakzyanov 5e59ae2162 Tested component 2026-03-20 23:15:50 +03:00
solocla 71a19144c8 gitignore fixed 2026-03-19 13:48:51 +01:00
RMubarakzyanov 381a05341b PrimaPagina, StampaNelRapporto flags handling 2026-03-19 13:02:13 +03:00
solocla 5a58decd40 added clienti in template, export 2026-03-19 10:08:29 +01:00
RMubarakzyanov eb21910ef3 per-row buttons locking, progress 2026-03-18 19:58:57 +03:00
RMubarakzyanov 5dedc779df format date to d/m/Y 2026-03-18 19:24:21 +03:00
solocla f4e0074a73 fixed tested components 2026-03-18 16:11:03 +01:00
solocla 9c850e4ea6 fixed filter import 2026-03-18 16:07:25 +01:00
solocla f300811341 photos fixed background 2026-03-18 15:53:46 +01:00
RMubarakzyanov 48387a9945 cliente fornitore 2026-03-17 15:43:12 +03:00
RMubarakzyanov 0e90db8219 import date format 2026-03-17 12:44:37 +03:00
RMubarakzyanov eaf70d5a46 export date format 2026-03-16 20:57:05 +03:00
RMubarakzyanov cdd6551e9c remove validation simulate 2026-03-14 19:09:17 +03:00
RMubarakzyanov 1b97bf4362 1,2,3,4,5,6 points of milestone 2026-03-13 00:33:55 +03:00
solocla c516589483 changed notes TRF 2026-03-12 10:28:58 +01:00
solocla 817bbadf22 added flag to photos 2026-03-12 10:20:22 +01:00
solocla 1f27bc48d4 added get utenti 2026-03-11 14:53:38 +01:00
RMubarakzyanov c9fba48d88 fix post data 2026-03-11 16:04:37 +03:00
RMubarakzyanov 70d4c0759e importpayload logging 2026-03-09 17:34:32 +03:00
RMubarakzyanov 5d7880160a send userid to import 2026-03-09 17:12:38 +03:00
RMubarakzyanov bbe74d1529 fetch changes from branch export_to_lims 2026-03-09 17:07:20 +03:00
RMubarakzyanov 93930227a2 fetch changes from branch export_to_lims 2026-03-09 17:04:03 +03:00
solocla 2598a4c91b Step 9 remove comment on export to lims 2026-03-06 09:15:30 +01:00
solocla 5e677a8b9c Merge branch 'feature/export-to-lims' 2026-03-06 08:48:57 +01:00
solocla 540c44d89a fixed column mappings 2026-03-05 14:19:49 +01:00
solocla 2ee9f2ecb1 added proceed on the top 2026-03-04 10:13:33 +01:00
solocla c9122774b1 added additional field in parts template e layout 2026-03-02 14:37:20 +01:00
RMubarakzyanov 1fed113c5c VisualLimsApiClientMock: fake data for all LIMS endpoints; getInstance() branches on SIMULATE_EXPORT_LIMS
get_clienti.php, get_fixed_field_data.php: simulate mode support
CustomField dropdown values mock added (get_customfield_values.php)

exportUnsavedModal: prompt save before export, MutationObserver waits for save, then proceeds to confirm
Removed old jQuery .export-lims-btn handler that bypassed confirm modal
Fix false "Unsaved changes" on page load: data-restoring guard in all programmatic trigger/dispatchEvent calls (populateSelect, populateClientDropdowns, populateDropdowns)

Fix ConsegnaRichiesta not shown on refresh: add to PHP $fixedAliasMap
Add step5_2_photos, step9_1_importa
2026-02-28 00:44:21 +03:00
RMubarakzyanov e3994d6f9f fix notices running on php-8.2 2026-02-26 22:59:41 +03:00
solocla 9af0df3cca env example update 2026-02-26 17:14:16 +01:00
solocla 48d2a3ff42 time and date setting 2026-02-25 22:07:44 +01:00
solocla cf44e67922 added auto user accettatore id 2026-02-25 15:24:41 +01:00
solocla 35021e9d9b dropdown in alphabetical order 2026-02-25 10:02:45 +01:00
solocla 497ebda65a update gitignore 2026-02-24 09:09:32 +01:00
solocla b0024edb70 mapping fixed 2026-02-24 09:06:45 +01:00
solocla b9852ba226 saved rows 2026-02-19 12:07:32 +01:00
solocla 407d6884a1 fixed note save and import special character 2026-02-19 11:55:24 +01:00
solocla 1a4beadbb2 fixed propagate dropdown 2026-02-18 11:52:26 +01:00
solocla 4bb0445cff added select 2 on all fields (with limit of 12) 2026-02-18 10:48:04 +01:00
solocla 73dd8f4ce8 fixed column size final 2026-02-18 10:04:27 +01:00
solocla bdc4e0e60c fixed size column 2026-02-18 09:58:42 +01:00
solocla ef8f2d8000 fixed labels 2026-02-18 09:54:21 +01:00
solocla 74c9a1d02a added time field 2026-02-18 09:34:26 +01:00
solocla cd3bccd183 added multiple tested components 2026-02-17 09:04:30 +01:00
solocla 78154e43a9 fixed tested components 2026-02-05 10:32:28 +01:00
solocla 9fe9243e60 fixed mapping template 2026-02-05 09:05:51 +01:00
solocla a8330d4aba fixed layout 2026-02-04 15:31:53 +01:00
MGrigoryan 82d6a2ee18 feat(import_edit2): expand entire column including header and fix sticky columns 2026-02-04 17:00:01 +04:00
MGrigoryan d8eddb3aa5 feat(import_edit2): enhance grid UI with sticky columns and textarea conversion
- Fix grid-top alignment and datepicker positioning
- Extend column resizer to grid-top cells
- Add sticky columns for Actions and Sample Description
- Implement input-to-textarea conversion on focus
- Increase Select2 dropdown height
- Improve grid-container CSS for sticky support
2026-02-04 11:59:59 +04:00
solocla f60dc64b2d fixed importedit 2026-02-02 08:46:47 +01:00
solocla 4e4cae1df8 added fixed fields 2026-01-30 12:07:43 +01:00
solocla 8838edf3a1 fixed fields 2026-01-28 11:49:11 +01:00
solocla e75be99e43 added resort manually 2025-11-08 17:55:10 +01:00
solocla a482d975da Merge branch 'feature/parts-photo-load-from-quotation' 2025-10-31 10:38:28 +01:00
solocla 598a2cc84c change marker dimension 2025-10-31 10:16:57 +01:00
MGrigoryan 6ec0c2062e fix: Include single sample photo in save operation when no photos selected 2025-10-31 11:45:04 +04:00
MGrigoryan 5eb5bd1613 fix: deselect part when clicking selected row 2025-10-31 11:30:18 +04:00
MGrigoryan 03771e3ca8 feat: Add quotation modal and iddatadb update for quotation parts/images
- Add quotation modal for selecting quotation
- Load quotation parts and photos
- Update iddatadb with quotation parts and photo references
2025-10-31 11:26:11 +04:00
MGrigoryan 03642fdfab feat(annotations): support multiple pins per part 2025-10-30 17:08:57 +04:00
MGrigoryan f6ea17388c fix(partsTable, annotationsModal): correct table height and restore "Torna alle Parti"
- partsTable: reduce excessive height to prevent oversized rendering
- annotationsModal: fix "Torna alle Parti" button behavior to return to parts view
2025-10-30 10:26:46 +04:00
solocla 1c2b4ab7a6 Merge branch 'bugfix/part-creation-matrice-dropdown-fix' 2025-10-29 18:25:26 +01:00
MGrigoryan 31cb23b00e fix(partsTable): skip change handler when setting Select2 value programmatically
Pass skipHandler flag in change event data to prevent handler execution
during programmatic value updates.
2025-10-29 19:18:33 +04:00
MGrigoryan d29563d20d feat(partsTable): add image icon to show/hide photo button
- Add image icon alongside eye icon in show/hide photo button
- Improve visual indication of photo toggle functionality
2025-10-29 18:27:33 +04:00
solocla 82af925ac1 added size to line markers 2025-10-29 13:45:38 +01:00
solocla 5d8360dd87 marker with size 2025-10-29 12:32:09 +01:00
MGrigoryan 683073c244 fix(partsTable): initialize new item matrice as empty and refresh on global filter change
- Default new items' matrice to empty to prevent stale values leaking from prior state
- Force matrice update when the global filter changes to keep items in sync
2025-10-29 14:17:00 +04:00
200 changed files with 79301 additions and 2772 deletions
+7 -18
View File
@@ -7,9 +7,9 @@ LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST="localhost"
DB_DATABASE="trfcertest"
DB_USERNAME="solocla"
DB_PASSWORD="xxxxxxx"
DB_DATABASE="xxxx"
DB_USERNAME="xxxx"
DB_PASSWORD="xxxxx"
DB_PREFIX="auth_"
BROADCAST_DRIVER=log
@@ -41,19 +41,8 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Credenziali API VisualLims
SIMULATE_EXPORT_LIMS=true
API_BASE_URL=https://93.43.5.102/limsapi
API_USERNAME=xxxx
API_PASSWORD=XXXX
API_BASE_URL=https://bvcpsitaly-elims.com/limsapi
API_USERNAME=WebApiUser
API_PASSWORD=webapiuser01
BASE_URL=http://localhost:8000/userarea/
# Credenziali ENTRAID
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
AZURE_TENANT_ID=
MICROSOFT_CLIENT_ID=your_client_id_here
MICROSOFT_CLIENT_SECRET=your_client_secret_here
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
BASE_URL=http://localhost:8000/userarea/
+39 -41
View File
@@ -1,66 +1,64 @@
.DS_Store
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
/.idea
/.fleet
/.vscode
/.vagrant
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.phpunit.result.cache
.php_cs.cache
/documentation
/.phpunit.cache
/public/hot
/public/storage
/public/build
/storage/*.key
.env
.env.backup
.env.production
auth.json
# File di debug e temporanei JSON e log
.phpunit.result.cache
.php_cs.cache
/.phpunit.cache
npm-debug.log
yarn-error.log
/documentation
# --- Runtime / Debug (userarea) ---
/public/userarea/*.json
/public/userarea/*.log
/public/userarea/*.txt
# File di log nella sottocartella class
/public/userarea/*_response.json
/public/userarea/customfield_values_response.json
/public/userarea/error_log.txt
/public/userarea/import_debug.log
/public/userarea/last_url.txt
/public/userarea/logaspi/
/public/userarea/logs/api/
/public/userarea/logs/api/**
/public/userarea/logs/
/public/userarea/logs/**
/public/userarea/photostrf/
/public/userarea/class/*.log
/public/userarea/class/curl_auth_debug.log
/public/userarea/class/curl_request_debug.log
/public/userarea/schema_dettagli_response.json
public/userarea/schemi_base_response.json
# File XLSX temporanei importati
/public/userarea/imported_trf/*.xlsx
/public/userarea/xlstemplates/*.xlsx
# Ignora cartelle di foto generate
# Cartelle foto generate
/public/photostrf/
/public/photostrf/qrcodes/
public/userarea/import_debug.log
public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log
public/userarea/last_url.txt
public/userarea/class/curl_auth_debug.log
public/userarea/class/curl_request_debug.log
# Ignora tutti i log
# Ignora tutti i log ovunque
*.log
# Ignora cartella photostrf in public/userarea
/public/userarea/photostrf/
public/userarea/customfield_values_response.json
/public/userarea/logaspi/
public/userarea/logsapi/campione_762_1.json
public/userarea/logsapi/campione_763_1.json
public/userarea/logsapi/campione_762_2.json
public/userarea/logsapi/campione_763_2.json
public/userarea/logsapi/commessaweb_create_762.json
public/userarea/logsapi/commessaweb_create_763.json
public/userarea/logsapi/commessaweb_customfields_762.json
public/userarea/logsapi/commessaweb_customfields_763.json
public/userarea/logsapi/commessaweb_invia_762.json
public/userarea/logsapi/commessaweb_invia_763.json
public/userarea/logsapi/last_auth_url.txt
public/userarea/cache/
public/userarea/error_log.txt
@@ -109,6 +109,8 @@ class LoginController extends Controller
// Reindirizza in base al ruolo
if ($user->hasRole('Admin')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('SuperUser')) {
return redirect()->to('userarea/import_dashboard.php');
} elseif ($user->hasRole('User')) {
return redirect()->to('userarea/import_dashboard.php');
}
+1
View File
@@ -44,6 +44,7 @@
"phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1",
"proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.12",
"socialiteproviders/microsoft": "^4.7",
"spatie/laravel-query-builder": "^5.0",
"vanguardapp/activity-log": "^6.0",
Generated
+646 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9c4f1e3bc3ee2180211c055e70635aef",
"content-hash": "557df76fdde549210349be369d479f5d",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -251,6 +251,330 @@
],
"time": "2023-11-29T23:19:16+00:00"
},
{
"name": "cakephp/chronos",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/cakephp/chronos.git",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6",
"reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Cake\\Chronos\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
},
{
"name": "The CakePHP Team",
"homepage": "https://cakephp.org"
}
],
"description": "A simple API extension for DateTime.",
"homepage": "https://cakephp.org",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"issues": "https://github.com/cakephp/chronos/issues",
"source": "https://github.com/cakephp/chronos"
},
"time": "2026-04-10T02:50:39+00:00"
},
{
"name": "cakephp/core",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/core.git",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/core/zipball/9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12",
"shasum": ""
},
"require": {
"cakephp/utility": "^5.3.0",
"league/container": "^5.1",
"php": ">=8.2",
"psr/container": "^1.1 || ^2.0"
},
"provide": {
"psr/container-implementation": "^2.0"
},
"suggest": {
"cakephp/cache": "To use Configure::store() and restore().",
"cakephp/event": "To use PluginApplicationInterface or plugin applications.",
"league/container": "To use Container and ServiceProvider classes"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Cake\\Core\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/core/graphs/contributors"
}
],
"description": "CakePHP Framework Core classes",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"core",
"framework"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/core"
},
"time": "2026-05-15T03:31:14+00:00"
},
{
"name": "cakephp/database",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/database.git",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/database/zipball/442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca",
"shasum": ""
},
"require": {
"cakephp/chronos": "^3.3",
"cakephp/core": "^5.3.0",
"cakephp/datasource": "^5.3.0",
"php": ">=8.2",
"psr/log": "^3.0"
},
"require-dev": {
"cakephp/i18n": "^5.3.0",
"cakephp/log": "^5.3.0"
},
"suggest": {
"cakephp/i18n": "If you are using locale-aware datetime formats.",
"cakephp/log": "If you want to use query logging without providing a logger yourself."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Database\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/database/graphs/contributors"
}
],
"description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
"homepage": "https://cakephp.org",
"keywords": [
"abstraction",
"cakephp",
"database",
"database abstraction",
"pdo"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/database"
},
"time": "2026-05-21T19:38:13+00:00"
},
{
"name": "cakephp/datasource",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/datasource.git",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/datasource/zipball/b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2",
"psr/simple-cache": "^2.0 || ^3.0"
},
"require-dev": {
"cakephp/cache": "^5.3.0",
"cakephp/collection": "^5.3.0",
"cakephp/utility": "^5.3.0"
},
"suggest": {
"cakephp/cache": "If you decide to use Query caching.",
"cakephp/collection": "If you decide to use ResultSetInterface.",
"cakephp/utility": "If you decide to use EntityTrait."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Cake\\Datasource\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/datasource/graphs/contributors"
}
],
"description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"connection management",
"datasource",
"entity",
"query"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/datasource"
},
"time": "2026-05-20T10:27:33+00:00"
},
{
"name": "cakephp/utility",
"version": "5.3.6",
"source": {
"type": "git",
"url": "https://github.com/cakephp/utility.git",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/utility/zipball/4c703a010b9d955fed44731669e35d3043425cc7",
"reference": "4c703a010b9d955fed44731669e35d3043425cc7",
"shasum": ""
},
"require": {
"cakephp/core": "^5.3.0",
"php": ">=8.2"
},
"suggest": {
"ext-intl": "To use Text::transliterate() or Text::slug()",
"lib-ICU": "To use Text::transliterate() or Text::slug()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.next": "5.4.x-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Cake\\Utility\\": "."
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/utility/graphs/contributors"
}
],
"description": "CakePHP Utility classes such as Inflector, String, Hash, and Security",
"homepage": "https://cakephp.org",
"keywords": [
"cakephp",
"hash",
"inflector",
"security",
"string",
"utility"
],
"support": {
"forum": "https://stackoverflow.com/tags/cakephp",
"irc": "irc://irc.freenode.org/cakephp",
"issues": "https://github.com/cakephp/cakephp/issues",
"source": "https://github.com/cakephp/utility"
},
"time": "2026-05-21T19:38:13+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -2627,6 +2951,90 @@
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/container",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5",
"reference": "58accbc032f0090a9bd08326f93062c5a658b2c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/container": "^2.0.2",
"psr/event-dispatcher": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"nette/php-generator": "^4.1",
"nikic/php-parser": "^5.0",
"phpstan/phpstan": "^2.1.11",
"phpunit/phpunit": "^10.5.45|^11.5.15|^12.0",
"roave/security-advisories": "dev-latest",
"scrutinizer/ocular": "^1.9",
"squizlabs/php_codesniffer": "^3.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev",
"dev-2.x": "2.x-dev",
"dev-3.x": "3.x-dev",
"dev-4.x": "4.x-dev",
"dev-5.x": "5.x-dev",
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "mail@philbennett.co.uk",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/5.2.0"
},
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2026-03-19T18:52:39+00:00"
},
{
"name": "league/flysystem",
"version": "3.28.0",
@@ -4980,6 +5388,93 @@
],
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "robmorgan/phinx",
"version": "0.16.12",
"source": {
"type": "git",
"url": "https://github.com/cakephp/phinx.git",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cakephp/phinx/zipball/1cce44387a9146dc04c312e6a231b17fb30385cd",
"reference": "1cce44387a9146dc04c312e6a231b17fb30385cd",
"shasum": ""
},
"require": {
"cakephp/database": "^5.0.2",
"composer-runtime-api": "^2.0",
"php-64bit": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/config": "^4.0|^5.0|^6.0|^7.0|^8.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/i18n": "^5.0",
"ext-json": "*",
"ext-pdo": "*",
"phpunit/phpunit": "^10.5",
"symfony/yaml": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"suggest": {
"ext-json": "Install if using JSON configuration format",
"ext-pdo": "PDO extension is needed",
"symfony/yaml": "Install if using YAML configuration format"
},
"bin": [
"bin/phinx"
],
"type": "library",
"autoload": {
"psr-4": {
"Phinx\\": "src/Phinx/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Morgan",
"email": "robbym@gmail.com",
"homepage": "https://robmorgan.id.au",
"role": "Lead Developer"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com",
"homepage": "https://shadowhand.me",
"role": "Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Developer"
},
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/phinx/graphs/contributors",
"role": "Developer"
}
],
"description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.",
"homepage": "https://phinx.org",
"keywords": [
"database",
"database migrations",
"db",
"migrations",
"phinx"
],
"support": {
"issues": "https://github.com/cakephp/phinx/issues",
"source": "https://github.com/cakephp/phinx/tree/0.16.12"
},
"time": "2026-07-03T07:27:17+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.8.1",
@@ -5312,6 +5807,85 @@
],
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/config",
"version": "v7.4.14",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"reference": "7b665e443381ea7c4db03eb03b4bf79ea2b020eb",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/finder": "<6.4",
"symfony/service-contracts": "<2.5"
},
"require-dev": {
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Config\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v7.4.14"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-06-09T07:51:57+00:00"
},
{
"name": "symfony/console",
"version": "v7.1.3",
@@ -5768,6 +6342,76 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-11T16:38:44+00:00"
},
{
"name": "symfony/finder",
"version": "v7.1.3",
@@ -11355,6 +11999,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1,15 @@
<?php
use Phinx\Migration\AbstractMigration;
final class CreatePhinxTestTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('phinx_test');
$table
->addColumn('name', 'string', ['limit' => 100, 'null' => false])
->addColumn('created_at', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
->create();
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddAddnoteToTemplateMapping extends AbstractMigration
{
public function change(): void
{
$table = $this->table('template_mapping');
$table->addColumn('is_addnote', 'boolean', [
'default' => 0,
'null' => false,
'after' => 'is_visible_parts',
'comment' => 'Campo utilizzabile come addnote (multi-selezione)',
])
->update();
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateOptionsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('options');
$table
->addColumn('opt_key', 'string', ['limit' => 100])
->addColumn('opt_value', 'text', ['null' => true])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['opt_key'], ['unique' => true])
->create();
// Righe iniziali del popup
$this->table('options')->insert([
[
'opt_key' => 'popup_enabled',
'opt_value' => '1',
],
[
'opt_key' => 'popup_version',
'opt_value' => '2026-07-migrazione',
],
[
'opt_key' => 'popup',
'opt_value' =>
'<h2 style="margin-top:0;">Nuovo portale disponibile</h2>'
. '<p>Ci siamo spostati. Da ora usa il nuovo indirizzo:</p>'
. '<p style="text-align:center;">'
. '<a href="https://trfsmart-bv.cesoft.io/" '
. 'style="display:inline-block;padding:12px 24px;background:#2563eb;'
. 'color:#fff;text-decoration:none;border-radius:6px;">Vai al nuovo portale</a>'
. '</p>'
. '<p style="font-size:13px;color:#666;">Salva il nuovo indirizzo nei preferiti. '
. 'Se riscontri problemi puoi continuare a usare questo portale per ora.</p>',
],
])->saveData();
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddExportDateToDatadb extends AbstractMigration
{
/**
* Aggiunge export_date su datadb: data/ora di esportazione verso il LIMS,
* valorizzata insieme al numero commessa quando status passa a 'l'.
*/
public function change(): void
{
$this->table('datadb')
->addColumn('export_date', 'datetime', [
'null' => true,
'default' => null,
'after' => 'commessaweb',
'comment' => 'Data/ora esportazione verso LIMS (valorizzata quando status = l)',
])
->addIndex(['export_date'], ['name' => 'idx_datadb_export_date'])
->update();
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateLimsDailySamplesTable extends AbstractMigration
{
/**
* Tabella con un dato al giorno: numero totale di campioni inseriti
* nel LIMS (loro software) in quella giornata. Serve per calcolare
* la % di campioni esportati sul totale inserito nel LIMS.
*/
public function change(): void
{
$table = $this->table('lims_daily_samples', [
'id' => false,
'primary_key' => ['id'],
'comment' => 'Totale campioni inseriti nel LIMS per giornata',
]);
$table
->addColumn('id', 'integer', [
'identity' => true,
'signed' => false,
])
->addColumn('sample_date', 'date', [
'null' => false,
'comment' => 'Giornata di riferimento',
])
->addColumn('total_samples', 'integer', [
'signed' => false,
'null' => false,
'default' => 0,
'comment' => 'Totale campioni inseriti nel LIMS in quella giornata',
])
->addColumn('note', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
])
->addColumn('created_at', 'timestamp', [
'null' => true,
'default' => 'CURRENT_TIMESTAMP',
])
->addColumn('updated_at', 'timestamp', [
'null' => true,
'default' => null,
'update' => 'CURRENT_TIMESTAMP',
])
->addIndex(['sample_date'], [
'unique' => true,
'name' => 'uq_lims_daily_samples_date',
])
->create();
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddTemplateNoteToExcelTemplates extends AbstractMigration
{
public function change(): void
{
$this->table('excel_templates')
->addColumn('template_note', 'text', [
'null' => true,
'default' => null,
'after' => 'idroutine',
'comment' => 'Nota opzionale del template, aggiunta in fondo alle note in esportazione',
])
->update();
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class DropUniqPartAnalysisIndex extends AbstractMigration
{
public function up(): void
{
$table = $this->table('identification_parts_analyses');
if ($table->hasIndexByName('uniq_part_analysis')) {
$table->removeIndexByName('uniq_part_analysis')->update();
}
}
public function down(): void
{
$table = $this->table('identification_parts_analyses');
if (!$table->hasIndexByName('uniq_part_analysis')) {
$table->addIndex(['part_id', 'analysis_recordkey'], [
'unique' => true,
'name' => 'uniq_part_analysis',
])->update();
}
}
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# DB LOCALE (Windows 11)
url=jdbc:mysql://localhost:3306/trfcertest
username=solocla
password=!Massarosa2
driver=com.mysql.cj.jdbc.Driver
changeLogFile=liquibase/changelog/db.changelog-master.yaml
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
return [
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds',
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'production',
'production' => [
'adapter' => 'mysql',
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_DATABASE'] ?? '',
'user' => $_ENV['DB_USERNAME'] ?? '',
'pass' => $_ENV['DB_PASSWORD'] ?? '',
'port' => $_ENV['DB_PORT'] ?? 3306,
'charset' => 'utf8mb4',
],
],
'version_order' => 'creation',
];
+91
View File
@@ -0,0 +1,91 @@
<?php
header('Content-Type: application/json');
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once(__DIR__ . '/include/headscript.php'); // Auth + $iduserlogin + DBHandlerSelect
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$iddatadb = isset($_POST['iddatadb']) ? (int)$_POST['iddatadb'] : 0;
if ($iddatadb <= 0) {
throw new Exception('Missing iddatadb');
}
// ✅ Supporta sia singola stringa (con |) che array part_descriptions[]
$parts = [];
if (isset($_POST['part_descriptions']) && is_array($_POST['part_descriptions'])) {
$parts = $_POST['part_descriptions'];
} else {
$raw = isset($_POST['part_description']) ? (string)$_POST['part_description'] : '';
// split su "|" per multi-part
$parts = preg_split('/\s*\|\s*/', $raw);
}
// normalizza: trim + rimuovi vuoti + dedup
$cleanParts = [];
foreach ($parts as $p) {
$p = trim((string)$p);
if ($p === '') continue;
// limita lunghezza come da DB varchar(255)
if (mb_strlen($p) > 255) {
$p = mb_substr($p, 0, 255);
}
$cleanParts[] = $p;
}
$cleanParts = array_values(array_unique($cleanParts));
if (count($cleanParts) === 0) {
throw new Exception('Part description is empty');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->beginTransaction();
try {
// prende il prossimo part_number per iddatadb
$stmtMax = $pdo->prepare("SELECT COALESCE(MAX(part_number), 0) AS maxnum FROM identification_parts WHERE iddatadb = ?");
$stmtMax->execute([$iddatadb]);
$nextNumber = (int)$stmtMax->fetchColumn() + 1;
$stmtIns = $pdo->prepare("
INSERT INTO identification_parts (iddatadb, part_number, part_description)
VALUES (?, ?, ?)
");
$insertedIds = [];
foreach ($cleanParts as $p) {
$ok = $stmtIns->execute([$iddatadb, $nextNumber, $p]);
if (!$ok) {
throw new Exception('Insert failed');
}
$insertedIds[] = $pdo->lastInsertId();
$nextNumber++;
}
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'Parts added',
'count' => count($insertedIds),
'ids' => $insertedIds
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
throw $e;
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
+73
View File
@@ -0,0 +1,73 @@
<?php
include('include/headscript.php');
header('Content-Type: application/json');
try {
$input = json_decode(file_get_contents('php://input'), true);
$templateId = intval($input['template_id'] ?? 0);
$importRefFromClient = trim($input['importreferencecode'] ?? '');
if (!$templateId) {
throw new Exception('Template ID missing');
}
$userId = $iduserlogin ?? 1;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// Get default idclient from template
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
$idclient = $template['idclient'] ?? null;
// Use provided importreferencecode (from filtered page) or generate new
$importReferenceCode = $importRefFromClient !== '' ? $importRefFromClient : date('YmdHis') . '-' . uniqid();
// Insert empty record
$stmt = $pdo->prepare("
INSERT INTO datadb (templateid, user_id, status, idclient, importreferencecode, importdate)
VALUES (?, ?, 'i', ?, ?, NOW())
");
$stmt->execute([$templateId, $userId, $idclient, $importReferenceCode]);
$iddatadb = (int)$pdo->lastInsertId();
// Create import_data_details for all mappings (with auto_value support)
$mappingStmt = $pdo->prepare("SELECT id, auto_value, manual_default, data_type FROM template_mapping WHERE template_id = ?");
$mappingStmt->execute([$templateId]);
$mappings = $mappingStmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($mappings)) {
$insertStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
foreach ($mappings as $m) {
$val = '';
$auto = $m['auto_value'] ?? 'none';
if ($auto === 'import_date') {
$val = date('Y-m-d');
} elseif ($auto === 'import_time') {
$val = date('H:i');
} elseif ($m['data_type'] === 'DATE' && ($m['manual_default'] ?? '') === 'today') {
$val = date('Y-m-d');
} elseif (!empty($m['manual_default'])) {
$val = $m['manual_default'];
}
$insertStmt->execute([$iddatadb, $m['id'], $val]);
}
}
// Get user name
$userStmt = $pdo->prepare("SELECT CONCAT(first_name, ' ', last_name) AS user_name FROM auth_users WHERE id = ?");
$userStmt->execute([$userId]);
$userName = $userStmt->fetchColumn() ?: '';
echo json_encode([
'success' => true,
'iddatadb' => $iddatadb,
'importreferencecode' => $importReferenceCode,
'user_name' => $userName,
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+878
View File
@@ -0,0 +1,878 @@
(function () {
"use strict";
let analysisMatriciMap = {};
let analysisLoadedCache = {};
let analysisAssignedState = {};
let analysisSelectedState = {};
function loadAnalysisMatrixNames() {
return $.ajax({
url: "search_matrici.php?full_list=1",
method: "GET",
dataType: "json",
})
.done(function (data) {
analysisMatriciMap = {};
(data.value || []).forEach(function (matrice) {
analysisMatriciMap[String(matrice.IdMatrice)] =
matrice.NomeMatrice || "#" + matrice.IdMatrice;
});
applyAnalysisMatrixNames();
})
.fail(function () {
analysisMatriciMap = {};
applyAnalysisMatrixNames();
});
}
function applyAnalysisMatrixNames() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
const matrixId = item.getAttribute("data-matrix-id");
const nameEl = item.querySelector(".analysis-matrix-name");
let matrixName = "No Matrix";
if (matrixId && matrixId !== "NO_MATRIX") {
matrixName =
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
}
item.setAttribute("data-matrix-name", matrixName);
if (nameEl) {
nameEl.textContent = matrixName;
}
});
modal.querySelectorAll(".analysis-part-matrix-name").forEach((el) => {
const matrixId = el.getAttribute("data-matrix-id");
if (!matrixId || matrixId === "NO_MATRIX") {
el.textContent = "No Matrix";
return;
}
el.textContent =
analysisMatriciMap[String(matrixId)] || "#" + matrixId;
});
}
function readInitialAssignedAnalyses() {
const jsonEl = document.getElementById("analysisAssignedInitialData");
if (!jsonEl) {
analysisAssignedState = {};
return;
}
try {
analysisAssignedState =
JSON.parse(jsonEl.textContent || "{}") || {};
} catch (e) {
analysisAssignedState = {};
}
}
function getSelectedPartIds() {
const modal = document.getElementById("analysisModal");
if (!modal) return [];
return Array.from(
modal.querySelectorAll(".analysis-part-checkbox:checked"),
).map((el) => String(el.value));
}
function getCurrentSelectedAnalysisRecordKeys() {
return Object.keys(analysisSelectedState).filter(function (key) {
return analysisSelectedState[key] === true;
});
}
function renderAssignedAnalysesForPart(partId) {
const container = document.getElementById(
"analysisAssignedListPart" + partId,
);
if (!container) return;
const items = Array.isArray(analysisAssignedState[String(partId)])
? analysisAssignedState[String(partId)]
: [];
if (!items.length) {
container.innerHTML = "";
return;
}
container.innerHTML = items
.map(function (item, index) {
const recordKey = item.analysis_recordkey || "";
const rowId = item.id != null ? item.id : "";
const title = item.analysis_name || "Unnamed analysis";
const method = item.analysis_method || "";
return `
<div class="analysis-assigned-chip" data-recordkey="${escapeHtml(recordKey)}">
<div class="analysis-assigned-chip-text">
<div class="analysis-assigned-chip-title">${escapeHtml(title)}</div>
${method ? `<div class="analysis-assigned-chip-method">${escapeHtml(method)}</div>` : ""}
</div>
<button type="button"
class="analysis-remove-btn"
data-part-id="${escapeHtml(partId)}"
data-recordkey="${escapeHtml(recordKey)}"
data-row-id="${escapeHtml(rowId)}"
data-index="${index}"
title="Remove analysis">×</button>
</div>
`;
})
.join("");
}
function renderAssignedAnalysesForSelectedParts() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-part-item").forEach(function (item) {
const partId = item.getAttribute("data-part-id");
renderAssignedAnalysesForPart(partId);
});
}
function syncSelectedAnalysisRows() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal
.querySelectorAll(".analysis-analysis-item")
.forEach(function (item) {
const recordKey = item.getAttribute("data-recordkey") || "";
if (recordKey && analysisSelectedState[recordKey]) {
item.classList.add("analysis-selected");
} else {
item.classList.remove("analysis-selected");
}
});
}
function buildAnalysisPayloadFromRow(rowEl) {
return {
analysis_recordkey: rowEl.getAttribute("data-recordkey") || "",
analysis_name: rowEl.getAttribute("data-analysis-name") || "",
analysis_method: rowEl.getAttribute("data-analysis-method") || "",
analysis_level: rowEl.getAttribute("data-analysis-level") || "",
is_web_selectable: rowEl.getAttribute("data-web") === "1" ? 1 : 0,
is_accredited:
rowEl.getAttribute("data-accredited") === "1" ? 1 : 0,
};
}
function addAnalysisToLocalState(
partId,
payload,
iddatadb,
idmatrice,
newId,
) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
analysisAssignedState[key] = [];
}
{
analysisAssignedState[key].push({
id: newId || null,
part_id: parseInt(partId, 10),
iddatadb: iddatadb || null,
idmatrice: idmatrice || null,
analysis_recordkey: payload.analysis_recordkey,
analysis_name: payload.analysis_name,
analysis_method: payload.analysis_method,
analysis_level:
payload.analysis_level !== ""
? parseInt(payload.analysis_level, 10)
: null,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
});
}
}
function removeAnalysisFromLocalState(partId, recordKey, index) {
const key = String(partId);
if (!Array.isArray(analysisAssignedState[key])) {
return;
}
const i = parseInt(index, 10);
if (!isNaN(i) && i >= 0 && i < analysisAssignedState[key].length) {
analysisAssignedState[key].splice(i, 1);
return;
}
const found = analysisAssignedState[key].findIndex(function (item) {
return item.analysis_recordkey === recordKey;
});
if (found !== -1) {
analysisAssignedState[key].splice(found, 1);
}
}
function saveAnalysisAssociation(partId, payload, callback) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const iddatadb =
modal.querySelector("#analysisModalIddatadb")?.value || "";
const partItem = modal.querySelector(
'.analysis-part-item[data-part-id="' + partId + '"]',
);
const idmatrice = partItem
? partItem.getAttribute("data-matrix-id") || ""
: "";
$.ajax({
url: "save_part_analysis.php",
method: "POST",
dataType: "json",
data: {
part_id: partId,
iddatadb: iddatadb,
idmatrice: idmatrice !== "NO_MATRIX" ? idmatrice : "",
analysis_recordkey: payload.analysis_recordkey,
analysis_name: payload.analysis_name,
analysis_method: payload.analysis_method,
analysis_level: payload.analysis_level,
is_web_selectable: payload.is_web_selectable,
is_accredited: payload.is_accredited,
},
})
.done(function (resp) {
addAnalysisToLocalState(
partId,
payload,
iddatadb,
idmatrice !== "NO_MATRIX" ? idmatrice : null,
resp && resp.id ? resp.id : null,
);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
})
.fail(function (xhr) {
let message = "Error saving analysis association";
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
}
alert(message);
if (typeof callback === "function") callback(false);
});
}
function deleteAnalysisAssociation(
partId,
recordKey,
rowId,
index,
callback,
) {
$.ajax({
url: "delete_part_analysis.php",
method: "POST",
dataType: "json",
data: {
part_id: partId,
analysis_recordkey: recordKey,
row_id: rowId || "",
},
})
.done(function () {
removeAnalysisFromLocalState(partId, recordKey, index);
renderAssignedAnalysesForPart(partId);
if (typeof callback === "function") callback(true);
})
.fail(function (xhr) {
let message = "Error deleting analysis association";
if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
}
alert(message);
if (typeof callback === "function") callback(false);
});
}
function setAnalysisLoadingState(isLoading) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const loadingEl = modal.querySelector("#analysisLoadingBox");
if (!loadingEl) return;
if (isLoading) {
loadingEl.classList.remove("d-none");
} else {
loadingEl.classList.add("d-none");
}
}
function showAnalysisError(message) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
if (emptyEl) emptyEl.classList.add("d-none");
if (errorEl) {
errorEl.textContent = message || "Error loading analyses";
errorEl.classList.remove("d-none");
}
}
function showAnalysisEmpty(message) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
if (errorEl) errorEl.classList.add("d-none");
if (emptyEl) {
emptyEl.textContent =
message || "No analyses found for this matrix";
emptyEl.classList.remove("d-none");
}
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function renderAnalysesList(analyses) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const listEl = modal.querySelector("#analysisList");
if (!listEl) return;
if (errorEl) errorEl.classList.add("d-none");
if (!Array.isArray(analyses) || analyses.length === 0) {
showAnalysisEmpty("No analyses found for this matrix");
return;
}
if (emptyEl) emptyEl.classList.add("d-none");
listEl.innerHTML = analyses
.map(function (item) {
const recordKey = item.RecordKey || "";
const analysisName =
item.NomeAnalisiTraduzione ||
item.NomeAnalisi ||
"Unnamed analysis";
const methodName = item.MetodoNome || "";
const selectable = item.SelezionabileSuWeb === true;
const accredited = item.Accreditato === true;
const level = item.Livello ?? "";
const searchText = (
analysisName +
" " +
methodName
).toLowerCase();
let badges = "";
if (selectable) {
badges += '<span class="badge bg-success">Web</span>';
} else {
badges += '<span class="badge bg-secondary">Not web</span>';
}
if (accredited) {
badges +=
'<span class="badge bg-info text-dark">Accredited</span>';
}
if (level !== "") {
badges +=
'<span class="badge bg-light text-dark border">Level ' +
escapeHtml(level) +
"</span>";
}
return `
<div
class="list-group-item analysis-analysis-item"
data-recordkey="${escapeHtml(recordKey)}"
data-analysis-name="${escapeHtml(analysisName)}"
data-analysis-method="${escapeHtml(methodName)}"
data-analysis-level="${escapeHtml(level)}"
data-web="${selectable ? "1" : "0"}"
data-accredited="${accredited ? "1" : "0"}"
data-search="${escapeHtml(searchText)}"
>
<div class="fw-semibold">${escapeHtml(analysisName)}</div>
${methodName ? `<div class="analysis-analysis-meta mt-1">${escapeHtml(methodName)}</div>` : ""}
<div class="analysis-analysis-badges mt-2">
${badges}
</div>
</div>
`;
})
.join("");
filterAnalysisList();
syncSelectedAnalysisRows();
}
function filterAnalysisList() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const webOnlyEl = modal.querySelector("#analysisWebOnly");
const searchEl = modal.querySelector("#analysisSearchInput");
const items = modal.querySelectorAll(".analysis-analysis-item");
const emptyEl = modal.querySelector("#analysisEmptyBox");
const errorEl = modal.querySelector("#analysisErrorBox");
const webOnly = true;
const searchValue = searchEl ? searchEl.value.trim().toLowerCase() : "";
let visibleCount = 0;
items.forEach((item) => {
const isWeb = item.getAttribute("data-web") === "1";
const searchText = (
item.getAttribute("data-search") || ""
).toLowerCase();
let visible = true;
if (webOnly && !isWeb) {
visible = false;
}
if (visible && searchValue && !searchText.includes(searchValue)) {
visible = false;
}
item.style.display = visible ? "" : "none";
if (visible) {
visibleCount++;
}
});
if (errorEl) {
errorEl.classList.add("d-none");
}
if (emptyEl) {
if (items.length === 0) {
emptyEl.textContent = "No analyses found for this matrix";
emptyEl.classList.remove("d-none");
} else if (visibleCount === 0) {
emptyEl.textContent = "No analyses match the current filters";
emptyEl.classList.remove("d-none");
} else {
emptyEl.classList.add("d-none");
}
}
}
function loadAnalysesByMatrix(matrixId) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
if (!matrixId || matrixId === "NO_MATRIX") {
showAnalysisEmpty("No matrix selected");
return;
}
const listEl = modal.querySelector("#analysisList");
const errorEl = modal.querySelector("#analysisErrorBox");
const emptyEl = modal.querySelector("#analysisEmptyBox");
if (listEl) listEl.innerHTML = "";
if (errorEl) errorEl.classList.add("d-none");
if (emptyEl) {
emptyEl.textContent = "";
emptyEl.classList.add("d-none");
}
const cacheKey = String(matrixId) + "_WEB_ONLY";
if (analysisLoadedCache[cacheKey]) {
renderAnalysesList(analysisLoadedCache[cacheKey]);
return;
}
setAnalysisLoadingState(true);
$.ajax({
url: "get_analisi_matrice_filter.php",
method: "GET",
dataType: "json",
data: {
id_matrice: matrixId,
web_only: 1,
},
})
.done(function (response) {
const analyses = Array.isArray(response.value)
? response.value.filter(function (item) {
const name =
item.NomeAnalisiTraduzione ||
item.NomeAnalisi ||
"";
if (name.trim().startsWith("*")) {
return false;
}
return (
item.SelezionabileSuWeb === true ||
item.SelezionabileSuWeb === 1 ||
item.SelezionabileSuWeb === "1"
);
})
: [];
analysisLoadedCache[cacheKey] = analyses;
renderAnalysesList(analyses);
})
.fail(function (xhr) {
let message = "Error loading analyses";
if (xhr && xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
showAnalysisError(message);
})
.always(function () {
setAnalysisLoadingState(false);
});
}
function updateSelectedPartsInfo() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const checked = modal.querySelectorAll(
".analysis-part-checkbox:checked",
);
const ids = Array.from(checked).map((el) => el.value);
const countEl = modal.querySelector("#analysisSelectedPartsCount");
const idsEl = modal.querySelector("#analysisSelectedPartsIds");
if (countEl) {
countEl.textContent = `${ids.length} selected`;
}
if (idsEl) {
idsEl.textContent = ids.length ? ids.join(", ") : "-";
}
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
const checkbox = item.querySelector(".analysis-part-checkbox");
if (checkbox && checkbox.checked) {
item.classList.add("part-checked");
} else {
item.classList.remove("part-checked");
}
});
}
function selectPartsByMatrix(matrixId, matrixLabel) {
const modal = document.getElementById("analysisModal");
if (!modal) return;
const partItems = modal.querySelectorAll(".analysis-part-item");
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
partItems.forEach((item) => {
const itemMatrixId = item.getAttribute("data-matrix-id");
const checkbox = item.querySelector(".analysis-part-checkbox");
item.classList.remove("matrix-active");
if (itemMatrixId === String(matrixId)) {
item.classList.add("matrix-active");
if (checkbox) checkbox.checked = true;
} else {
if (checkbox) checkbox.checked = false;
}
});
if (matrixLabelEl) {
matrixLabelEl.textContent = matrixLabel || "-";
}
analysisSelectedState = {};
const selectedPartIds = getSelectedPartIds();
selectedPartIds.forEach(function (partId) {
const assigned = Array.isArray(
analysisAssignedState[String(partId)],
)
? analysisAssignedState[String(partId)]
: [];
assigned.forEach(function (item) {
if (item.analysis_recordkey) {
analysisSelectedState[item.analysis_recordkey] = true;
}
});
});
updateSelectedPartsInfo();
loadAnalysesByMatrix(matrixId);
}
function clearAnalysisSelection() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
modal.querySelectorAll(".analysis-matrix-item").forEach((item) => {
item.classList.remove("active");
});
modal.querySelectorAll(".analysis-part-item").forEach((item) => {
item.classList.remove("matrix-active", "part-checked");
});
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
cb.checked = false;
});
const matrixLabelEl = modal.querySelector("#analysisCurrentMatrix");
if (matrixLabelEl) matrixLabelEl.textContent = "-";
const webOnlyEl = modal.querySelector("#analysisWebOnly");
if (webOnlyEl) webOnlyEl.checked = false;
const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) searchEl.value = "";
const listEl = modal.querySelector("#analysisList");
if (listEl) listEl.innerHTML = "";
showAnalysisEmpty("Select a matrix to load analyses");
updateSelectedPartsInfo();
}
function initAnalysisModal() {
const modal = document.getElementById("analysisModal");
if (!modal) return;
analysisLoadedCache = {};
analysisSelectedState = {};
readInitialAssignedAnalyses();
renderAssignedAnalysesForSelectedParts();
modal.querySelectorAll(".analysis-matrix-item").forEach((btn) => {
btn.addEventListener("click", function () {
modal
.querySelectorAll(".analysis-matrix-item")
.forEach((x) => x.classList.remove("active"));
this.classList.add("active");
const matrixId = this.getAttribute("data-matrix-id");
const matrixLabel =
this.getAttribute("data-matrix-name") ||
this.textContent.trim();
selectPartsByMatrix(matrixId, matrixLabel);
});
});
modal.querySelectorAll(".analysis-part-checkbox").forEach((cb) => {
cb.addEventListener("change", function () {
updateSelectedPartsInfo();
});
});
const clearBtn = modal.querySelector("#analysisClearSelectionBtn");
if (clearBtn) {
clearBtn.addEventListener("click", function () {
clearAnalysisSelection();
});
}
// WEB only is now fixed by default
const searchEl = modal.querySelector("#analysisSearchInput");
if (searchEl) {
searchEl.addEventListener("input", function () {
filterAnalysisList();
});
}
modal.addEventListener("click", function (e) {
const removeBtn = e.target.closest(".analysis-remove-btn");
if (removeBtn) {
e.preventDefault();
e.stopPropagation();
const partId = removeBtn.getAttribute("data-part-id");
const recordKey = removeBtn.getAttribute("data-recordkey");
const rowId = removeBtn.getAttribute("data-row-id");
const index = removeBtn.getAttribute("data-index");
deleteAnalysisAssociation(
partId,
recordKey,
rowId,
index,
function () {
const stillUsed = Object.keys(
analysisAssignedState,
).some(function (pid) {
return (
Array.isArray(analysisAssignedState[pid]) &&
analysisAssignedState[pid].some(
function (item) {
return (
item.analysis_recordkey ===
recordKey
);
},
)
);
});
if (!stillUsed) {
delete analysisSelectedState[recordKey];
}
syncSelectedAnalysisRows();
},
);
return;
}
const row = e.target.closest(".analysis-analysis-item");
if (!row) return;
const selectedPartIds = getSelectedPartIds();
if (!selectedPartIds.length) {
alert("Select at least one part first");
return;
}
const payload = buildAnalysisPayloadFromRow(row);
if (!payload.analysis_recordkey) {
alert("Invalid analysis record key");
return;
}
const recordKey = payload.analysis_recordkey;
let pending = selectedPartIds.length;
let atLeastOneSaved = false;
selectedPartIds.forEach(function (partId) {
saveAnalysisAssociation(partId, payload, function (ok) {
if (ok) atLeastOneSaved = true;
pending--;
if (pending <= 0 && atLeastOneSaved) {
analysisSelectedState[recordKey] = true;
syncSelectedAnalysisRows();
}
});
});
});
const firstActiveMatrix = modal.querySelector(
".analysis-matrix-item.active",
);
if (firstActiveMatrix) {
const matrixId = firstActiveMatrix.getAttribute("data-matrix-id");
const matrixLabel =
firstActiveMatrix.getAttribute("data-matrix-name") ||
firstActiveMatrix.textContent.trim();
selectPartsByMatrix(matrixId, matrixLabel);
} else {
updateSelectedPartsInfo();
}
}
// OPEN ANALYSIS MODAL FROM PARTS MODAL BUTTON
$(document).on("click", ".open-analysis-modal-btn", function () {
const partsModal = document.getElementById("partsModal");
const iddatadb = $("#partsModal").data("iddatadb");
if (!iddatadb) {
console.error("iddatadb not found on #partsModal");
alert("iddatadb not found");
return;
}
$.ajax({
url: "modal_analysis.php",
method: "GET",
data: { iddatadb: iddatadb },
success: function (response) {
$("#analysisModalContainer").html(response);
const modalElement = document.getElementById("analysisModal");
if (!modalElement) {
console.error("Analysis modal not found: #analysisModal");
return;
}
let modal = bootstrap.Modal.getInstance(modalElement);
if (!modal) {
modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
}
loadAnalysisMatrixNames().always(function () {
initAnalysisModal();
modal.show();
});
},
error: function (xhr, status, error) {
console.error("Error loading analysis modal:", error);
alert("Error loading analysis modal: " + error);
},
});
});
// CLEANUP ON CLOSE
$(document).on("hidden.bs.modal", "#analysisModal", function () {
const modalElement = document.getElementById("analysisModal");
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) modal.dispose();
}
$("#analysisModalContainer").empty();
// keep parts modal alive, but remove extra backdrop leftovers
$(".modal-backdrop").last().remove();
if ($("#partsModal").hasClass("show")) {
$("body").addClass("modal-open");
} else {
$("body").removeClass("modal-open").css("padding-right", "");
}
});
})();
+515 -146
View File
@@ -12,12 +12,21 @@ $(document).ready(function () {
let photoAnnotations = {};
let partColors = {};
let partSizes = {}; // memorizza la dimensione specifica per parte
let selectedPartNumber = null;
let unsavedChanges = false;
let fabricCanvas = null;
let descriptionTextbox = null;
let markerObjects = {};
let nextMarkerId = 1;
let freeTextMode = false;
let freeTextObjects = {};
let nextFreeTextId = 1;
let partsListData = [];
// DIMENSIONE GLOBALE MARKER
let globalMarkerSize = 16;
// COLORE TESTO LISTA DESCRIZIONI
let globalDescriptionColor = "#000000";
// ===================
// MODAL INITIALIZATION
@@ -29,6 +38,8 @@ $(document).ready(function () {
trfHeader,
});
$("#annotationsModal").attr("data-iddatadb", iddatadb);
if (!iddatadb && !idquotations) {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante. Impossibile inizializzare il modale delle annotazioni.</div>',
@@ -47,7 +58,7 @@ $(document).ready(function () {
.data("iddatadb", iddatadb || null)
.data("idquotations", idquotations || null);
loadPhoto(iddatadb, idquotations);
// Parti (non legate al canvas) possono partire subito
loadExistingParts(iddatadb, idquotations);
const modalElement = document.getElementById("annotationsModal");
@@ -66,7 +77,22 @@ $(document).ready(function () {
focus: true,
});
}
// IMPORTANTE: carica la foto SOLO dopo che il modale è completamente
// visibile (animazione fade terminata). Se l'immagine è già in cache
// del browser, l'evento "load" può scattare quasi subito, PRIMA che
// il contenitore del canvas abbia dimensioni reali (parent ancora
// display:none durante il fade) — questo rompeva il canvas Fabric.js
// per la prima foto mostrata (bug riprodotto solo con foto già in cache,
// tipico del server di test dove le foto erano già state visualizzate).
$(modalElement).one("shown.bs.modal", function () {
loadPhoto(iddatadb, idquotations);
});
modal.show();
// Inizializza slider dimensione marker
$("#markerSizeSlider").val(globalMarkerSize);
$("#markerSizeValue").text(globalMarkerSize + "px");
// Debug: Verifica presenza elementi DOM
console.log(
@@ -121,6 +147,9 @@ $(document).ready(function () {
selectedPartNumber = null;
unsavedChanges = false;
partsListData = [];
freeTextMode = false; // ← aggiungi
freeTextObjects = {}; // ← aggiungi
$("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi
if (fabricCanvas) {
fabricCanvas.off();
fabricCanvas.dispose();
@@ -128,6 +157,8 @@ $(document).ready(function () {
}
descriptionTextbox = null;
markerObjects = {};
globalMarkerSize = 16;
globalDescriptionColor = "#000000";
$("#photoSelectorContainerAnnotations").empty().hide();
$("#samplePhotoAnnotations").attr("src", "");
$("#partsListAnnotations").empty();
@@ -144,6 +175,84 @@ $(document).ready(function () {
$(":focus").blur();
});
// SLIDER DIMENSIONE MARKER
$(document)
.off("input", "#markerSizeSlider")
.on("input", "#markerSizeSlider", function () {
globalMarkerSize = parseInt($(this).val());
$("#markerSizeValue").text(globalMarkerSize + "px");
partsListData.forEach(function (part) {
if (part && part.part_number != null) {
partSizes[part.part_number] = globalMarkerSize;
}
});
$("#partsListAnnotations .marker-size-slider").val(
globalMarkerSize,
);
$("#partsListAnnotations .marker-size-value").text(
globalMarkerSize + "px",
);
updateMarkers();
markUnsaved();
});
// ===================
// TESTO LIBERO — TOGGLE MODALITÀ
// ===================
$(document)
.off("click.freeTextToggle", "#addFreeTextBtnAnnotations")
.on("click.freeTextToggle", "#addFreeTextBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
freeTextMode = !freeTextMode;
$(this).toggleClass("active", freeTextMode);
// Disattiva la selezione parte/marker mentre si aggiunge testo libero
if (freeTextMode) {
selectedPartNumber = null;
$("#partsListAnnotations .list-group-item").removeClass(
"active",
);
}
console.log("Free text mode:", freeTextMode);
});
// ===================
// COLORE LISTA DESCRIZIONI
// ===================
$(document)
.off("input.descriptionColor", "#descriptionColorPickerAnnotations")
.on(
"input.descriptionColor",
"#descriptionColorPickerAnnotations",
function () {
globalDescriptionColor = $(this).val();
if (descriptionTextbox && fabricCanvas) {
descriptionTextbox.set("fill", globalDescriptionColor);
}
// Aggiorna anche i testi liberi già piazzati sulla foto corrente
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (annotations && annotations.freeTexts) {
annotations.freeTexts.forEach((entry) => {
entry.color = globalDescriptionColor;
});
}
Object.values(freeTextObjects).forEach((obj) => {
obj.set("fill", globalDescriptionColor);
});
if (fabricCanvas) fabricCanvas.renderAll();
markUnsaved();
},
);
// ===================
// PHOTO LOADERS
// ===================
@@ -315,7 +424,29 @@ $(document).ready(function () {
console.log(
"Evento mouse:down su canvas, selectedPartNumber:",
selectedPartNumber,
"freeTextMode:",
freeTextMode,
);
// Modalità testo libero: ha priorità, un solo click e si disattiva da sola
if (freeTextMode) {
if (options.target) {
console.log(
"Click su un oggetto esistente, ignoro.",
);
return;
}
const pointer = fabricCanvas.getPointer(options.e);
const x = pointer.x / photoData.scale;
const y = pointer.y / photoData.scale;
addFreeText(x, y);
freeTextMode = false;
$("#addFreeTextBtnAnnotations").removeClass("active");
return;
}
if (selectedPartNumber === null) {
console.log(
"Nessuna parte selezionata, ignoro il click.",
@@ -347,22 +478,13 @@ $(document).ready(function () {
const partColor =
partColors[selectedPartNumber] || "#ff0000";
const existingMarker = photoAnnotations[
currentPhoto
].markers.find((m) => m.partNumber == selectedPartNumber);
if (existingMarker) {
existingMarker.x = x;
existingMarker.y = y;
existingMarker.color = partColor;
} else {
photoAnnotations[currentPhoto].markers.push({
partNumber: selectedPartNumber,
x,
y,
color: partColor,
});
}
photoAnnotations[currentPhoto].markers.push({
id: nextMarkerId++,
partNumber: selectedPartNumber,
x,
y,
color: partColor,
});
console.log("Marker aggiunto/spostato:", {
partNumber: selectedPartNumber,
@@ -372,8 +494,6 @@ $(document).ready(function () {
});
updateMarkers();
markUnsaved();
selectedPartNumber = null;
$("#partsListAnnotations li").removeClass("active");
});
fabricCanvas.upperCanvasEl.focus();
@@ -381,6 +501,7 @@ $(document).ready(function () {
updateMarkers();
updateDescriptions();
updateFreeTexts();
}, 10);
});
}
@@ -465,113 +586,89 @@ $(document).ready(function () {
);
// ===================
// BACK TO PARTS MODAL
// TORNA AL MODALE PARTI (modal_partsTable.php)
// ===================
$(document)
.off("click.backToParts", "#backToPartsBtnAnnotations")
.on("click.backToParts", "#backToPartsBtnAnnotations", function (e) {
e.preventDefault();
e.stopPropagation();
console.log(
"Evento click su #backToPartsBtnAnnotations, ID elemento:",
$(this).attr("id"),
);
if (!$("#backToPartsBtnAnnotations").length) {
console.error(
"Pulsante #backToPartsBtnAnnotations non trovato nel DOM.",
);
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #backToPartsBtnAnnotations non trovato nel DOM.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
// Controlla modifiche non salvate
if (
unsavedChanges &&
!confirm(
"Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?",
)
) {
console.log(
"Tornare al modale delle parti annullato a causa di modifiche non salvate.",
);
return;
}
// Chiudi annotationsModal
const annotationsModalElement =
document.getElementById("annotationsModal");
const annotationsModal = bootstrap.Modal.getInstance(
annotationsModalElement,
);
if (annotationsModal) {
annotationsModal.hide();
} else {
console.error(
"Impossibile trovare l'istanza del modale #annotationsModal.",
);
}
// Apri partsModal
const iddatadb = $("#annotationsModal").data("iddatadb");
const idquotations = $("#annotationsModal").data("idquotations");
const trfHeader = $("#trfHeaderAnnotations").text();
console.log("Apertura partsModal con:", {
// CHIUDI MODALE ANNOTAZIONI IN MODO SICURO
const annotationsModalElement =
document.getElementById("annotationsModal");
if (annotationsModalElement) {
const modalInstance = bootstrap.Modal.getInstance(
annotationsModalElement,
);
if (modalInstance) {
modalInstance.hide();
} else {
$(annotationsModalElement).modal("hide"); // fallback jQuery
}
}
console.log("Torno a modal_partsTable.php con:", {
iddatadb,
idquotations,
trfHeader,
});
// CARICA E APRI MODALE PARTI
$.get(
"modal_partsTable.php",
{
iddatadb: iddatadb || "",
idquotations: idquotations || "",
trfHeader: trfHeader,
},
function (data) {
// Rimuovi vecchio modale (con ID corretto)
$("#partsTableModal").remove();
$(".modal-backdrop").remove();
const partsModalElement = document.getElementById("partsModal");
if (!partsModalElement) {
console.error("Elemento #partsModal non trovato nel DOM.");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Il modale delle parti non è presente nel DOM.</div>',
// Aggiungi nuovo modale
$("body").append(data);
// Apri con Bootstrap 5
const partsModalElement =
document.getElementById("partsTableModal");
if (partsModalElement) {
const modal = new bootstrap.Modal(partsModalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
modal.show();
} else {
let iddatadb =
$("#annotationsModal").attr("data-iddatadb");
$(
"button.parts-btn[data-iddatadb='" +
iddatadb +
"']",
).trigger("click");
}
},
).fail(function (xhr) {
console.error("Errore caricamento modale parti:", xhr);
alert(
"Errore 404: modal_partsTable.php non trovato o errore server.",
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
return;
}
let partsModal = bootstrap.Modal.getInstance(partsModalElement);
if (!partsModal) {
partsModal = new bootstrap.Modal(partsModalElement, {
backdrop: true,
keyboard: true,
focus: true,
});
}
// Inizializza il modale delle parti
if (typeof window.initPartsModal === "function") {
window.initPartsModal(iddatadb, idquotations, trfHeader);
partsModal.show();
console.log("partsModal aperto con successo.");
} else {
console.error("Funzione initPartsModal non definita.");
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore: Funzione initPartsModal non trovata.</div>',
);
$("#annotationsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
errorMsg.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
});
});
// ===================
// PARTS LIST
// ===================
@@ -615,17 +712,11 @@ $(document).ready(function () {
partsListData.forEach((part) => {
const partNumber = part.part_number;
const partDescription = part.part_description;
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
const partColor =
partColors[partNumber] ||
(partDescription.toLowerCase().startsWith("mix")
? "#0000ff"
: "#ff0000");
if (
partNumber &&
partDescription &&
(showMixParts ||
!partDescription.toLowerCase().startsWith("mix"))
) {
partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000");
if (partNumber && partDescription && (showMixParts || !isMixPart)) {
const colorOptions = predefinedColors
.map(
(color) =>
@@ -633,12 +724,19 @@ $(document).ready(function () {
)
.join("");
const listItem = `
<li class="list-group-item" data-part-number="${partNumber}">
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
<div class="color-picker-container" style="display: inline-block; position: relative; overflow: visible; z-index: 2000;">
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; margin-left: 5px; cursor: pointer; pointer-events: auto;"></div>
<div class="color-picker" style="display: none; position: absolute; right: 0; z-index: 2000; background: #fff; border: 1px solid #ccc; padding: 5px;">${colorOptions}</div>
</div>
<li class="list-group-item" data-part-number="${partNumber}">
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
<div class="color-picker-container" style="display:inline-block; position: relative; overflow: visible; z-index: 2000; margin-left:5px;">
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; cursor: pointer; pointer-events: auto;"></div>
<div class="color-picker" style="display: none; position: absolute; right: 0; z-index: 2000; background: #fff; border: 1px solid #ccc; padding: 5px;">${colorOptions}</div>
</div>
<div style="display:inline-flex; align-items:center; gap:4px; margin-left:5px;">
<input type="range" class="marker-size-slider"
min="12" max="48" step="2"
value="${partSizes[partNumber] || globalMarkerSize}"
style="width:70px;">
<span class="marker-size-value" style="font-size:0.8rem;">${partSizes[partNumber] || globalMarkerSize}px</span>
</div>
</li>`;
partsListElement.append(listItem);
}
@@ -663,8 +761,21 @@ $(document).ready(function () {
);
return;
}
const $listItem = $(this);
selectedPartNumber = $listItem.data("part-number");
const partNumber = $listItem.data("part-number");
if (
selectedPartNumber == partNumber &&
$listItem.hasClass("active")
) {
selectedPartNumber = null;
$listItem.removeClass("active");
return;
}
selectedPartNumber = partNumber;
$listItem.addClass("active").siblings().removeClass("active");
console.log(
"Parte selezionata tramite riga:",
@@ -687,30 +798,88 @@ $(document).ready(function () {
$picker.toggle();
});
// === Gestione cambio colore ===
partsListElement
.off("click.colorOption")
.on("click.colorOption", ".color-option", function (e) {
e.stopPropagation();
e.preventDefault();
const $this = $(this);
const color = $this.data("color");
const $listItem = $this.closest("li");
const partNumber = $listItem.data("part-number");
console.log(
"Cliccato .color-option, colore:",
color,
"per parte:",
partNumber,
);
// Salva il nuovo colore
partColors[partNumber] = color;
// Aggiorna il colore visivo nel selettore
$listItem
.find(".selected-color")
.css("background-color", color);
let currentPhoto = $("#samplePhotoAnnotations").attr("src");
let annotations = photoAnnotations[currentPhoto];
if (annotations) {
annotations.markers.forEach(function (m) {
if (m.partNumber == partNumber) {
m.color = color;
let group = markerObjects[m.id];
if (group) {
let circle = group.item(0);
circle.set("fill", color);
circle.set("stroke", color);
}
}
});
fabricCanvas.renderAll();
}
// Chiudi la palette e aggiorna canvas
$this.closest(".color-picker").hide();
updateMarkers();
markUnsaved();
});
// === Slider locale per dimensione marker ===
partsListElement
.off("input.localMarkerSize")
.on("input.localMarkerSize", ".marker-size-slider", function (e) {
e.stopPropagation();
e.preventDefault();
// Riferimenti alla parte e valore scelto
const $slider = $(this);
const partNumber = $slider.closest("li").data("part-number");
const newSize = parseInt($slider.val());
// Aggiorna il valore visivo accanto allo slider
$slider.siblings(".marker-size-value").text(newSize + "px");
// Memorizza la nuova dimensione per quella parte
partSizes[partNumber] = newSize;
// Aggiorna i marker sul canvas
updateMarkers();
// Segnala modifiche non salvate
markUnsaved();
console.log(
`Dimensione marker aggiornata per parte ${partNumber}: ${newSize}px`,
);
});
$(document)
.off("click.colorPicker")
.on("click.colorPicker", function (e) {
@@ -721,6 +890,48 @@ $(document).ready(function () {
});
}
function enableDragDropPartsList() {
const list = $("#partsListAnnotations");
if (!list.length || typeof $.ui === "undefined" || !$.ui.sortable) {
console.warn("jQuery UI o .sortable non disponibile. Ritento...");
setTimeout(enableDragDropPartsList, 100);
return;
}
if (list.hasClass("ui-sortable")) {
list.sortable("destroy"); // evita duplicati
}
list.sortable({
items: "li.list-group-item",
placeholder: "list-group-item placeholder",
axis: "y",
containment: "parent",
tolerance: "pointer",
start: function (e, ui) {
ui.item.addClass("dragging");
},
stop: function (e, ui) {
ui.item.removeClass("dragging");
const newOrder = [];
list.find("li").each(function () {
const partNumber = $(this).data("part-number");
const part = partsListData.find(
(p) => p.part_number == partNumber,
);
if (part) newOrder.push(part);
});
partsListData = newOrder;
console.log(
"Ordine parti aggiornato:",
partsListData.map((p) => p.part_number),
);
markUnsaved();
updateMarkers();
},
});
}
// Delegazione evento per il checkbox
$(document)
.off("change.showMix", "#showMixPartsAnnotations")
@@ -753,6 +964,7 @@ $(document).ready(function () {
}
updatePartsList();
updateMarkers();
setTimeout(enableDragDropPartsList, 50);
if (
photoAnnotations[$("#samplePhotoAnnotations").attr("src")]
?.hasDescriptions
@@ -802,11 +1014,10 @@ $(document).ready(function () {
) {
partsListData = response.parts;
response.parts.forEach((part) => {
const defaultColor = part.part_description
.toLowerCase()
.startsWith("mix")
? "#0000ff"
: "#ff0000";
const isMixPart =
String(part.mix || "N").toUpperCase() === "Y";
const defaultColor = isMixPart ? "#0000ff" : "#ff0000";
partColors[part.part_number] = defaultColor;
});
updatePartsList();
@@ -851,7 +1062,128 @@ $(document).ready(function () {
},
});
}
// ===================
// TESTO LIBERO — CREAZIONE E RENDER
// ===================
function addFreeText(x, y) {
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
if (!photoAnnotations[currentPhoto]) {
photoAnnotations[currentPhoto] = {
markers: [],
freeTexts: [],
hasDescriptions: false,
descriptionPosition: { x: 10, y: 10 },
descriptionSize: {
width: photoData.displayWidth * 0.3,
height: photoData.displayHeight * 0.3,
},
};
}
if (!photoAnnotations[currentPhoto].freeTexts) {
photoAnnotations[currentPhoto].freeTexts = [];
}
const entry = {
id: nextFreeTextId++,
x,
y,
text: "Testo",
color: globalDescriptionColor,
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
};
photoAnnotations[currentPhoto].freeTexts.push(entry);
renderFreeText(entry, true);
markUnsaved();
}
function renderFreeText(entry, startEditing = false) {
const itext = new fabric.IText(entry.text, {
left: entry.x * photoData.scale,
top: entry.y * photoData.scale,
fontFamily: "Arial",
fontSize: entry.fontSize,
fill: entry.color,
selectable: true,
hasControls: true,
editable: true,
borderColor: "#333",
cornerColor: "#000",
cornerSize: 12,
transparentCorners: false,
cornerStyle: "rect",
borderScaleFactor: 2,
});
itext.freeTextId = entry.id;
// FIX: Fabric.js appende la <textarea> nascosta (usata per catturare la
// tastiera durante l'editing) a document.body, FUORI dal modale.
// Il focus trap di Bootstrap la considera "esterna" e ruba il focus
// ogni volta che Fabric lo assegna a quella textarea — così i tasti
// digitati non arrivano mai al testo. Spostiamo la textarea dentro
// il modale non appena l'editing viene avviato.
itext.on("editing:entered", function () {
const modalEl = document.getElementById("annotationsModal");
if (
this.hiddenTextarea &&
modalEl &&
this.hiddenTextarea.parentNode !== modalEl
) {
modalEl.appendChild(this.hiddenTextarea);
}
});
itext.on("moving", function () {
entry.x = this.left / photoData.scale;
entry.y = this.top / photoData.scale;
markUnsaved();
});
itext.on("editing:exited", function () {
entry.text = this.text;
markUnsaved();
});
itext.on("scaling", function () {
entry.fontSize = Math.round(this.fontSize * this.scaleY);
this.set({ fontSize: entry.fontSize, scaleX: 1, scaleY: 1 });
markUnsaved();
});
fabricCanvas.add(itext);
freeTextObjects[entry.id] = itext;
// IMPORTANTE: non attivare l'editing nello stesso evento mouse:down che
// ha creato l'oggetto. Fabric processa quel click come "click su canvas
// vuoto" (il target non esisteva ancora al momento del mousedown) e al
// successivo mouseup deseleziona tutto, interrompendo l'editing appena
// avviato — l'utente non riesce a scrivere né a spostare il testo.
// Rimandiamo l'attivazione al prossimo giro di event loop, così avviene
// DOPO che Fabric ha concluso la gestione nativa di quel click.
if (startEditing) {
setTimeout(() => {
fabricCanvas.setActiveObject(itext);
itext.enterEditing();
itext.selectAll();
fabricCanvas.renderAll();
}, 0);
}
fabricCanvas.renderAll();
}
function updateFreeTexts() {
for (let id in freeTextObjects) {
fabricCanvas.remove(freeTextObjects[id]);
delete freeTextObjects[id];
}
freeTextObjects = {};
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (!annotations || !annotations.freeTexts) return;
annotations.freeTexts.forEach((entry) => renderFreeText(entry, false));
}
// ===================
// MARKERS & DESCRIPTIONS
// ===================
@@ -860,9 +1192,9 @@ $(document).ready(function () {
"updateMarkers chiamato, markerObjects:",
Object.keys(markerObjects),
);
for (let partNumber in markerObjects) {
fabricCanvas.remove(markerObjects[partNumber]);
delete markerObjects[partNumber];
for (let markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
@@ -883,17 +1215,18 @@ $(document).ready(function () {
(p) => p.part_number == marker.partNumber,
);
const partDescription = part ? part.part_description : "";
if (
!showMixParts &&
partDescription &&
partDescription.toLowerCase().startsWith("mix")
) {
const isMixPart =
part && String(part.mix || "N").toUpperCase() === "Y";
if (!showMixParts && isMixPart) {
console.log("Ignoro marker per parte Mix:", marker.partNumber);
return;
}
const radius = 12;
const fontSize = 16;
const size = partSizes[marker.partNumber] || globalMarkerSize;
const radius = size / 2;
const fontSize = Math.max(10, Math.round(size * 0.65));
const markerColor =
marker.color || partColors[marker.partNumber] || "#ff0000";
@@ -941,6 +1274,9 @@ $(document).ready(function () {
lockRotation: true,
});
group.markerId = marker.id;
group.partNumber = marker.partNumber;
group.on("moving", function () {
marker.x = this.left / photoData.scale;
marker.y = this.top / photoData.scale;
@@ -953,7 +1289,7 @@ $(document).ready(function () {
});
fabricCanvas.add(group);
markerObjects[marker.partNumber] = group;
markerObjects[marker.id] = group;
});
fabricCanvas.renderAll();
@@ -990,11 +1326,10 @@ $(document).ready(function () {
}
const partsList = partsListData
.filter(
(part) =>
showMixParts ||
!part.part_description.toLowerCase().startsWith("mix"),
)
.filter((part) => {
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
return showMixParts || !isMixPart;
})
.map((part) => `${part.part_number} ${part.part_description}`);
const text = partsList.join("\n");
@@ -1013,8 +1348,8 @@ $(document).ready(function () {
scaleY: 1,
backgroundColor: "transparent",
fontFamily: "Arial",
fontSize: 24,
fill: "#000000",
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
fill: globalDescriptionColor,
padding: 10,
editable: false,
selectable: true,
@@ -1107,9 +1442,9 @@ $(document).ready(function () {
}
}
for (let partNumber in markerObjects) {
fabricCanvas.remove(markerObjects[partNumber]);
delete markerObjects[partNumber];
for (let markerId in markerObjects) {
fabricCanvas.remove(markerObjects[markerId]);
delete markerObjects[markerId];
}
markerObjects = {};
@@ -1144,9 +1479,9 @@ $(document).ready(function () {
photoAnnotations[currentPhoto].markers.length > 0
) {
const lastMarker = photoAnnotations[currentPhoto].markers.pop();
if (markerObjects[lastMarker.partNumber]) {
fabricCanvas.remove(markerObjects[lastMarker.partNumber]);
delete markerObjects[lastMarker.partNumber];
if (markerObjects[lastMarker.id]) {
fabricCanvas.remove(markerObjects[lastMarker.id]);
delete markerObjects[lastMarker.id];
fabricCanvas.renderAll();
}
console.log("Ultimo marker rimosso:", lastMarker);
@@ -1154,6 +1489,40 @@ $(document).ready(function () {
}
}
// ===================
// TESTO LIBERO — RIMOZIONE CON CANC/DELETE
// ===================
$(document).on("keydown.freeTextDelete", function (e) {
// Solo se il modale annotazioni è aperto
if (!$("#annotationsModal").hasClass("show")) return;
if (!fabricCanvas) return;
// Ignora se si sta scrivendo dentro un campo di editing testo
// (es. l'utente sta modificando il contenuto dell'IText stesso)
const active = fabricCanvas.getActiveObject();
if (active && active.isEditing) return;
if (e.key !== "Delete" && e.key !== "Backspace") return;
if (!active || active.freeTextId === undefined) return;
e.preventDefault();
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
const annotations = photoAnnotations[currentPhoto];
if (annotations && annotations.freeTexts) {
annotations.freeTexts = annotations.freeTexts.filter(
(entry) => entry.id !== active.freeTextId,
);
}
fabricCanvas.remove(active);
delete freeTextObjects[active.freeTextId];
fabricCanvas.renderAll();
markUnsaved();
});
// Delegazione evento per il pulsante "Descrizioni"
$(document)
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
+3
View File
@@ -3,6 +3,9 @@ ob_start();
session_start();
require_once '../../vendor/autoload.php';
Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
$response = ['error' => '', 'rows' => [], 'columns' => [], 'template_id' => 0, 'filename' => '', 'excel_data' => []];
try {
@@ -24,7 +24,17 @@ class VisualLimsApiClient
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new VisualLimsApiClient();
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 3));
$dotenv->load();
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
if ($simulate) {
require_once __DIR__ . '/VisualLimsApiClientMock.class.php';
self::$instance = new VisualLimsApiClientMock();
} else {
self::$instance = new VisualLimsApiClient();
}
}
return self::$instance;
}
@@ -193,8 +203,91 @@ class VisualLimsApiClient
return json_decode($response, true);
}
/**
* POST a file as multipart/form-data (used for photo/attachment uploads).
*
* @param string $endpoint OData endpoint, e.g. "Campione(613388)/UploadCampioneFile"
* @param string $filePath Absolute path to the file on disk
* @param string $fileName Original file name to send
* @param array $extraFields Additional form fields to include
* @return array|null Decoded JSON response
*/
public function postMultipart($endpoint, $filePath, $fileName, array $extraFields = [])
{
$token = $this->getToken();
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', $fileName);
$payload = array_merge($extraFields, [
'file' => $cfile,
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/json",
// Content-Type is set automatically to multipart/form-data by cURL
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore nella richiesta POST multipart: {$curl_error}");
}
if ($http_code < 200 || $http_code >= 300) {
throw new Exception("POST multipart fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
}
return json_decode($response, true);
}
public function getBaseUrl()
{
return $this->baseUrl;
}
/**
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
// IMPORTANTE: usa /odata/ e NON /api/odata/
$url = "{$this->baseUrl}/odata/{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: */*"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL: " . $curl_error);
}
if ($http_code !== 200) {
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
}
return $response;
}
}
@@ -0,0 +1,135 @@
<?php
/**
* Mock implementation of VisualLimsApiClient.
* Activated when SIMULATE_EXPORT_LIMS=true in .env.
* All HTTP calls are skipped; fake but structurally valid data is returned.
* Every simulated call is logged via error_log() with a [SIMULATE] prefix.
*/
class VisualLimsApiClientMock
{
private int $fakeCommessaId;
public function __construct()
{
// Stable fake ID for the lifetime of a single request
$this->fakeCommessaId = mt_rand(90001, 99999);
error_log("[SIMULATE] VisualLimsApiClientMock initialised (fakeCommessaId={$this->fakeCommessaId})");
}
public function get(string $endpoint): array
{
error_log("[SIMULATE] GET {$endpoint}");
// --- Fixed-field dropdown lists ---
if (str_starts_with($endpoint, 'MoltiplicatorePrezzi')) {
return ['value' => [
['IdMoltiplicatorePrezzo' => 1, 'Codice' => 'MP-01', 'Descrizione' => 'Standard (1x)'],
['IdMoltiplicatorePrezzo' => 2, 'Codice' => 'MP-02', 'Descrizione' => 'Urgente (1.5x)'],
['IdMoltiplicatorePrezzo' => 3, 'Codice' => 'MP-03', 'Descrizione' => 'Extra Urgente (2x)'],
]];
}
if (str_starts_with($endpoint, 'AnagraficaCertestObject')) {
return ['value' => [
['IdAnagrafica' => 1, 'Codice' => 'OBJ-01', 'NomeAnagrafica' => 'Articolo Tessile'],
['IdAnagrafica' => 2, 'Codice' => 'OBJ-02', 'NomeAnagrafica' => 'Componente Meccanico'],
['IdAnagrafica' => 3, 'Codice' => 'OBJ-03', 'NomeAnagrafica' => 'Materiale Plastico'],
]];
}
if (str_starts_with($endpoint, 'AnagraficaCertestService')) {
return ['value' => [
['IdAnagrafica' => 1, 'Codice' => 'SRV-01', 'NomeAnagrafica' => 'Analisi Chimica'],
['IdAnagrafica' => 2, 'Codice' => 'SRV-02', 'NomeAnagrafica' => 'Test Meccanico'],
['IdAnagrafica' => 3, 'Codice' => 'SRV-03', 'NomeAnagrafica' => 'Prova Ambientale'],
]];
}
// Cliente? list — get_clienti.php exits early in simulate mode, but guard here too
if (str_starts_with($endpoint, 'Cliente?')) {
return ['value' => []];
}
// Cliente(N)?$expand=Responsabili
if (str_starts_with($endpoint, 'Cliente(')) {
preg_match('/Cliente\((\d+)\)/', $endpoint, $m);
$clienteId = isset($m[1]) ? (int) $m[1] : 0;
return [
'IdCliente' => $clienteId,
'Responsabili' => [
['IdClienteResponsabile' => 1, 'Nominativo' => 'Marco Bianchi'],
['IdClienteResponsabile' => 2, 'Nominativo' => 'Giulia Ferrari'],
['IdClienteResponsabile' => 3, 'Nominativo' => 'Andrea Russo'],
],
];
}
// --- CustomField dropdown values (get_customfield_values.php) ---
if (str_starts_with($endpoint, 'CustomField(')) {
preg_match('/CustomField\((\d+)\)/', $endpoint, $m);
$fieldId = isset($m[1]) ? (int) $m[1] : 0;
return [
'CustomFieldsValues' => [
['IdCustomFieldsValue' => $fieldId * 10 + 1, 'Valore' => 'Opzione A'],
['IdCustomFieldsValue' => $fieldId * 10 + 2, 'Valore' => 'Opzione B'],
['IdCustomFieldsValue' => $fieldId * 10 + 3, 'Valore' => 'Opzione C'],
],
];
}
// --- CommessaWeb OData calls (STEP 7 GET + STEP 10 verification) ---
preg_match('/\((\d+)\)/', $endpoint, $m);
$id = isset($m[1]) ? (int) $m[1] : $this->fakeCommessaId;
return [
'IdCommessa' => $id,
'CodiceCommessa' => "SIM-{$id}",
'CommesseCustomFields' => [], // Empty → PATCH step is skipped correctly
];
}
public function post(string $endpoint, array $payload): array
{
error_log("[SIMULATE] POST {$endpoint} payload=" . json_encode($payload));
// CommessaWeb creation
if ($endpoint === 'CommessaWeb') {
return [
'IdCommessa' => $this->fakeCommessaId,
'CodiceCommessa' => "SIM-{$this->fakeCommessaId}",
'Richiedente' => $payload['Richiedente'] ?? '',
'Descrizione' => $payload['Descrizione'] ?? '',
];
}
// Campione creation
if ($endpoint === 'Campione') {
return [
'IdCampione' => mt_rand(10001, 19999),
'Commessa' => $payload['Commessa'] ?? null,
'Matrice' => $payload['Matrice'] ?? null,
];
}
// InviaCommessa / ImportaCommessa (currently commented out upstream)
return ['simulated' => true, 'endpoint' => $endpoint];
}
public function patch(string $endpoint, array $payload): array
{
error_log("[SIMULATE] PATCH {$endpoint} payload=" . json_encode($payload));
return [];
}
public function postMultipart(string $endpoint, string $filePath, string $fileName, array $extraFields = []): array
{
error_log("[SIMULATE] POST multipart {$endpoint} file={$fileName}");
return ['simulated' => true, 'file' => $fileName];
}
}
+3
View File
@@ -3,6 +3,9 @@ require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
use Dotenv\Dotenv;
Dotenv::createImmutable(dirname(__DIR__, 3))->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
class DBHandlerSelect
{
private static $instance = null;
@@ -0,0 +1,152 @@
<?php
/**
* debug_odata_metadata.php TEMPORANEO, da rimuovere dopo il test.
* Scopo: leggere $metadata (XML standard OData) per vedere:
* - tutti gli EntitySet disponibili (non solo "Cliente")
* - tutti i campi (Property) definiti per l'EntityType Cliente
* Questo bypassa qualunque vista/proiezione ristretta lato server
* applicata sull'endpoint "Cliente" normale.
*/
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
use Dotenv\Dotenv;
header('Content-Type: text/plain; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->load();
$baseUrl = $_ENV['API_BASE_URL'];
$username = $_ENV['API_USERNAME'];
$password = $_ENV['API_PASSWORD'];
$scriptStart = microtime(true);
// ── 1. Autenticazione (stessa logica della classe originale) ──────────────
$ch = curl_init("{$baseUrl}/api/authentication/authenticate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'Username' => $username,
'Password' => $password
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$authResponse = curl_exec($ch);
$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($authHttpCode !== 200) {
echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n";
exit;
}
$tokenData = json_decode($authResponse, true);
$token = null;
if (is_array($tokenData) && isset($tokenData['token'])) {
$token = $tokenData['token'];
} elseif (is_string($tokenData)) {
$token = trim($tokenData, '"');
} else {
$token = trim($authResponse, '"');
}
if (empty($token)) {
echo "TOKEN NOT RECEIVED\n{$authResponse}\n";
exit;
}
$authElapsed = microtime(true) - $scriptStart;
// ── 2. Richiesta $metadata (XML, non JSON) ─────────────────────────────────
// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto.
$cacheBuster = uniqid('cb_', true);
$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}";
$metadataStart = microtime(true);
$ch = curl_init($metadataUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/xml"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione
$rawResponse = curl_exec($ch);
$curlInfo = curl_getinfo($ch);
$httpCode = $curlInfo['http_code'];
$curlTotalTime = $curlInfo['total_time'];
$curlError = curl_error($ch);
$headerSize = $curlInfo['header_size'];
curl_close($ch);
$metadataElapsed = microtime(true) - $metadataStart;
if ($rawResponse === false || $httpCode !== 200) {
echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n";
exit;
}
$responseHeaders = substr($rawResponse, 0, $headerSize);
$xml = substr($rawResponse, $headerSize);
echo "==================== TIMING (prova che NON è cache) ====================\n";
echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n";
echo "Cache-buster usato: {$cacheBuster}\n";
echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n";
echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n";
echo $responseHeaders . "\n";
// ── 3. Estrai tutti gli EntitySet disponibili ──────────────────────────────
echo "==================== ENTITY SETS DISPONIBILI ====================\n";
if (preg_match_all('/<EntitySet Name="([^"]+)"/', $xml, $matches)) {
foreach ($matches[1] as $name) {
echo "- {$name}\n";
}
} else {
echo "(nessun EntitySet trovato — controlla il formato XML grezzo sotto)\n";
}
// ── 4. Estrai tutte le Property dell'EntityType "Cliente" (e simili) ──────
echo "\n==================== CAMPI PER ENTITYTYPE CHE CONTENGONO 'Client' ====================\n";
if (preg_match_all('/<EntityType Name="([^"]*[Cc]lient[^"]*)"[^>]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) {
foreach ($entityMatches as $entity) {
$entityName = $entity[1];
$entityBody = $entity[2];
echo "\n--- EntityType: {$entityName} ---\n";
if (preg_match_all('/<Property Name="([^"]+)" Type="([^"]+)"/', $entityBody, $propMatches, PREG_SET_ORDER)) {
foreach ($propMatches as $prop) {
echo " {$prop[1]} ({$prop[2]})\n";
}
}
}
} else {
echo "(nessun EntityType con 'Client' nel nome trovato)\n";
}
// ── 5. Salva l'XML grezzo completo per ispezione manuale, se serve ────────
file_put_contents(__DIR__ . '/metadata_debug.xml', $xml);
echo "\n\nXML completo salvato in: metadata_debug.xml (rimuovilo dopo il test)\n";
+200
View File
@@ -0,0 +1,200 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0;
$targetList = $data['target_iddatadb_list'] ?? [];
// Default true: se il client non manda il parametro, cloniamo comunque le note
// (comportamento di default richiesto, coerente con la checkbox pre-spuntata)
$cloneNotes = array_key_exists('clone_notes', $data) ? !empty($data['clone_notes']) : true;
$cloneAnalyses = !empty($data['clone_analyses']);
$overwrite = !empty($data['overwrite']);
$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) {
return $v > 0 && $v !== $sourceIddatadb;
})));
if ($sourceIddatadb <= 0 || empty($targetIds)) {
echo json_encode([
'success' => false,
'message' => 'Missing source or target records'
]);
exit;
}
try {
$pdo->beginTransaction();
// 1. Load source parts
$stmtParts = $pdo->prepare("
SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry
FROM identification_parts
WHERE iddatadb = ?
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY part_number ASC, id ASC
");
$stmtParts->execute([$sourceIddatadb]);
$sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC);
if (empty($sourceParts)) {
$pdo->rollBack();
echo json_encode([
'success' => false,
'message' => 'No parts found for source record'
]);
exit;
}
// 2. Prepare statements
$stmtInsertPart = $pdo->prepare("
INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES
(:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())
");
$stmtLoadCF = $pdo->prepare("
SELECT field_id, value_id, value_text
FROM identification_parts_customfields
WHERE part_id = ?
ORDER BY id ASC
");
$stmtLoadAnalyses = $pdo->prepare("
SELECT idmatrice, analysis_recordkey, analysis_name, analysis_method,
analysis_level, is_web_selectable, is_accredited
FROM identification_parts_analyses
WHERE part_id = ?
ORDER BY id ASC
");
$stmtInsertAnalysis = $pdo->prepare("
INSERT INTO identification_parts_analyses
(part_id, iddatadb, idmatrice, analysis_recordkey, analysis_name, analysis_method,
analysis_level, is_web_selectable, is_accredited, created_at, updated_at)
VALUES
(:part_id, :iddatadb, :idmatrice, :analysis_recordkey, :analysis_name, :analysis_method,
:analysis_level, :is_web_selectable, :is_accredited, NOW(), NOW())
");
$stmtInsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields
(part_id, field_id, value_id, value_text, created_at, updated_at)
VALUES
(:part_id, :field_id, :value_id, :value_text, NOW(), NOW())
");
$details = [];
$totalClonedParts = 0;
$totalClonedAnalyses = 0;
$totalDeletedParts = 0;
// 2b. Se overwrite: elimina le parti esistenti dei target (e i loro figli)
if ($overwrite) {
$stmtSelectExisting = $pdo->prepare("SELECT id FROM identification_parts WHERE iddatadb = ?");
$stmtDeleteCFByPart = $pdo->prepare("DELETE FROM identification_parts_customfields WHERE part_id = ?");
$stmtDeleteAnByPart = $pdo->prepare("DELETE FROM identification_parts_analyses WHERE part_id = ?");
$stmtDeletePart = $pdo->prepare("DELETE FROM identification_parts WHERE id = ?");
foreach ($targetIds as $targetIddatadb) {
$stmtSelectExisting->execute([$targetIddatadb]);
$existingIds = $stmtSelectExisting->fetchAll(PDO::FETCH_COLUMN);
foreach ($existingIds as $existingPartId) {
$existingPartId = (int)$existingPartId;
$stmtDeleteCFByPart->execute([$existingPartId]);
$stmtDeleteAnByPart->execute([$existingPartId]);
$stmtDeletePart->execute([$existingPartId]);
$totalDeletedParts++;
}
}
}
// 3. Clone source parts to each target record
foreach ($targetIds as $targetIddatadb) {
$clonedCountForTarget = 0;
foreach ($sourceParts as $part) {
$stmtInsertPart->execute([
':iddatadb' => $targetIddatadb,
':part_number' => $part['part_number'],
':part_description' => $part['part_description'],
':mix' => $part['mix'] ?? 'N',
':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null,
':note' => $cloneNotes ? ($part['note'] ?? null) : null,
':dateexpiry' => $part['dateexpiry'] ?? null,
]);
$newPartId = (int)$pdo->lastInsertId();
// Load source custom fields for this part
$stmtLoadCF->execute([(int)$part['id']]);
$customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC);
foreach ($customFields as $cf) {
$stmtInsertCF->execute([
':part_id' => $newPartId,
':field_id' => (int)$cf['field_id'],
':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null,
':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null,
]);
}
// Clone linked analyses, if requested
if ($cloneAnalyses) {
$stmtLoadAnalyses->execute([(int)$part['id']]);
$analyses = $stmtLoadAnalyses->fetchAll(PDO::FETCH_ASSOC);
foreach ($analyses as $an) {
$stmtInsertAnalysis->execute([
':part_id' => $newPartId,
':iddatadb' => $targetIddatadb,
':idmatrice' => ($an['idmatrice'] !== null && $an['idmatrice'] !== '') ? (int)$an['idmatrice'] : null,
':analysis_recordkey' => $an['analysis_recordkey'],
':analysis_name' => $an['analysis_name'] ?? null,
':analysis_method' => $an['analysis_method'] ?? null,
':analysis_level' => ($an['analysis_level'] !== null && $an['analysis_level'] !== '') ? (int)$an['analysis_level'] : null,
':is_web_selectable' => (int)($an['is_web_selectable'] ?? 0),
':is_accredited' => (int)($an['is_accredited'] ?? 0),
]);
$totalClonedAnalyses++;
}
}
$clonedCountForTarget++;
$totalClonedParts++;
}
$details[] = [
'target_iddatadb' => $targetIddatadb,
'cloned_parts' => $clonedCountForTarget
];
}
$pdo->commit();
echo json_encode([
'success' => true,
'source_iddatadb' => $sourceIddatadb,
'cloned_targets' => count($targetIds),
'total_cloned_parts' => $totalClonedParts,
'total_cloned_analyses' => $totalClonedAnalyses,
'details' => $details
]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => 'Clone failed: ' . $e->getMessage()
]);
}
+228
View File
@@ -0,0 +1,228 @@
<?php
require_once 'class/db-functions.php';
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
die("Invalid template ID");
}
$sourceTemplateId = (int)$_GET['id'];
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$pdo->beginTransaction();
// 1. Get source template
$stmt = $pdo->prepare("
SELECT
name,
source_type,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
FROM excel_templates
WHERE id = ?
LIMIT 1
");
$stmt->execute([$sourceTemplateId]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
throw new Exception("Template not found.");
}
// 2. Generate unique clone name
$baseName = 'Copia di ' . $template['name'];
$newName = $baseName;
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM excel_templates WHERE name = ?");
$checkStmt->execute([$newName]);
if ((int)$checkStmt->fetchColumn() > 0) {
$counter = 2;
do {
$newName = $baseName . ' (' . $counter . ')';
$checkStmt->execute([$newName]);
$exists = (int)$checkStmt->fetchColumn() > 0;
$counter++;
} while ($exists);
}
// 3. Insert cloned template
$insertTemplate = $pdo->prepare("
INSERT INTO excel_templates (
name,
source_type,
created_at,
updated_at,
header_row,
start_column,
description,
target_table,
sample_xlsx,
button_size,
button_bg_color,
button_text_color,
button_label,
status,
client_specific_fields,
idclient,
clientname,
schemaname,
idschema,
schemajson,
xls_headers,
api_sample_json,
json_nodes,
idroutine
) VALUES (
:name,
:source_type,
NOW(),
NOW(),
:header_row,
:start_column,
:description,
:target_table,
:sample_xlsx,
:button_size,
:button_bg_color,
:button_text_color,
:button_label,
:status,
:client_specific_fields,
:idclient,
:clientname,
:schemaname,
:idschema,
:schemajson,
:xls_headers,
:api_sample_json,
:json_nodes,
:idroutine
)
");
$insertTemplate->execute([
':name' => $newName,
':source_type' => $template['source_type'],
':header_row' => $template['header_row'],
':start_column' => $template['start_column'],
':description' => $template['description'],
':target_table' => $template['target_table'],
':sample_xlsx' => $template['sample_xlsx'],
':button_size' => $template['button_size'],
':button_bg_color' => $template['button_bg_color'],
':button_text_color' => $template['button_text_color'],
':button_label' => $template['button_label'],
':status' => $template['status'],
':client_specific_fields' => $template['client_specific_fields'],
':idclient' => $template['idclient'],
':clientname' => $template['clientname'],
':schemaname' => $template['schemaname'],
':idschema' => $template['idschema'],
':schemajson' => $template['schemajson'],
':xls_headers' => $template['xls_headers'],
':api_sample_json' => $template['api_sample_json'],
':json_nodes' => $template['json_nodes'],
':idroutine' => $template['idroutine']
]);
$newTemplateId = (int)$pdo->lastInsertId();
if ($newTemplateId <= 0) {
throw new Exception("Unable to create cloned template.");
}
// 4. Clone template_mapping rows
$cloneMappings = $pdo->prepare("
INSERT INTO template_mapping (
template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
)
SELECT
:new_template_id,
schema_id,
field_id,
excel_column,
json_node,
is_manual,
manual_default,
auto_value,
data_type,
is_required,
default_value,
has_list,
length,
decimals,
min_value,
max_value,
default_curr_date,
tablename,
field_label,
main_field,
is_visible_import,
is_visible_parts
FROM template_mapping
WHERE template_id = :source_template_id
");
$cloneMappings->execute([
':new_template_id' => $newTemplateId,
':source_template_id' => $sourceTemplateId
]);
$pdo->commit();
header("Location: templates_dashboard.php?cloned=1&new_id=" . $newTemplateId);
exit;
} catch (Exception $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
die("Clone error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
}
+75
View File
@@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$templateId = (int)($input['template_id'] ?? 0);
if ($templateId <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid template_id']);
exit;
}
// If already exists, do nothing
$stmt = $pdo->prepare("SELECT COUNT(*) FROM template_fixed_mapping WHERE template_id = ?");
$stmt->execute([$templateId]);
if ((int)$stmt->fetchColumn() > 0) {
echo json_encode(['success' => true, 'created' => 0, 'message' => 'Fixed fields already exist']);
exit;
}
/**
* FIXED FIELDS STANDARD
*/
$fixedFields = [
['ClienteResponsabile', 'INT'],
['ClienteFornitore', 'INT'],
['ClienteAnalisi', 'INT'],
['ClienteFatturazione', 'INT'], // ← NUOVO
['MoltiplicatorePrezzo', 'INT'],
['AnagraficaCertestObject', 'INT'],
['AnagraficaCertestService', 'INT'],
['ConsegnaRichiesta', 'DATE'],
];
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("
INSERT INTO template_fixed_mapping
(template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import)
VALUES
(:template_id, :fixed_field_key, 1, :data_type, 1, NULL, 1)
");
foreach ($fixedFields as $f) {
$ins->execute([
':template_id' => $templateId,
':fixed_field_key' => $f[0],
':data_type' => $f[1],
]);
}
$pdo->commit();
// Return rows (for client render)
$stmt = $pdo->prepare("
SELECT id, template_id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
$stmt->execute([$templateId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'created' => count($rows), 'rows' => $rows]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,8 @@ try {
exit(1);
}
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Recupera le variabili d'ambiente
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_DATABASE'];
+1
View File
@@ -0,0 +1 @@
https://93.43.5.102/limsapi/api/odata/Matrice
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
2026-04-03 10:07:01 - Aggiornamento completato: 3198 record inseriti.
File diff suppressed because one or more lines are too long
+290
View File
@@ -0,0 +1,290 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
// Force HTML response
header('Content-Type: text/html; charset=UTF-8');
// Initialize variables
$error = null;
$result = null;
$entityType = $_GET['entity_type'] ?? 'CommessaWeb';
$entityId = isset($_GET['entity_id']) ? (int)$_GET['entity_id'] : 0;
$customExpand = trim($_GET['expand'] ?? '');
$rawEndpoint = null;
function h($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function renderArrayAsTable(array $items)
{
if (empty($items)) {
echo '<div class="alert alert-secondary">No records found</div>';
return;
}
$firstRow = reset($items);
if (!is_array($firstRow)) {
echo '<pre class="bg-light p-3 border rounded">' . h(json_encode($items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '</pre>';
return;
}
$headers = [];
foreach ($items as $row) {
if (is_array($row)) {
$headers = array_unique(array_merge($headers, array_keys($row)));
}
}
echo '<div class="table-responsive">';
echo '<table class="table table-striped table-bordered table-sm align-middle">';
echo '<thead class="table-dark"><tr>';
foreach ($headers as $header) {
echo '<th>' . h($header) . '</th>';
}
echo '</tr></thead><tbody>';
foreach ($items as $row) {
echo '<tr>';
foreach ($headers as $header) {
$value = $row[$header] ?? '';
if (is_array($value) || is_object($value)) {
$value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo '<td><pre style="white-space:pre-wrap; margin:0;">' . h($value) . '</pre></td>';
} else {
echo '<td>' . h($value) . '</td>';
}
}
echo '</tr>';
}
echo '</tbody></table>';
echo '</div>';
}
try {
if ($entityId > 0) {
$api = VisualLimsApiClient::getInstance();
// Default expands for better debugging
if ($customExpand !== '') {
$expand = $customExpand;
} else {
if ($entityType === 'CommessaWeb') {
$expand = implode(',', [
'CommesseCustomFields($expand=CustomField)',
'Campioni'
]);
} else {
$expand = implode(',', [
'CommesseCustomFields($expand=CustomField)',
'Campioni'
]);
}
}
$rawEndpoint = $entityType . '(' . $entityId . ')?$expand=' . $expand;
$result = $api->get($rawEndpoint);
}
} catch (Exception $e) {
$error = $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Debug Commessa API</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background: #f5f7fb;
font-family: Arial, sans-serif;
}
.debug-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
padding: 20px;
margin-bottom: 20px;
}
.section-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 15px;
}
pre {
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
white-space: pre-wrap;
word-break: break-word;
}
.label-box {
display: inline-block;
padding: 6px 10px;
border-radius: 8px;
background: #eef3ff;
color: #2446a8;
font-weight: 600;
margin-right: 8px;
margin-bottom: 8px;
}
.mini-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.mini-box {
background: #f8fafc;
border: 1px solid #e4e7eb;
border-radius: 10px;
padding: 12px;
}
.mini-box strong {
display: block;
margin-bottom: 6px;
color: #1f2937;
}
</style>
</head>
<body>
<div class="container py-4">
<div class="debug-card">
<h2 class="mb-3">Commessa / CommessaWeb API Inspector</h2>
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">Entity Type</label>
<select name="entity_type" class="form-select">
<option value="CommessaWeb" <?= $entityType === 'CommessaWeb' ? 'selected' : '' ?>>CommessaWeb</option>
<option value="Commessa" <?= $entityType === 'Commessa' ? 'selected' : '' ?>>Commessa</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Entity ID</label>
<input type="number" name="entity_id" class="form-control" value="<?= h($entityId) ?>" required>
</div>
<div class="col-md-5">
<label class="form-label">Expand</label>
<input type="text" name="expand" class="form-control" value="<?= h($customExpand) ?>" placeholder="CommesseCustomFields($expand=CustomField),Campioni">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Load Data</button>
</div>
</form>
</div>
<?php if ($rawEndpoint): ?>
<div class="debug-card">
<div class="section-title">Requested Endpoint</div>
<pre><?= h($rawEndpoint) ?></pre>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="debug-card">
<div class="alert alert-danger mb-0">
<strong>API Error:</strong><br>
<?= h($error) ?>
</div>
</div>
<?php endif; ?>
<?php if ($result && is_array($result)): ?>
<div class="debug-card">
<div class="section-title">Main Information</div>
<div class="mini-grid">
<div class="mini-box">
<strong>ID</strong>
<?= h($result['IdCommessa'] ?? $result['IdCommessaWeb'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Code</strong>
<?= h($result['CodiceCommessa'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Cliente</strong>
<?= h($result['Cliente'] ?? '') ?>
</div>
<div class="mini-box">
<strong>SchemaCustomField</strong>
<?= h($result['SchemaCustomField'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Richiedente</strong>
<?= h($result['Richiedente'] ?? '') ?>
</div>
<div class="mini-box">
<strong>Descrizione</strong>
<?= h($result['Descrizione'] ?? '') ?>
</div>
</div>
</div>
<div class="debug-card">
<div class="section-title">Direct Properties</div>
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
</div>
<?php if (!empty($result['CommesseCustomFields']) && is_array($result['CommesseCustomFields'])): ?>
<div class="debug-card">
<div class="section-title">CommesseCustomFields</div>
<?php
$fieldsRows = [];
foreach ($result['CommesseCustomFields'] as $field) {
$fieldsRows[] = [
'IdCommesseCustomFields' => $field['IdCommesseCustomFields'] ?? '',
'Valore' => $field['Valore'] ?? '',
'CustomFieldId' => $field['CustomField']['IdCustomField'] ?? '',
'Label' => $field['CustomField']['Descrizione'] ?? ($field['CustomField']['Name'] ?? ''),
'Tipo' => $field['CustomField']['Tipo'] ?? '',
];
}
renderArrayAsTable($fieldsRows);
?>
</div>
<?php endif; ?>
<?php if (!empty($result['Campioni']) && is_array($result['Campioni'])): ?>
<div class="debug-card">
<div class="section-title">Campioni</div>
<?php renderArrayAsTable($result['Campioni']); ?>
</div>
<?php endif; ?>
<div class="debug-card">
<div class="section-title">Raw JSON</div>
<pre><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
</div>
<?php elseif ($entityId > 0 && !$error): ?>
<div class="debug-card">
<div class="alert alert-warning mb-0">No data returned from API</div>
</div>
<?php endif; ?>
</div>
</body>
</html>
<!-- Example of use
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779
debug_commessa_api.php?entity_type=Commessa&entity_id=12345
debug_commessa_api.php?entity_type=CommessaWeb&entity_id=564779&expand=CommesseCustomFields($expand=CustomField),Campioni
-->
+152
View File
@@ -0,0 +1,152 @@
<?php
/**
* debug_odata_metadata.php TEMPORANEO, da rimuovere dopo il test.
* Scopo: leggere $metadata (XML standard OData) per vedere:
* - tutti gli EntitySet disponibili (non solo "Cliente")
* - tutti i campi (Property) definiti per l'EntityType Cliente
* Questo bypassa qualunque vista/proiezione ristretta lato server
* applicata sull'endpoint "Cliente" normale.
*/
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
use Dotenv\Dotenv;
header('Content-Type: text/plain; charset=utf-8');
ini_set('display_errors', '1');
error_reporting(E_ALL);
$dotenv = Dotenv::createImmutable(dirname(__DIR__, 2));
$dotenv->load();
$baseUrl = $_ENV['API_BASE_URL'];
$username = $_ENV['API_USERNAME'];
$password = $_ENV['API_PASSWORD'];
$scriptStart = microtime(true);
// ── 1. Autenticazione (stessa logica della classe originale) ──────────────
$ch = curl_init("{$baseUrl}/api/authentication/authenticate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'Username' => $username,
'Password' => $password
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$authResponse = curl_exec($ch);
$authHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($authHttpCode !== 200) {
echo "AUTH FAILED: HTTP {$authHttpCode}\n{$authResponse}\n";
exit;
}
$tokenData = json_decode($authResponse, true);
$token = null;
if (is_array($tokenData) && isset($tokenData['token'])) {
$token = $tokenData['token'];
} elseif (is_string($tokenData)) {
$token = trim($tokenData, '"');
} else {
$token = trim($authResponse, '"');
}
if (empty($token)) {
echo "TOKEN NOT RECEIVED\n{$authResponse}\n";
exit;
}
$authElapsed = microtime(true) - $scriptStart;
// ── 2. Richiesta $metadata (XML, non JSON) ─────────────────────────────────
// Cache-buster nell'URL: alcuni server/reverse-proxy potrebbero cachare per URL esatto.
$cacheBuster = uniqid('cb_', true);
$metadataUrl = "{$baseUrl}/api/odata/\$metadata?_cb={$cacheBuster}";
$metadataStart = microtime(true);
$ch = curl_init($metadataUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: application/xml"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, true); // includi header risposta per ispezione
$rawResponse = curl_exec($ch);
$curlInfo = curl_getinfo($ch);
$httpCode = $curlInfo['http_code'];
$curlTotalTime = $curlInfo['total_time'];
$curlError = curl_error($ch);
$headerSize = $curlInfo['header_size'];
curl_close($ch);
$metadataElapsed = microtime(true) - $metadataStart;
if ($rawResponse === false || $httpCode !== 200) {
echo "METADATA REQUEST FAILED: HTTP {$httpCode}\nError: {$curlError}\n{$rawResponse}\n";
exit;
}
$responseHeaders = substr($rawResponse, 0, $headerSize);
$xml = substr($rawResponse, $headerSize);
echo "==================== TIMING (prova che NON è cache) ====================\n";
echo "Tempo autenticazione: " . round($authElapsed * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (curl total_time): " . round($curlTotalTime * 1000, 1) . " ms\n";
echo "Tempo chiamata \$metadata (wall clock PHP): " . round($metadataElapsed * 1000, 1) . " ms\n";
echo "Cache-buster usato: {$cacheBuster}\n";
echo "Dimensione XML ricevuto: " . strlen($xml) . " bytes\n\n";
echo "==================== HEADER RISPOSTA (controlla se c'è Cache-Control/Age/X-Cache) ====================\n";
echo $responseHeaders . "\n";
// ── 3. Estrai tutti gli EntitySet disponibili ──────────────────────────────
echo "==================== ENTITY SETS DISPONIBILI ====================\n";
if (preg_match_all('/<EntitySet Name="([^"]+)"/', $xml, $matches)) {
foreach ($matches[1] as $name) {
echo "- {$name}\n";
}
} else {
echo "(nessun EntitySet trovato — controlla il formato XML grezzo sotto)\n";
}
// ── 4. Estrai tutte le Property dell'EntityType "Cliente" (e simili) ──────
echo "\n==================== CAMPI PER ENTITYTYPE CHE CONTENGONO 'Client' ====================\n";
if (preg_match_all('/<EntityType Name="([^"]*[Cc]lient[^"]*)"[^>]*>(.*?)<\/EntityType>/s', $xml, $entityMatches, PREG_SET_ORDER)) {
foreach ($entityMatches as $entity) {
$entityName = $entity[1];
$entityBody = $entity[2];
echo "\n--- EntityType: {$entityName} ---\n";
if (preg_match_all('/<Property Name="([^"]+)" Type="([^"]+)"/', $entityBody, $propMatches, PREG_SET_ORDER)) {
foreach ($propMatches as $prop) {
echo " {$prop[1]} ({$prop[2]})\n";
}
}
}
} else {
echo "(nessun EntityType con 'Client' nel nome trovato)\n";
}
// ── 5. Salva l'XML grezzo completo per ispezione manuale, se serve ────────
file_put_contents(__DIR__ . '/metadata_debug.xml', $xml);
echo "\n\nXML completo salvato in: metadata_debug.xml (rimuovilo dopo il test)\n";
+63
View File
@@ -0,0 +1,63 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$partId = isset($_POST['part_id']) ? (int)$_POST['part_id'] : 0;
$analysisRecordkey = trim($_POST['analysis_recordkey'] ?? '');
$rowId = isset($_POST['row_id']) && $_POST['row_id'] !== '' ? (int)$_POST['row_id'] : 0;
if ($partId <= 0 || ($analysisRecordkey === '' && $rowId <= 0)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing required data']);
exit;
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
if ($rowId > 0) {
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE id = :id
AND part_id = :part_id
LIMIT 1
");
$stmt->execute([
':id' => $rowId,
':part_id' => $partId,
]);
} else {
$stmt = $pdo->prepare("
DELETE FROM identification_parts_analyses
WHERE part_id = :part_id
AND analysis_recordkey = :analysis_recordkey
LIMIT 1
");
$stmt->execute([
':part_id' => $partId,
':analysis_recordkey' => $analysisRecordkey,
]);
}
echo json_encode([
'success' => true,
'message' => 'Association deleted'
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+112
View File
@@ -0,0 +1,112 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* download_rapporto_file.php?id=123
*/
try {
$api = VisualLimsApiClient::getInstance();
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$id) {
throw new Exception("ID RapportoFile mancante");
}
$debugDir = __DIR__ . '/logs/lims_get_commessa/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* Recupero il record RapportoFile.
*/
$data = $api->get("RapportoFile({$id})", []);
file_put_contents(
$debugDir . "rapporto_file_{$id}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
/**
* Provo vari nomi campo possibili per il contenuto PDF.
* Dopo il primo test potremo adattarlo al nome esatto.
*/
$possibleContentFields = [
'File',
'file',
'Content',
'content',
'FileContent',
'fileContent',
'Contenuto',
'contenuto',
'Bytes',
'bytes'
];
$base64 = null;
foreach ($possibleContentFields as $field) {
if (!empty($data[$field])) {
$base64 = $data[$field];
break;
}
}
if (!$base64) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'message' => 'Record RapportoFile recuperato, ma non ho trovato un campo base64 del PDF.',
'hint' => 'Apri logs/lims_get_commessa/rapporto_file_' . $id . '.json e controlla il nome reale del campo file.',
'data' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Se il contenuto ha prefisso data URI, lo pulisco.
*/
if (strpos($base64, 'base64,') !== false) {
$parts = explode('base64,', $base64);
$base64 = end($parts);
}
$pdfBinary = base64_decode($base64, true);
if ($pdfBinary === false) {
throw new Exception("Il contenuto trovato non è un base64 valido");
}
$filename = $data['NomeFile']
?? $data['nomeFile']
?? $data['FileName']
?? $data['fileName']
?? "rapporto_{$id}.pdf";
if (!str_ends_with(strtolower($filename), '.pdf')) {
$filename .= '.pdf';
}
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . basename($filename) . '"');
header('Content-Length: ' . strlen($pdfBinary));
echo $pdfBinary;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+411 -75
View File
@@ -1,29 +1,75 @@
<?php include('include/headscript.php');
// Controlla se è stato passato un ID valido
// Check if a valid ID was provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
header("Location: templates_dashboard.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']); // Sanifica l'ID
$id = intval($_GET['id']);
// Recupera il template dal database
// Retrieve template from database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
header("Location: templates_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
// Recupera tutte le routine dal database
// Retrieve all routines
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$buttonBgPalette = [
'#0d6efd' => 'Blue',
'#6610f2' => 'Indigo',
'#6f42c1' => 'Purple',
'#d63384' => 'Pink',
'#dc3545' => 'Red',
'#fd7e14' => 'Orange',
'#ffc107' => 'Yellow',
'#198754' => 'Green',
'#20c997' => 'Teal',
'#0dcaf0' => 'Cyan',
'#212529' => 'Dark',
'#6c757d' => 'Gray',
'#495057' => 'Slate',
'#795548' => 'Brown',
'#2f5d50' => 'Sage Green',
];
$buttonTextPalette = [
'#ffffff' => 'White',
'#6c757d' => 'Gray',
'#000000' => 'Black',
];
$currentButtonBgColor = strtolower($template['button_bg_color'] ?? '#007bff');
$currentButtonTextColor = strtolower($template['button_text_color'] ?? '#ffffff');
if (!array_key_exists($currentButtonBgColor, array_change_key_case($buttonBgPalette, CASE_LOWER))) {
$currentButtonBgColor = '#0d6efd';
}
if (!array_key_exists($currentButtonTextColor, array_change_key_case($buttonTextPalette, CASE_LOWER))) {
$currentButtonTextColor = '#ffffff';
}
?>
<!doctype html>
<html lang="en">
@@ -34,7 +80,64 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<?php include('cssinclude.php'); ?>
<!-- Include jQuery prima di Select2 -->
<style>
.color-palette {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.color-option {
position: relative;
cursor: pointer;
}
.color-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.color-swatch {
width: 42px;
height: 42px;
border-radius: 10px;
border: 2px solid #d6dbe0;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
transition: all 0.18s ease;
}
.color-option input:checked+.color-swatch {
border: 3px solid #111827;
transform: scale(1.08);
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.18);
}
.color-swatch .check-mark {
display: none;
font-size: 18px;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.color-option input:checked+.color-swatch .check-mark {
display: inline-block;
}
.color-name {
display: block;
font-size: 11px;
text-align: center;
margin-top: 4px;
color: #555;
max-width: 55px;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<title>Edit Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
@@ -44,19 +147,22 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Update XLS Template</h5>
<h5 class="mb-0">Update Template</h5>
</div>
<div class="card-body">
<p class="mb-2">Edit the following form in order to update the selected import XLS template</p>
<p class="mb-2">Edit the following form in order to update the selected import template</p>
<p class="mb-2">Mandatory Fields</p>
<ul class="mb-0">
<li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li>
<li>Schema</li>
<li>Source Type</li>
<li>Schema and Client</li>
<li>Row Header and Column Header only for XLS templates</li>
</ul>
</div>
</div>
@@ -69,34 +175,104 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
<div class="card-body">
<div class="col-12">
<form id="editTemplateForm" method="POST">
<input type="hidden" name="id" value="<?php echo $template['id']; ?>">
<input type="hidden" name="id" value="<?php echo (int)$template['id']; ?>">
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name']); ?>" required>
<input type="text" name="name" class="form-control" value="<?php echo htmlspecialchars($template['name'] ?? ''); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" <?php echo (($template['source_type'] ?? 'XLS') === 'XLS') ? 'selected' : ''; ?>>XLS</option>
<option value="API" <?php echo (($template['source_type'] ?? 'XLS') === 'API') ? 'selected' : ''; ?>>API</option>
<option value="JSON" <?php echo (($template['source_type'] ?? 'XLS') === 'JSON') ? 'selected' : ''; ?>>JSON</option>
<option value="PDF" <?php echo (($template['source_type'] ?? 'XLS') === 'PDF') ? 'selected' : ''; ?>>PDF</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div>
<div class="mb-3" id="headerRowWrapper">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" class="form-control" value="<?php echo $template['header_row']; ?>" required>
<input type="number" name="header_row" id="headerRow" class="form-control" value="<?php echo htmlspecialchars($template['header_row'] ?? ''); ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?>*</label>
<input type="text" name="start_column" class="form-control" value="<?php echo htmlspecialchars($template['start_column']); ?>" required>
<div class="mb-3" id="startColumnWrapper">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="start_column" id="startColumn" class="form-control" value="<?php echo htmlspecialchars($template['start_column'] ?? ''); ?>">
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="<?php echo htmlspecialchars($template['xls_sheet_index'] ?? 0); ?>">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option
value="<?php echo (int)$apiConfig['id']; ?>"
<?php echo ((int)($template['api_config_id'] ?? 0) === (int)$apiConfig['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></label>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description']); ?></textarea>
<textarea name="description" class="form-control"><?php echo htmlspecialchars($template['description'] ?? ''); ?></textarea>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?>*</label>
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table']); ?>" readonly required>
<label class="form-label">Template Note</label>
<textarea name="template_note" class="form-control" rows="3"><?php echo htmlspecialchars($template['template_note'] ?? ''); ?></textarea>
<small class="text-muted">If set, this note will be appended at the end of the notes during export.</small>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($desttable, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="target_table" class="form-control" value="<?php echo htmlspecialchars($template['target_table'] ?? 'datadb'); ?>" readonly required>
</div>
<div class="mb-3">
@@ -110,12 +286,46 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3">
<label class="form-label">Button Background Color</label>
<input type="color" name="button_bg_color" class="form-control" value="<?php echo htmlspecialchars($template['button_bg_color'] ?? '#007bff'); ?>">
<div class="color-palette">
<?php foreach ($buttonBgPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonBgColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_bg_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
<label class="form-label">Button Text Color</label>
<input type="color" name="button_text_color" class="form-control" value="<?php echo htmlspecialchars($template['button_text_color'] ?? '#ffffff'); ?>">
<div class="color-palette">
<?php foreach ($buttonTextPalette as $colorValue => $colorLabel): ?>
<?php $isChecked = strtolower($colorValue) === $currentButtonTextColor; ?>
<label class="color-option" title="<?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?>">
<input
type="radio"
name="button_text_color"
value="<?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>"
<?= $isChecked ? 'checked' : ''; ?>>
<span class="color-swatch" style="background-color: <?= htmlspecialchars($colorValue, ENT_QUOTES, 'UTF-8'); ?>;">
<span class="check-mark"></span>
</span>
<span class="color-name"><?= htmlspecialchars($colorLabel, ENT_QUOTES, 'UTF-8'); ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-3">
@@ -128,7 +338,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="client_id" id="clientSelect" class="form-control" required>
<option value="">Select a client...</option>
</select>
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span>
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading clients...</span>
</div>
<div class="mb-3">
@@ -136,7 +346,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="schema_id" id="schemaSelect" class="form-control" required>
<option value="">Select a schema...</option>
</select>
<span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Caricamento schemi in corso...</span>
<span id="schemaLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Loading schemas...</span>
</div>
<div class="mb-3">
@@ -144,11 +354,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<select name="idroutine" id="routineSelect" class="form-control">
<option value="">Select a routine...</option>
<?php foreach ($routines as $routine): ?>
<option value="<?php echo $routine['idroutine']; ?>" <?php echo ($template['idroutine'] ?? '') == $routine['idroutine'] ? 'selected' : ''; ?>>
<option value="<?php echo $routine['idroutine']; ?>" <?php echo (($template['idroutine'] ?? '') == $routine['idroutine']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($routine['name']); ?>
</option>
<?php endforeach; ?>
</select>
<div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -166,8 +377,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -175,9 +388,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<script>
document.addEventListener("DOMContentLoaded", function() {
// Verifica che jQuery sia caricato
if (typeof jQuery === 'undefined') {
alert("Errore: jQuery non è caricato. Contatta l'amministratore.");
alert("Error: jQuery is not loaded.");
return;
}
@@ -192,12 +404,21 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
return;
}
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
const selectedClientId = <?php echo json_encode((int)($template['idclient'] ?? 0)); ?>;
const selectedSchemaId = <?php echo json_encode((int)($template['idschema'] ?? 0)); ?>;
// Inizializza Select2
$('#clientSelect').select2({
placeholder: "Search for a client...",
allowClear: true
@@ -213,108 +434,201 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
// Carica i clienti
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
const isXls = selectedSource === 'XLS';
const isApiOrJson = selectedSource === 'API' || selectedSource === 'JSON';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiOrJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
function formatClientLabel(client) {
const nome = client.Nominativo || "Name not available";
const id = client.IdCliente || "";
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
return `${nome.trim()} - ${shortCode} (ID: ${id})`;
}
async function loadClients() {
try {
clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
clientLoadingStatus.textContent = 'Loading clients...';
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `HTTP error: ${response.status}`);
}
const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($template['idclient'] ?? 0); ?>)) {
const id = client.IdCliente || "";
const option = new Option(formatClientLabel(client), id);
if (parseInt(id) === parseInt(selectedClientId)) {
option.selected = true;
}
select.add(option);
});
$(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati.";
clientLoadingStatus.textContent = "Clients loaded.";
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
clientLoadingStatus.textContent = "Loading error.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message,
title: "Error!",
text: "Unable to load clients: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
} finally {
setTimeout(() => clientLoadingStatus.style.display = 'none', 2000);
setTimeout(() => clientLoadingStatus.style.display = 'none', 1500);
}
}
// Carica gli schemi
async function loadSchemas() {
try {
schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
schemaLoadingStatus.textContent = 'Loading schemas...';
const response = await fetch("get_schemi.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `HTTP error: ${response.status}`);
}
const select = document.getElementById("schemaSelect");
select.innerHTML = '<option value="">Select a schema...</option>';
data.value.forEach(schema => {
const nome = schema.Nome || "Nome non disponibile";
const id = schema.IdSchemaCustomFields || "ID non disponibile";
const sortedSchemas = [...data.value].sort((a, b) => {
const nomeA = (a.Nome || "").trim().toLowerCase();
const nomeB = (b.Nome || "").trim().toLowerCase();
return nomeA.localeCompare(nomeB, 'it', {
sensitivity: 'base'
});
});
sortedSchemas.forEach(schema => {
const nome = schema.Nome || "Name not available";
const id = schema.IdSchemaCustomFields || "";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
if (parseInt(id) === parseInt(<?php echo json_encode($template['idschema'] ?? 0); ?>)) {
if (parseInt(id) === parseInt(selectedSchemaId)) {
option.selected = true;
}
select.add(option);
});
$(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati.";
schemaLoadingStatus.textContent = "Schemas loaded.";
} catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento.";
schemaLoadingStatus.textContent = "Loading error.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare gli schemi: " + error.message,
title: "Error!",
text: "Unable to load schemas: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
} finally {
setTimeout(() => schemaLoadingStatus.style.display = 'none', 2000);
setTimeout(() => schemaLoadingStatus.style.display = 'none', 1500);
}
}
// Carica i dati
async function loadData() {
try {
await loadClients();
await loadSchemas();
} catch (error) {
Swal.fire({
title: "Errore!",
text: "Errore nel caricamento dei dati: " + error.message,
title: "Error!",
text: "Error while loading data: " + error.message,
icon: "error",
confirmButtonText: "OK"
});
}
}
loadData();
// Routine dettagli
const routines = <?php echo json_encode($routines); ?>;
function updateRoutineDetails() {
const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId);
if (routine) {
routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A';
@@ -336,10 +650,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
routineAction3.textContent = '';
}
}
routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails(); // Inizializza dettagli se una routine è preselezionata
// Submit del form
routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails();
form.addEventListener("submit", function(e) {
e.preventDefault();
@@ -351,8 +665,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!clientId) {
Swal.fire({
title: "Errore!",
text: "Per favore seleziona un cliente.",
title: "Error!",
text: "Please select a client.",
icon: "error",
confirmButtonText: "OK"
});
@@ -373,8 +687,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!schemaId) {
Swal.fire({
title: "Errore!",
text: "Per favore seleziona uno schema.",
title: "Error!",
text: "Please select a schema.",
icon: "error",
confirmButtonText: "OK"
});
@@ -387,13 +701,35 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
}
formData.append("idschema", schemaId);
formData.append("schemaname", schemaName);
// Aggiungi idroutine
const routineId = routineSelect.value;
formData.append("idroutine", routineId);
const selectedSource = sourceType.value;
if ((selectedSource === 'API' || selectedSource === 'JSON') && !apiConfigSelect.value) {
Swal.fire({
title: "Error!",
text: "Please select an API/JSON configuration.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Error!",
text: "Please enter the XLS sheet number.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
fetch("process_edit_template_xls.php", {
method: "POST",
body: formData
@@ -402,8 +738,8 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
.then(data => {
if (data.success) {
Swal.fire({
title: "Successo!",
text: "Template aggiornato con successo!",
title: "Success!",
text: "Template updated successfully!",
icon: "success",
confirmButtonText: "OK"
}).then(() => {
@@ -411,17 +747,17 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
} else {
Swal.fire({
title: "Errore!",
title: "Error!",
text: data.message,
icon: "error",
confirmButtonText: "OK"
});
}
})
.catch(error => {
.catch(() => {
Swal.fire({
title: "Errore!",
text: "Si è verificato un errore imprevisto.",
title: "Error!",
text: "An unexpected error occurred.",
icon: "error",
confirmButtonText: "OK"
});
+362
View File
@@ -12,3 +12,365 @@
2025-08-26 16:49:24 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-08-26 16:50:23 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2025-09-08 08:39:17 - Risposta non JSON valida: <?xml version="1.0" encoding="utf-8"?><edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"><edmx:DataServices><Schema Namespace="DevExpress.ExpressApp.SystemModule" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="DashboardViewItemDescriptor"><Key><PropertyRef Name="ViewId" /></Key><Property Name="ViewId" Type="Edm.String" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem" BaseType="DevExpress.ExpressApp.NonPersistentLiteObject" Abstract="true"><Property Name="Visibility" Type="DevExpress.ExpressApp.Editors.ViewItemVisibility" Nullable="false" /></EntityType><EntityType Name="DashboardOrganizationItem_1OfIModelDashboardViewItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem" Abstract="true" /><EntityType Name="ViewDashboardOrganizationItem" BaseType="DevExpress.ExpressApp.SystemModule.DashboardOrganizationItem_1OfIModelDashboardViewItem"><Property Name="ObjectType" Type="System.Type" /><Proper
2026-01-27 15:33:53 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:06 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:34:10 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-27 15:35:13 - Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:38 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:33:39 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-29 14:34:04 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:37:29 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:41:55 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:03 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:42:52 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-29 14:43:00 [ClienteResponsabile] Errore nel recupero dati: HTTP 404, Risposta:
2026-01-30 10:50:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 10:50:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-01-30 11:09:22 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-19 10:00:44 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-23 10:44:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:42 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:43 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:52 [AnagraficaCertestObject] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:44:53 [AnagraficaCertestService] Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:26 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:48:27 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:04 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 10:49:05 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:52 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 14:26:59 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 15:54:16 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:22:45 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-23 16:32:17 - Autenticazione fallita: HTTP 404, Errore cURL: , Risposta: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Not Found</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Not Found</h2>
<hr><p>HTTP Error 404. The requested resource is not found.</p>
</BODY></HTML>
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-18 15:51:45 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-19 09:50:34 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-25 14:13:34 - Autenticazione fallita: HTTP 503, Errore cURL: , Risposta: The service is unavailable.
2026-03-26 10:57:18 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:21 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:36 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:41 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:57:56 [AnagraficaCertestObject] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-03-26 10:58:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 500, Errore cURL: , Risposta: {"title":"Internal Server Error","status":500,"detail":"Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.","instance":"POST /api/authentication/authenticate","errorCode":"63ab532c6"}
2026-04-30 15:01:25 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2621521)","errorCode":"d25cbd678"}
2026-04-30 15:02:04 - Errore nel recupero dati: HTTP 404, Risposta:
2026-04-30 15:03:19 - Errore nella richiesta: URL rejected: Malformed input to a URL function
2026-06-16 14:29:15 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21037 ms: Couldn't connect to server, Risposta:
2026-06-16 14:29:36 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21099 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21076 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:03 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21083 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:34 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:30:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21050 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21048 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21043 ms: Couldn't connect to server, Risposta:
2026-06-16 14:31:46 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21042 ms: Couldn't connect to server, Risposta:
2026-06-16 14:32:07 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21058 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:09 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:33:30 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21047 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21062 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21064 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:11 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21049 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21057 ms: Couldn't connect to server, Risposta:
2026-06-16 14:36:33 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21053 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:13 [MoltiplicatorePrezzo] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21032 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestService] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21031 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:14 [AnagraficaCertestObject] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21027 ms: Couldn't connect to server, Risposta:
2026-06-16 14:37:35 [ClienteResponsabile] Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21054 ms: Couldn't connect to server, Risposta:
2026-06-16 17:13:55 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21067 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:25 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21065 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21071 ms: Couldn't connect to server, Risposta:
2026-06-16 17:14:46 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to bvcpsitaly-elims.com port 443 after 21078 ms: Couldn't connect to server, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:33 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-09 08:06:41 - Autenticazione fallita: HTTP 0, Errore cURL: Could not resolve host: bvcpsitaly-elims.com, Risposta:
2026-07-10 08:12:24 - Errore nella richiesta: SSL connection timeout
+740
View File
@@ -0,0 +1,740 @@
/**
* exportLims_gridData.js — Export to LIMS using gridData (for imported.php)
*
* Replaces export_to_lims.js for pages that use gridData instead of DOM-rendered rows.
* Single export + batch export (Export All) with validation.
*/
(function () {
"use strict";
let pendingConfirmHandler = null;
let batchRunning = false;
Object.defineProperty(window, "batchRunning", { get: () => batchRunning });
let batchCancelled = false;
let pendingBatchConfirmHandler = null;
// ── Helpers ──────────────────────────────────────────────────────────
function cleanupBackdrop() {
document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
const overlay = document.querySelector(".overlay.toggle-icon");
if (overlay) overlay.style.display = "none";
}
function getGridRow(iddatadb) {
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
}
function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(
(r) => String(r.iddatadb) === String(iddatadb),
);
}
// ── Validation ──────────────────────────────────────────────────────
async function validateRows(rowsToValidate) {
const response = await fetch("validate_export.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: rowsToValidate }),
});
if (!response.ok)
throw new Error(`Validation HTTP error: ${response.status}`);
return response.json();
}
function clearValidationErrors() {
// Clear from gridData
(window.gridData || []).forEach((row) => {
delete row._validationErrors;
delete row._exportError;
});
document
.querySelectorAll(".grid-cell.validation-error")
.forEach((cell) => {
cell.classList.remove("validation-error");
cell.querySelectorAll(".input-validation-error").forEach((el) =>
el.classList.remove("input-validation-error"),
);
const tooltip = cell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
});
document
.querySelectorAll(".grid-row.validation-row-error")
.forEach((row) => row.classList.remove("validation-row-error"));
clearAllRowErrors();
}
function showValidationErrors(gridRow, iddatadb, errors) {
// Store in gridData for re-render persistence
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) window.gridData[idx]._validationErrors = errors;
if (!gridRow) return;
gridRow.classList.add("validation-row-error");
const messages = [];
errors.forEach((err) => {
messages.push(err.message);
if (!err.field) return;
let cell = null;
if (err.field.startsWith("field_label:")) {
const label = err.field.substring("field_label:".length);
const headers = document.querySelectorAll(".grid-header");
let targetIndex = null;
headers.forEach((h) => {
if (h.textContent.trim() === label)
targetIndex = h.getAttribute("data-index");
});
if (targetIndex) {
cell = gridRow.querySelector(
`.grid-cell[data-index="${targetIndex}"]`,
);
}
} else {
cell = gridRow.querySelector(
`.grid-cell[data-col="${err.field}"]`,
);
}
if (cell) {
cell.classList.add("validation-error");
cell.querySelectorAll("input, select").forEach((el) =>
el.classList.add("input-validation-error"),
);
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "validation-tooltip";
cell.appendChild(tooltip);
}
tooltip.textContent = err.message;
}
});
showRowError(gridRow, iddatadb, messages.join("\n"));
}
// ── Send export ─────────────────────────────────────────────────────
async function sendExport(iddatadb, batchUuid) {
const formData = new FormData();
formData.append("iddatadb", iddatadb);
if (batchUuid) formData.append("batch_uuid", batchUuid);
const response = await fetch("export_to_lims.php", {
method: "POST",
body: formData,
});
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
if (data.success) {
// Update gridData
const idx = getRowIndexByIddatadb(iddatadb);
if (idx >= 0) {
window.gridData[idx].status = "l";
window.gridData[idx].commessaweb = data.commessaweb;
}
// Update visible DOM row
const gridRow = getGridRow(iddatadb);
if (gridRow) {
const statusBadge = gridRow.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge) {
statusBadge.classList.remove("status-i", "status-P");
statusBadge.classList.add("status-l");
statusBadge.textContent = "To LIMS";
}
const statusCell = gridRow.querySelector(
'.grid-cell[data-col="status"]',
);
if (statusCell && data.commessaweb) {
let cwSpan = statusCell.querySelector(".commessaweb-code");
if (!cwSpan) {
cwSpan = document.createElement("span");
cwSpan.className = "commessaweb-code";
cwSpan.style.cssText =
"display:block; font-size:0.75em; color:#555; margin-top:2px;";
statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
const exportBtn = gridRow.querySelector(".export-lims-btn");
if (exportBtn) {
exportBtn.disabled = true;
exportBtn.style.background = "#ccc";
exportBtn.style.cursor = "not-allowed";
exportBtn.style.opacity = "0.5";
}
}
}
return data;
}
// ── Show result modal ───────────────────────────────────────────────
function showExportResult(data) {
const el = document.getElementById("exportResponseModal");
if (!el) return;
const modal = new bootstrap.Modal(el, { keyboard: false });
const msg = document.getElementById("exportResponseMessage");
const label = document.getElementById("exportResponseModalLabel");
if (data.success) {
msg.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "");
label.textContent = "Esportazione Completata";
} else {
msg.textContent = `Errore: ${data.message}`;
label.textContent = "Errore Esportazione";
}
modal.show();
el.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
}
// ── Row UI helpers ──────────────────────────────────────────────────
function setRowExporting(row, active) {
if (!row) return;
const btnCell = row.querySelector(".button-cell");
if (active) {
row.classList.remove("batch-disabled");
row.classList.add("batch-exporting");
if (btnCell) {
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.dataset.prevDisplay = b.style.display;
b.style.display = "none";
});
const spinner = document.createElement("span");
spinner.className = "batch-row-spinner";
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Exporting...';
btnCell.appendChild(spinner);
}
} else {
row.classList.remove("batch-exporting");
if (btnCell) {
const spinner = btnCell.querySelector(".batch-row-spinner");
if (spinner) spinner.remove();
btnCell.querySelectorAll(".action-btn").forEach((b) => {
b.style.display = b.dataset.prevDisplay || "";
delete b.dataset.prevDisplay;
});
}
}
}
function showRowError(row, iddatadb, message) {
if (!row) return;
row.classList.add("batch-row-error");
const btnCell = row.querySelector(".button-cell");
if (btnCell) {
const old = btnCell.querySelector(".batch-error-msg");
if (old) old.remove();
const errorEl = document.createElement("div");
errorEl.className = "batch-error-msg";
errorEl.textContent = "Warning — click for details";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML =
message.replace(/\n/g, "<br>");
document.getElementById(
"exportResponseModalLabel",
).textContent = "Error (id: " + iddatadb + ")";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
});
btnCell.appendChild(errorEl);
}
}
function clearAllRowErrors() {
document
.querySelectorAll(".grid-row.batch-row-error")
.forEach((row) => {
row.classList.remove("batch-row-error");
const msg = row.querySelector(".batch-error-msg");
if (msg) msg.remove();
});
}
function disableAllRowButtons() {
document
.querySelectorAll(".grid-row[data-id]")
.forEach((row) => row.classList.add("batch-disabled"));
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = true;
toggle.style.opacity = "0.5";
toggle.style.pointerEvents = "none";
}
}
function enableAllRowButtons() {
document
.querySelectorAll(".grid-row[data-id]")
.forEach((row) => row.classList.remove("batch-disabled"));
const toggle = document.querySelector(
".actions-dropdown .dropdown-toggle",
);
if (toggle) {
toggle.disabled = false;
toggle.style.opacity = "";
toggle.style.pointerEvents = "";
}
}
// ── Single row export: validate → confirm → send ────────────────────
function startExportConfirmFlow(iddatadb, rowIndex) {
const gridRow = getGridRow(iddatadb);
clearValidationErrors();
if (gridRow) {
setRowExporting(gridRow, true);
const spinner = gridRow.querySelector(".batch-row-spinner");
if (spinner)
spinner.innerHTML =
'<i class="fas fa-spinner fa-spin"></i> Validating...';
}
validateRows([{ iddatadb: parseInt(iddatadb), index: rowIndex }])
.then((validationData) => {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (!validationData.success) {
showExportResult({
success: false,
message: validationData.message || "Validation error",
});
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
if (gridRow)
showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
showConfirmAndExport(iddatadb, rowIndex);
})
.catch((error) => {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
showExportResult({
success: false,
message: "Validation error: " + error.message,
});
});
}
function showConfirmAndExport(iddatadb, rowIndex) {
const confirmModalElement =
document.getElementById("exportConfirmModal");
if (!confirmModalElement) return;
const confirmModal = new bootstrap.Modal(confirmModalElement, {
keyboard: false,
});
document.getElementById("exportIddatadb").textContent = iddatadb;
confirmModal.show();
const confirmBtn = document.getElementById("exportConfirmBtn");
if (!confirmBtn) {
confirmModal.hide();
return;
}
const confirmHandler = async () => {
pendingConfirmHandler = null;
confirmModal.hide();
const gridRow = getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb);
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
if (!data.success)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
showExportResult(data);
} catch (error) {
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
showRowError(gridRow, iddatadb, error.message);
showExportResult({ success: false, message: error.message });
}
};
if (pendingConfirmHandler)
confirmBtn.removeEventListener("click", pendingConfirmHandler);
pendingConfirmHandler = confirmHandler;
confirmBtn.addEventListener("click", confirmHandler, { once: true });
}
// ── Single row click (event delegation) ─────────────────────────────
$(document).on("click", ".export-lims-btn", function (e) {
e.preventDefault();
if (batchRunning) return;
const iddatadb = this.dataset.iddatadb;
const rowIndex = parseInt(this.dataset.row);
const gridRow = getGridRow(iddatadb);
// Check unsaved changes for this row
const dataRow = window.gridData?.[rowIndex];
if (dataRow && dataRow._dirty) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show();
document.getElementById("saveAndExportBtn")?.addEventListener(
"click",
() => {
unsavedModal.hide();
// Save first, then export
const formData = window.buildSavePayload(rowIndex);
fetch("save_edited_row.php", {
method: "POST",
body: formData,
})
.then((r) => r.json())
.then((result) => {
if (result.success) {
dataRow._dirty = false;
startExportConfirmFlow(iddatadb, rowIndex);
} else {
alert("Save failed: " + result.message);
}
});
},
{ once: true },
);
return;
}
startExportConfirmFlow(iddatadb, rowIndex);
});
// ── Batch export (Export All) ───────────────────────────────────────
function collectEligibleRows() {
// Read from gridData, not DOM. Se i filtri sono attivi, limita al target set.
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
const eligible = [];
(window.gridData || []).forEach((row, index) => {
if (row.status === "l") return;
if (target && !target.has(parseInt(row.iddatadb, 10))) return;
eligible.push({
iddatadb: row.iddatadb,
index,
row: getGridRow(row.iddatadb),
});
});
return eligible;
}
function hasUnsavedChanges() {
const target =
typeof window.getTargetGridIds === "function"
? window.getTargetGridIds()
: null;
return (window.gridData || []).some(
(r) =>
r._dirty && (!target || target.has(parseInt(r.iddatadb, 10))),
);
}
async function validateAndFilter(eligibleRows) {
const rowsToValidate = eligibleRows.map(({ iddatadb, index }) => ({
iddatadb: parseInt(iddatadb),
index,
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success)
throw new Error(validationData.message || "Validation error");
const validRows = [];
let invalidCount = 0;
for (const item of eligibleRows) {
const result = validationData.results[item.index];
if (result && !result.valid) {
if (item.row)
showValidationErrors(
item.row,
item.iddatadb,
result.errors,
);
invalidCount++;
} else {
validRows.push(item);
}
}
return { validRows, invalidCount };
}
function showValidationSpinner(show) {
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (show) {
if (bar) bar.style.display = "";
if (statusEl) statusEl.textContent = "Validating...";
if (cancelBtn) cancelBtn.style.display = "none";
} else {
if (bar) bar.style.display = "none";
if (cancelBtn) cancelBtn.style.display = "";
}
}
function showBatchConfirm(eligibleRows) {
clearValidationErrors();
showValidationSpinner(true);
validateAndFilter(eligibleRows)
.then(({ validRows, invalidCount }) => {
showValidationSpinner(false);
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`No valid rows for export.<br><strong>${invalidCount}</strong> rows with validation errors.`;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Validation Failed";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
return;
}
const confirmModal = new bootstrap.Modal(
document.getElementById("exportBatchConfirmModal"),
{ keyboard: false },
);
let countText = String(validRows.length);
if (invalidCount > 0)
countText += ` (${invalidCount} excluded due to errors)`;
document.getElementById("exportBatchCount").textContent =
countText;
confirmModal.show();
const confirmBtn = document.getElementById(
"exportBatchConfirmBtn",
);
if (pendingBatchConfirmHandler)
confirmBtn.removeEventListener(
"click",
pendingBatchConfirmHandler,
);
pendingBatchConfirmHandler = () => {
pendingBatchConfirmHandler = null;
confirmModal.hide();
startBatchExport(validRows);
};
confirmBtn.addEventListener(
"click",
pendingBatchConfirmHandler,
{ once: true },
);
})
.catch((error) => {
showValidationSpinner(false);
document.getElementById("exportResponseMessage").textContent =
"Validation error: " + error.message;
document.getElementById(
"exportResponseModalLabel",
).textContent = "Validation Error";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
});
}
$(document).on("click", ".export-all-lims-btn", function (e) {
e.preventDefault();
if (batchRunning) return;
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportBatchUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show();
document.getElementById("batchSaveAndExportBtn")?.addEventListener(
"click",
() => {
unsavedModal.hide();
// Trigger save all first — listen for completion
alert("Please Save All first, then Export All.");
},
{ once: true },
);
return;
}
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent =
"All rows already exported to LIMS.";
document.getElementById("exportResponseModalLabel").textContent =
"Export All";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
return;
}
showBatchConfirm(eligibleRows);
});
function startBatchExport(eligibleRows) {
batchCancelled = false;
batchRunning = true;
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0,
succeeded = 0,
failed = 0;
disableAllRowButtons();
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (bar) bar.style.display = "";
if (cancelBtn) cancelBtn.disabled = false;
if (statusEl) statusEl.textContent = `Exporting 0 / ${total}...`;
cancelBtn?.addEventListener(
"click",
() => {
batchCancelled = true;
if (statusEl)
statusEl.textContent =
"Cancelling... (waiting for current row)";
if (cancelBtn) cancelBtn.disabled = true;
},
{ once: true },
);
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
if (statusEl)
statusEl.textContent = `Exporting ${processed + 1} / ${total} (id: ${iddatadb})...`;
const gridRow = row || getGridRow(iddatadb);
if (gridRow) setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
if (data.success) {
succeeded++;
} else {
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError =
data.message || "Unknown error";
if (gridRow)
showRowError(
gridRow,
iddatadb,
data.message || "Unknown error",
);
}
} catch (error) {
processed++;
failed++;
const errIdx = getRowIndexByIddatadb(iddatadb);
if (errIdx >= 0)
window.gridData[errIdx]._exportError = error.message;
if (gridRow) showRowError(gridRow, iddatadb, error.message);
}
if (gridRow) {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
}
}
batchRunning = false;
enableAllRowButtons();
if (bar) bar.style.display = "none";
// Re-render to reflect status changes, then restore error indicators
const gr = window.gridRenderer;
if (gr) gr.renderVisibleRows();
// Restore error state from gridData._exportError
(window.gridData || []).forEach((row, idx) => {
if (row._exportError) {
const gridRow = getGridRow(row.iddatadb);
if (gridRow)
showRowError(gridRow, row.iddatadb, row._exportError);
}
});
const msgEl = document.getElementById("exportResponseMessage");
const labelEl = document.getElementById("exportResponseModalLabel");
if (batchCancelled) {
labelEl.textContent = "Export All — Cancelled";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong><br>Not processed: <strong>${total - processed}</strong>`;
} else if (failed === 0) {
labelEl.textContent = "Export All — Complete";
msgEl.innerHTML = `All <strong>${succeeded}</strong> rows exported successfully.`;
} else {
labelEl.textContent = "Export All — Completed with errors";
msgEl.innerHTML = `Exported: <strong>${succeeded}</strong><br>Errors: <strong>${failed}</strong>`;
}
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
once: true,
});
})();
}
})();
File diff suppressed because it is too large Load Diff
+570 -44
View File
@@ -13,9 +13,65 @@ if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$uploadDir = realpath(__DIR__ . '/../photostrf') . '/';
/**
* Resolve a datadb_photos.file_path to an absolute file path on disk.
*/
function resolvePhotoFullPath(string $filePath, string $uploadDir, string $userareaDir): ?string
{
$filePath = trim($filePath);
if ($filePath === '') {
return null;
}
$base = rtrim($uploadDir, '/');
$candidates = [];
// Userarea-relative form: "../photostrf/annotated/x.png" → resolve from __DIR__.
if (strncmp($filePath, '../', 3) === 0) {
$candidates[] = rtrim($userareaDir, '/') . '/' . $filePath;
}
// Bare filename or "annotated/x.png" subpath - resolve from photostrf/.
$candidates[] = $base . '/' . ltrim($filePath, './');
// Safety net for any odd/legacy value.
$candidates[] = $base . '/' . basename($filePath);
foreach ($candidates as $candidate) {
$real = realpath($candidate);
if ($real && file_exists($real)) {
return $real;
}
}
return null;
}
// 🔹 Base URL API
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
// 🔹 Batch UUID — if present, all logs go to a single file
$batchUuid = $_POST['batch_uuid'] ?? null;
$writeLog = (function () use ($batchUuid, $logDir) {
$batchLogFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
return function ($individualPath, $content, $stepLabel = null) use ($batchLogFile) {
if ($batchLogFile) {
$header = "\n" . str_repeat("=", 60) . "\n";
if ($stepLabel) {
$header .= "[{$stepLabel}] " . date('Y-m-d H:i:s') . "\n";
}
$header .= str_repeat("=", 60) . "\n";
file_put_contents($batchLogFile, $header . $content . "\n", FILE_APPEND);
} else {
file_put_contents($individualPath, $content);
}
};
})();
// 🔹 Funzione per validare e convertire date
function validateDate($value)
{
@@ -27,15 +83,83 @@ function validateDate($value)
return null; // Imposta null se non è una data valida
}
// 🔹 Funzione per validare e convertire date
function formatDateToExport($value)
{
$date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value);
if ($date) {
return $date->format('d/m/Y');
}
return null; // Imposta null se non è una data valida
}
// ImportaCommessa con retry: la chiamata è asincrona lato LIMS e a volte
// risponde 200 senza importare (StatoCommessaWeb resta "Inviata"/"Nuova").
// Riprova con backoff esponenziale finché non passa a "Elaborata".
function importaCommessaWithRetry($api, $commessaId, array $payload, $maxRetries = 3, $initialBackoff = 1)
{
$result = null;
$stato = null;
$succeeded = false;
$log = "";
$backoff = $initialBackoff;
set_time_limit(120); // i backoff non devono far scadere il timeout della richiesta
for ($attempt = 1; $attempt <= $maxRetries + 1; $attempt++) {
try {
$result = $api->post("CommessaWeb({$commessaId})/ImportaCommessa", $payload);
$stato = $result['StatoCommessaWeb'] ?? null;
$log .= "Attempt {$attempt}: HTTP 200, StatoCommessaWeb=" . ($stato ?? 'null') . "\n";
} catch (Exception $e) {
$stato = null;
$log .= "Attempt {$attempt}: EXCEPTION " . $e->getMessage() . "\n";
}
if ($stato === 'Elaborata') {
$succeeded = true;
break;
}
if ($attempt <= $maxRetries) {
$log .= " -> not Elaborata, waiting {$backoff}s before retry\n";
sleep($backoff);
$backoff *= 2;
}
}
return [
'succeeded' => $succeeded,
'stato' => $stato,
'result' => $result,
'log' => $log,
];
}
try {
$iddatadb = $_POST['iddatadb'] ?? null;
if (!$iddatadb) {
throw new Exception("Missing iddatadb");
}
// TEMP: simulate error on every other row for testing
if (env('SIMULATE_EXPORT_LIMS') && $iddatadb % 2 === 0) {
throw new Exception("Simulated error for iddatadb $iddatadb");
}
// 🔹 STEP 1+2: Fetch Cliente ID from datadb and Schema ID from excel_templates
// Also fetch fixed fields stored in datadb
$stmt = $pdo->prepare("
SELECT d.idclient AS clienteId, et.idschema AS schemaId
SELECT d.idclient AS clienteId, et.idschema AS schemaId, et.id AS templateId,
et.template_note,
d.cliente_responsabile_id,
d.moltiplicatore_prezzo_id,
d.anagrafica_certest_object_id,
d.anagrafica_certest_service_id,
d.cliente_fornitore_id,
d.clienteAnalisi,
d.ClienteFatturazione,
d.consegna_richiesta
FROM datadb as d
INNER JOIN excel_templates as et ON d.templateid = et.id
WHERE d.iddatadb = :iddatadb
@@ -50,23 +174,73 @@ try {
$clienteId = (int) $result['clienteId'];
$schemaId = (int) $result['schemaId'];
$templateId = (int) $result['templateId'];
// 🔹 STEP 3: Fetch Parts (including idmatrice)
// Extract fixed fields (nullable INTs)
$clienteResponsabile = !empty($result['cliente_responsabile_id']) ? (int) $result['cliente_responsabile_id'] : null;
$moltiplicatorePrezzo = !empty($result['moltiplicatore_prezzo_id']) ? (int) $result['moltiplicatore_prezzo_id'] : null;
$anagraficaObject = !empty($result['anagrafica_certest_object_id']) ? (int) $result['anagrafica_certest_object_id'] : null;
$anagraficaService = !empty($result['anagrafica_certest_service_id']) ? (int) $result['anagrafica_certest_service_id'] : null;
$clienteFornitore = !empty($result['cliente_fornitore_id']) ? (int) $result['cliente_fornitore_id'] : null;
$clienteAnalisi = !empty($result['clienteAnalisi']) ? (int) $result['clienteAnalisi'] : null;
$clienteFatturazione = !empty($result['ClienteFatturazione']) ? (int) $result['ClienteFatturazione'] : null;
$consegnaRichiesta = !empty($result['consegna_richiesta']) ? $result['consegna_richiesta'] : null;
// 🔹 STEP 3: Fetch Parts (including idmatrice and part id for custom fields)
$stmt = $pdo->prepare("
SELECT part_number, part_description, material, color, mix, idmatrice
SELECT id AS part_id, part_number, part_description, material, color, mix, idmatrice, dateexpiry, note
FROM identification_parts
WHERE iddatadb = :iddatadb
");
AND part_description IS NOT NULL
AND TRIM(part_description) <> ''
ORDER BY CAST(part_number AS UNSIGNED) ASC, part_number ASC
");
$stmt->execute(['iddatadb' => $iddatadb]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 3.1: Fetch custom field values per part from identification_parts_customfields
$partIds = array_column($parts, 'part_id');
$partsCustomFields = []; // part_id => [ { field_id, value_id, value_text }, ... ]
if (!empty($partIds)) {
$placeholders = implode(',', array_fill(0, count($partIds), '?'));
$cfStmt = $pdo->prepare("
SELECT part_id, field_id, value_id, value_text
FROM identification_parts_customfields
WHERE part_id IN ({$placeholders})
");
$cfStmt->execute($partIds);
foreach ($cfStmt->fetchAll(PDO::FETCH_ASSOC) as $cfRow) {
$partsCustomFields[(int)$cfRow['part_id']][] = $cfRow;
}
}
// 🔹 STEP 4a: Auto-populate export_date / export_time fields
$stmt = $pdo->prepare("
UPDATE import_data_details idd
JOIN template_mapping m ON idd.mapping_id = m.id
SET idd.field_value = CASE m.auto_value
WHEN 'export_date' THEN :export_date
WHEN 'export_time' THEN :export_time
END
WHERE idd.id = :iddatadb
AND m.auto_value IN ('export_date', 'export_time')
");
$stmt->execute([
'iddatadb' => $iddatadb,
'export_date' => date('Y-m-d'),
'export_time' => date('H:i'),
]);
// 🔹 STEP 4: Fetch Field Values with Labels
$stmt = $pdo->prepare("
SELECT
idd.field_value,
m.field_label,
m.schema_id,
m.field_id
m.field_id,
m.field_order,
m.is_addnote
FROM
import_data_details as idd
JOIN template_mapping m ON idd.mapping_id = m.id
@@ -86,6 +260,35 @@ try {
$valueMap[(int) $row['field_id']] = $row['field_value'];
}
// 🔹 Note aggiuntive: custom field flaggati is_addnote e valorizzati.
// Valori a livello di commessa → identici su tutte le parti.
$addnoteChunks = [];
$addnoteRows = array_filter($rows, fn($r) => (int)($r['is_addnote'] ?? 0) === 1);
usort($addnoteRows, function ($a, $b) {
$oa = (int)($a['field_order'] ?? 9999);
$ob = (int)($b['field_order'] ?? 9999);
if ($oa !== $ob) return $oa <=> $ob;
return strcmp((string)($a['field_label'] ?? ''), (string)($b['field_label'] ?? ''));
});
foreach ($addnoteRows as $r) {
$val = trim((string)($r['field_value'] ?? ''));
if ($val !== '') {
$addnoteChunks[] = $val;
}
}
$addnoteText = implode(' - ', $addnoteChunks);
// Nota del template: appesa in fondo alle note in esportazione
$templateNote = trim((string)($result['template_note'] ?? ''));
if ($templateNote !== '') {
$addnoteText = $addnoteText !== ''
? $addnoteText . ' - ' . $templateNote
: $templateNote;
}
// Logga i fieldValues in error_log
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
error_log($logFieldValues);
@@ -93,12 +296,22 @@ try {
// 🔹 Initialize API client
$api = VisualLimsApiClient::getInstance();
// 🔹 STEP 5: Create CommessaWeb (NOT WebOrder)
// 🔹 STEP 5: Create CommessaWeb
// Fixed fields are sent as direct properties — they are navigation properties on Commessa
// accepted by the XAF API even though not explicitly listed in OData $metadata
$commessaWebPayload = [
"Cliente" => $clienteId,
"SchemaCustomField" => $schemaId,
"Richiedente" => "Test Web Import",
"Descrizione" => "TEST CommessaWeb",
"Cliente" => $clienteId,
"SchemaCustomField" => $schemaId,
"Richiedente" => "From TRFSmart Application", // TODO: replace with real value
"Descrizione" => "From TRFSmart Application", // TODO: replace with real value
"ClienteResponsabile" => $clienteResponsabile,
"MoltiplicatorePrezzo" => $moltiplicatorePrezzo,
"AnagraficaCertestObject" => $anagraficaObject,
"AnagraficaCertestService" => $anagraficaService,
"ClienteFornitore" => $clienteFornitore, // PLACEHOLDER — to be implemented
"ClienteAnalisi" => $clienteAnalisi, // PLACEHOLDER — to be implemented
// "ClienteFatturazione" => $clienteFatturazione, // ⚠️ TEMP DISABLED - problemi API lato LIMS
// DeliveryRequest goes to Campione, not CommessaWeb
];
// Costruisci log curl-like per STEP 5
@@ -114,7 +327,7 @@ try {
// Salva log
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
file_put_contents($logFileStep5, $logContentStep5);
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
@@ -130,12 +343,34 @@ try {
throw new Exception("Invalid or missing idmatrice for part: " . ($part["part_number"] ?? "Unknown"));
}
$partDescription = $part["part_description"] ?? "";
$partNote = trim((string)($part["note"] ?? ""));
// Note aggiuntive dai custom field flaggati is_addnote
$partDescription = $part["part_description"] ?? "";
$partNote = trim((string)($part["note"] ?? ""));
// Componi NoteWeb: descrizione [- note] [- addnote di commessa]
$noteParts = [];
if ($partDescription !== "") {
$noteParts[] = $partDescription;
}
if ($partNote !== "") {
$noteParts[] = $partNote;
}
if ($addnoteText !== "") {
$noteParts[] = $addnoteText;
}
$noteWebValue = implode(" - ", $noteParts);
$campionePayload = [
"Commessa" => $commessaId,
"Matrice" => $matriceId,
"SottoMatrice" => null,
"Commessa" => $commessaId,
"Matrice" => $matriceId,
"SottoMatrice" => null,
"SchemaCustomField" => $schemaId,
"NoteWeb" => $part["part_description"] ?? ""
// "Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $noteWebValue,
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
];
// Costruisci curl-like per questo campione
@@ -160,20 +395,255 @@ try {
// Salva log per STEP 6
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
file_put_contents($logFileStep6, $logContentStep6);
$writeLog($logFileStep6, $logContentStep6, "STEP 6 - Campioni (commessa={$commessaId})");
// 🔹 STEP 6.0: PATCH each Campione custom fields:
// - field 189 (Tested Component) = part_description
// - additional fields from identification_parts_customfields (field_id → value_id/value_text)
$logContentStep63 = "";
foreach ($campioni as $index => $campione) {
$campioneId = (int)($campione['IdCampione'] ?? 0);
$partDescription = $parts[$index]['part_description'] ?? '';
$partId = (int)($parts[$index]['part_id'] ?? 0);
if ($campioneId <= 0) {
continue;
}
// Build a map of overrides: IdCustomField => value
$overrides = [];
// Override 1: Tested Component (field 189) = part_description
if ($partDescription !== '') {
$overrides[189] = $partDescription;
}
// Override 2: values from identification_parts_customfields
$partCFs = $partsCustomFields[$partId] ?? [];
foreach ($partCFs as $pcf) {
$cfFieldId = (int)$pcf['field_id'];
$cfValue = $pcf['value_text'] ?? $pcf['value_id'] ?? null;
if ($cfFieldId > 0 && $cfValue !== null && $cfValue !== '') {
$overrides[$cfFieldId] = (string)$cfValue;
}
}
// Skip if nothing to override
if (empty($overrides)) {
continue;
}
// GET campione custom fields
$campioneWithFields = $api->get("Campione({$campioneId})?\$expand=CampioniCustomFields(\$expand=CustomField)");
$logContentStep63 .= "GET Campione({$campioneId}) CustomFields:\n" .
json_encode($campioneWithFields['CampioniCustomFields'] ?? [], JSON_PRETTY_PRINT) . "\n\n";
$logContentStep63 .= "Overrides for part {$partId}: " . json_encode($overrides) . "\n\n";
// Build PATCH payload — apply overrides where IdCustomField matches
$campioniCustomFields = [];
foreach ($campioneWithFields["CampioniCustomFields"] ?? [] as $cf) {
$definitionId = (int)($cf["CustomField"]["IdCustomField"] ?? 0);
$fieldInstanceId = (int)$cf["IdCampioniCustomFields"];
$currentValue = $cf["Valore"] ?? '';
$newValue = $overrides[$definitionId] ?? $currentValue;
$campioniCustomFields[] = [
"IdCampioniCustomFields" => $fieldInstanceId,
"Valore" => $newValue
];
}
if (!empty($campioniCustomFields)) {
$patchPayload = ["CampioniCustomFields" => $campioniCustomFields];
$patchJson = json_encode($patchPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContentStep63 .= "PATCH Campione({$campioneId}):\n" .
"curl --location --request PATCH '{$apiBaseUrl}Campione({$campioneId})' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$patchJson}'\n\n";
$patchResult = $api->patch("Campione({$campioneId})", $patchPayload);
$logContentStep63 .= "RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n---\n";
}
}
$logFileStep63 = $logDir . "commessa_{$commessaId}_campioni_customfields_step60_" . time() . ".txt";
$writeLog($logFileStep63, $logContentStep63, "STEP 6.0 - Campioni CustomFields (commessa={$commessaId})");
// 🔹 STEP 6.1: Fetch photos linked to this iddatadb
$stmtPhotos = $pdo->prepare("
SELECT id, file_path, file_name, StampaNelRapporto, PrimaPagina
FROM datadb_photos
WHERE iddatadb = :iddatadb
ORDER BY id ASC
");
$stmtPhotos->execute(['iddatadb' => $iddatadb]);
$photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
// 🔹 STEP 6.2: Upload photos to Campione .01 (fetched from API)
$photosUploaded = 0;
$logContentPhotos = "Photos for CommessaWeb {$commessaId} (iddatadb={$iddatadb}):\n";
$logContentPhotos .= "Total photos found: " . count($photos) . ", campioni: " . count($campioni) . "\n\n";
if (!empty($campioni) && !empty($photos)) {
// Fetch campioni list from API to find the .01 campione
$commessaCampioni = $api->get("CommessaWeb({$commessaId})?\$expand=Campioni");
$apiCampioni = $commessaCampioni['Campioni'] ?? [];
// Sort by CodiceCampione to find .01
usort($apiCampioni, fn($a, $b) => strcmp($a['CodiceCampione'] ?? '', $b['CodiceCampione'] ?? ''));
$mainCampione = $apiCampioni[0] ?? null;
$campioneId = (int)($mainCampione['IdCampione'] ?? 0);
$logContentPhotos .= "API Campioni order:\n";
foreach ($apiCampioni as $ac) {
$logContentPhotos .= " - {$ac['CodiceCampione']} (IdCampione: {$ac['IdCampione']})\n";
}
$logContentPhotos .= "Selected .01 campione: IdCampione={$campioneId}\n\n";
if ($campioneId > 0) {
$logContentPhotos .= "=== Campione {$campioneId} (main) ===\n";
foreach ($photos as $photo) {
$fullPath = resolvePhotoFullPath($photo['file_path'], $uploadDir, __DIR__);
if ($fullPath === null) {
$logContentPhotos .= "SKIP (file not found): file_path='{$photo['file_path']}'\n";
continue;
}
$photoEndpoint = "Campione({$campioneId})/UploadCampioneFile";
$stampaNelRapporto = !empty($photo['StampaNelRapporto']);
$primaPagina = !empty($photo['PrimaPagina']);
$logContentPhotos .= "curl --location --request POST '{$apiBaseUrl}{$photoEndpoint}' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--form 'file=@{$fullPath}'\n\n";
// Step 1: Upload file (flags are ignored by API during upload)
$photoResult = $api->postMultipart($photoEndpoint, $fullPath, $photo['file_name']);
$logContentPhotos .= "UPLOAD RESPONSE:\n" . json_encode($photoResult, JSON_PRETTY_PRINT) . "\n\n";
// Step 2: PATCH CampioneFile to set flags (StampaNelRapporto, PrimaPagina)
$campioneFileId = (int)($photoResult['IdCampioneFile'] ?? 0);
if ($campioneFileId > 0 && ($stampaNelRapporto || $primaPagina)) {
$patchPayload = [];
if ($stampaNelRapporto) {
$patchPayload['StampaNelRapporto'] = true;
}
if ($primaPagina) {
$patchPayload['PrimaPagina'] = true;
}
$patchEndpoint = "CampioneFile({$campioneFileId})";
$patchJsonLog = json_encode($patchPayload, JSON_PRETTY_PRINT);
$logContentPhotos .= "curl --location --request PATCH '{$apiBaseUrl}{$patchEndpoint}' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$patchJsonLog}'\n\n";
$patchResult = $api->patch($patchEndpoint, $patchPayload);
$logContentPhotos .= "PATCH RESPONSE:\n" . json_encode($patchResult, JSON_PRETTY_PRINT) . "\n\n";
}
$logContentPhotos .= "---\n";
$photosUploaded++;
}
} else {
$logContentPhotos .= "SKIP: main campione has invalid IdCampione\n";
}
} elseif (empty($campioni)) {
$logContentPhotos .= "SKIP: no campioni created, cannot upload photos\n";
}
$logFilePhotos = $logDir . "commessa_{$commessaId}_photos_step5_2_" . time() . ".txt";
$writeLog($logFilePhotos, $logContentPhotos, "STEP 6.2 - Photos (commessa={$commessaId})");
// 🔹 STEP 6.3: Add Analyses (AnalisiCampione) via Campione({id})/AddAnalisi bound action
$stmt = $pdo->prepare("
SELECT part_id, analysis_recordkey, analysis_name, analysis_method
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
");
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$partIdToIndex = [];
foreach ($parts as $idx => $part) {
$partIdToIndex[(int)$part['part_id']] = $idx;
}
$totalAnalyses = count($analysesRows);
$addedAnalyses = 0;
$failedAnalyses = [];
$logContentStep63Analisi = "Analyses for iddatadb={$iddatadb}: total={$totalAnalyses}\n\n";
foreach ($analysesRows as $a) {
$partId = (int)$a['part_id'];
$recordKey = trim((string)($a['analysis_recordkey'] ?? ''));
$idx = $partIdToIndex[$partId] ?? null;
if ($idx === null || !isset($campioni[$idx]) || $recordKey === '') {
$logContentStep63Analisi .= "SKIP (no campione for part_id={$partId} / empty recordkey): '{$recordKey}'\n";
continue;
}
$campioneId = (int)($campioni[$idx]['IdCampione'] ?? 0);
if ($campioneId <= 0) {
$logContentStep63Analisi .= "SKIP (invalid IdCampione for part_id={$partId}): '{$recordKey}'\n";
continue;
}
$payload = ['RecordKey' => $recordKey];
$jsonPayload = json_encode($payload, JSON_UNESCAPED_SLASHES);
$logContentStep63Analisi .= "curl --location --request POST '{$apiBaseUrl}Campione({$campioneId})/AddAnalisi' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n";
try {
$result = $api->post("Campione({$campioneId})/AddAnalisi", $payload);
$logContentStep63Analisi .= "OK (part_id={$partId}, campione={$campioneId}): " .
($a['analysis_name'] ?? '') . "\n---\n";
$addedAnalyses++;
} catch (Exception $e) {
$errMsg = $e->getMessage();
$logContentStep63Analisi .= "FAIL: {$errMsg}\n---\n";
$failedAnalyses[] = [
'part_id' => $partId,
'campione_id' => $campioneId,
'analysis_recordkey' => $recordKey,
'analysis_name' => $a['analysis_name'] ?? '',
'error' => $errMsg,
];
}
}
$logFileStep63Analisi = $logDir . "commessa_{$commessaId}_analyses_step63_" . time() . ".txt";
$writeLog($logFileStep63Analisi, $logContentStep63Analisi, "STEP 6.3 - AddAnalisi (commessa={$commessaId})");
// 🔹 STEP 7: Update Custom Fields for CommessaWeb
if (!empty($fieldValues)) {
// GET con espansione per CustomField
$expand = "CommesseCustomFields(\$expand=CustomField)";
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
$commessaWithFields = $api->get("CommessaWeb({$commessaId})?\$expand={$expand}");
// Logga il GET
$logContentGet = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaWithFields, JSON_PRETTY_PRINT);
$logFileGet = $logDir . "commessa_{$commessaId}_get_step7_" . time() . ".txt";
file_put_contents($logFileGet, $logContentGet);
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
// Prepara payload PATCH
$commessaCustomFields = [];
@@ -186,7 +656,7 @@ try {
// Valida se il campo è di tipo Data
if ($fieldType === 'Data' && $newValue !== $currentValue) {
$newValue = validateDate($newValue);
$newValue = formatDateToExport($newValue);
}
$commessaCustomFields[] = [
@@ -195,6 +665,7 @@ try {
];
}
$logFileStep7 = null;
if (!empty($commessaCustomFields)) {
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
@@ -209,14 +680,14 @@ try {
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
file_put_contents($logFileStep7, $logContentStep7);
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, and status
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, status and export date
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l'
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb, status = 'l', export_date = NOW()
WHERE iddatadb = :iddatadb
");
$stmt->execute([
@@ -226,18 +697,45 @@ try {
]);
// 🔹 STEP 9: Send CommessaWeb to laboratory (commentato come richiesto)
/*
$sendResult = $api->post("CommessaWeb({$commessaId})/InviaCommessa", []);
// Logga il POST
$logContentStep9 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/InviaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{}'\n\n" .
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{}'\n\n" .
"RESPONSE:\n" . json_encode($sendResult, JSON_PRETTY_PRINT);
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
file_put_contents($logFileStep9, $logContentStep9);
*/
$writeLog($logFileStep9, $logContentStep9, "STEP 9 - InviaCommessa (commessa={$commessaId})");
// 🔹 STEP 9.5: Importazione da CommessaWeb a Commessa (con retry)
// Supplier call: POST api/odata/CommessaWeb(XXX)/ImportaCommessa
$importUserId = (!empty($lims_global_user_id) && is_numeric($lims_global_user_id))
? (int) $lims_global_user_id
: 285;
$importPayload = [
"IdUtente" => $importUserId
];
$importPayloadLog = json_encode($importPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$importOutcome = importaCommessaWithRetry($api, $commessaId, $importPayload);
$importResult = $importOutcome['result'];
$importStato = $importOutcome['stato'];
$importSucceeded = $importOutcome['succeeded'];
// Logga il POST (tutti i tentativi)
$logContentStep91 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb({$commessaId})/ImportaCommessa' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$importPayloadLog}'\n\n" .
"ATTEMPTS:\n" . $importOutcome['log'] . "\n" .
"SUCCEEDED: " . ($importSucceeded ? 'yes' : 'NO') . "\n\n" .
"LAST RESPONSE:\n" . json_encode($importResult, JSON_PRETTY_PRINT);
$logFileStep91 = $logDir . "commessa_{$commessaId}_importa_step91_" . time() . ".txt";
$writeLog($logFileStep91, $logContentStep91, "STEP 9.5 - ImportaCommessa (commessa={$commessaId}, succeeded=" . ($importSucceeded ? '1' : '0') . ")");
// 🔹 STEP 10: GET di controllo post-PATCH
$expand = "CommesseCustomFields(\$expand=CustomField)";
@@ -248,8 +746,24 @@ try {
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
file_put_contents($logFileStep10, $logContentStep10);
$writeLog($logFileStep10, $logContentStep10, "STEP 10 - GET verify (commessa={$commessaId})");
// 🔹 STEP 10.1: Save final CodiceCommessa into datadb.commessaweb
// After ImportaCommessa, the API returns the final LIMS job code in CodiceCommessa.
// Example: CodiceCommessa = 2614795, CodiceCommessaWeb = 26C0103.
$finalCodiceCommessa = trim((string)($commessaAfterPatch['CodiceCommessa'] ?? ''));
if ($finalCodiceCommessa !== '') {
$stmt = $pdo->prepare("
UPDATE datadb
SET commessaweb = :commessaweb,
status = 'l'
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'commessaweb' => substr($finalCodiceCommessa, 0, 30),
'iddatadb' => $iddatadb
]);
}
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
@@ -262,17 +776,26 @@ try {
];
echo json_encode([
"success" => true,
"commessaWeb" => $finalCommessa,
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
"step6_campioni" => $logFileStep6,
"step7_patch" => $logFileStep7,
"step10_get" => $logFileStep10
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"message" => "Export successful",
"logFiles" => [
"step5_create" => $logFileStep5,
"step5_2_photos" => $logFilePhotos,
"step6_campioni" => $logFileStep6,
"step63_analyses" => $logFileStep63Analisi,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91,
"step10_get" => $logFileStep10
]
]);
} catch (Exception $e) {
@@ -282,10 +805,13 @@ try {
"success" => false,
"message" => "Export failed: " . $e->getMessage(),
"logFiles" => [
"step5_create" => $logFileStep5 ?? null,
"step6_campioni" => $logFileStep6 ?? null,
"step7_patch" => $logFileStep7 ?? null,
"step10_get" => $logFileStep10 ?? null
"step5_create" => $logFileStep5 ?? null,
"step5_2_photos" => $logFilePhotos ?? null,
"step6_campioni" => $logFileStep6 ?? null,
"step63_analyses" => $logFileStep63Analisi ?? null,
"step7_patch" => $logFileStep7 ?? null,
"step9_1_importa" => $logFileStep91 ?? null,
"step10_get" => $logFileStep10 ?? null
]
]);
}
+394
View File
@@ -0,0 +1,394 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/include/grid_data_builder.php';
header('Content-Type: application/json');
$pdo = DBHandlerSelect::getInstance()->getConnection();
const FILTER_ALLOWED_PER_PAGE = [20, 40, 60, 100];
const FILTER_ALLIDS_CAP = 5000;
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$templateId = (int)($body['template_id'] ?? 0);
if ($templateId <= 0) {
echo json_encode(['success' => false, 'message' => 'template_id mancante']);
exit;
}
$status = in_array($body['status'] ?? 'i', ['i', 'P', 'l'], true) ? $body['status'] : 'i';
$showAll = !empty($body['all_users']);
$importref = trim((string)($body['importref'] ?? '')); // scope su un singolo import
$filters = is_array($body['filters'] ?? null) ? $body['filters'] : [];
$userId = (int)($iduserlogin ?? 0);
$perPage = in_array((int)($body['per_page'] ?? 20), FILTER_ALLOWED_PER_PAGE, true) ? (int)$body['per_page'] : 20;
$page = max(1, (int)($body['page'] ?? 1));
$wantAllIds = !empty($body['want_all_ids']);
$cacheDir = __DIR__ . '/cache';
$explicitIds = [];
if (isset($body['ids']) && is_array($body['ids'])) {
$explicitIds = array_values(array_unique(array_filter(
array_map('intval', $body['ids']),
fn($v) => $v > 0
)));
}
if (!empty($explicitIds)) {
// Scoping di sicurezza: solo record del template/utente/import corretti.
$conds = ['d.templateid = ?', 'd.status = ?'];
$params = [$templateId, $status];
if (!$showAll) { $conds[] = 'd.user_id = ?'; $params[] = $userId; }
if ($importref !== '') { $conds[] = 'd.importreferencecode = ?'; $params[] = $importref; }
$ph = implode(',', array_fill(0, count($explicitIds), '?'));
$conds[] = "d.iddatadb IN ($ph)";
foreach ($explicitIds as $id) $params[] = $id;
$whereSql = implode(' AND ', $conds);
try {
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d WHERE {$whereSql}");
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
$idStmt = $pdo->prepare("
SELECT d.iddatadb FROM datadb d
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT {$perPage} OFFSET {$offset}
");
$idStmt->execute($params);
$pageIds = array_map('intval', $idStmt->fetchAll(PDO::FETCH_COLUMN));
$config = buildGridConfig($pdo, $templateId);
$rows = buildGridRows($pdo, $pageIds, $config);
echo json_encode([
'success' => true,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
'rows' => $rows,
'mode' => 'selection',
]);
} catch (Exception $e) {
error_log('filter_records (ids mode) error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'ClienteFatturazione' => 'ClienteFatturazione',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
$staticCols = ['importreferencecode', 'filename_import', 'importdate', 'commessaweb'];
// ── Risolutori "etichetta → lista di ID" via cache ────────────────────────
function cacheItems(string $file): array
{
if (!is_file($file)) return [];
$data = json_decode(file_get_contents($file), true);
if (!is_array($data)) return [];
return $data['value'] ?? $data ?? [];
}
/** Clienti: label "Nome - X - id" (come search_clienti.php). */
function resolveClientIdsByTerm(string $cacheDir, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$ids = [];
foreach (cacheItems($cacheDir . '/clienti.json') as $c) {
$name = trim($c['Nominativo'] ?? '');
$id = trim((string)($c['IdCliente'] ?? ''));
$code = trim((string)($c['CodiceCliente'] ?? ''));
$parts = explode('_', $code);
$suffix = trim($parts[1] ?? '');
if ($suffix === '' && $code !== '') $suffix = substr($code, 0, 1);
if ($suffix === '') $suffix = '--';
$label = mb_strtolower($name . ' - ' . $suffix . ' - ' . $id);
if (mb_strpos($label, $term) !== false) $ids[] = (int)$c['IdCliente'];
}
return array_values(array_unique($ids));
}
/** Dropdown generico: matcha il termine su qualsiasi label field, ritorna gli id. */
function resolveIdsFromCache(string $file, string $idField, array $labelFields, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$ids = [];
foreach (cacheItems($file) as $it) {
$hay = '';
foreach ($labelFields as $lf) $hay .= ' ' . ($it[$lf] ?? '');
if (mb_strpos(mb_strtolower($hay), $term) !== false && isset($it[$idField])) {
$ids[] = (int)$it[$idField];
}
}
return array_values(array_unique($ids));
}
/**
* Per un fixed field che memorizza un ID, risolve il termine in lista di id.
* Ritorna: array di id (IN), [] se il termine non matcha nulla, null se la colonna
* non è risolvibile ( il chiamante la salta invece di fare LIKE sbagliato).
*/
function resolveFixedIds(string $cacheDir, string $key, string $term): ?array
{
switch ($key) {
case 'ClienteAnalisi':
case 'ClienteFatturazione':
return resolveClientIdsByTerm($cacheDir, $term);
case 'MoltiplicatorePrezzo':
return resolveIdsFromCache($cacheDir . '/moltiplicatori_prezzo.json', 'IdMoltiplicatorePrezzo', ['Codice', 'Descrizione'], $term);
case 'AnagraficaCertestObject':
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_object.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
case 'AnagraficaCertestService':
return resolveIdsFromCache($cacheDir . '/anagrafica_certest_service.json', 'IdAnagrafica', ['Codice', 'NomeAnagrafica'], $term);
default:
// ClienteResponsabile (per-cliente) e altri: non risolvibili globalmente
return null;
}
}
/**
* @return array<array{0:int,1:int}> lista di [idclient, idResponsabile]
*/
function resolveResponsabilePairs(string $cacheDir, string $term): array
{
$term = mb_strtolower(trim($term));
if ($term === '') return [];
$pairs = [];
foreach (glob($cacheDir . '/cliente_responsabili_*.json') ?: [] as $file) {
if (!preg_match('/cliente_responsabili_(\d+)\.json$/', $file, $m)) continue;
$cid = (int)$m[1];
$data = json_decode(file_get_contents($file), true);
foreach (($data['Responsabili'] ?? []) as $r) {
$name = mb_strtolower((string)($r['Nominativo'] ?? ''));
if ($name !== '' && mb_strpos($name, $term) !== false && isset($r['IdClienteResponsabile'])) {
$pairs[] = [$cid, (int)$r['IdClienteResponsabile']];
}
}
}
return $pairs;
}
/** Meta di un mapping (data_type, field_id), con cache statica. */
function mappingMeta(PDO $pdo, int $mappingId): array
{
static $cache = [];
if (!array_key_exists($mappingId, $cache)) {
$st = $pdo->prepare("SELECT data_type, field_id FROM template_mapping WHERE id = ?");
$st->execute([$mappingId]);
$cache[$mappingId] = $st->fetch(PDO::FETCH_ASSOC) ?: ['data_type' => 'Testo', 'field_id' => null];
}
return $cache[$mappingId];
}
function resolveCustomFieldValueIds(string $cacheDir, $fieldId, string $term): array
{
$fieldId = (int)$fieldId;
$term = mb_strtolower(trim($term));
if ($fieldId <= 0 || $term === '') return [];
$ids = [];
foreach (cacheItems($cacheDir . "/customfield_{$fieldId}.json") as $it) {
$label = mb_strtolower((string)($it['Valore'] ?? ''));
if (mb_strpos($label, $term) !== false && isset($it['IdCustomFieldsValue'])) {
$ids[] = (int)$it['IdCustomFieldsValue'];
}
}
return array_values(array_unique($ids));
}
// ── Costruzione WHERE ──────────────────────────────────────────────────────
$conds = ['d.templateid = ?', 'd.status = ?'];
$params = [$templateId, $status];
$needUserJoin = false;
if (!$showAll) {
$conds[] = 'd.user_id = ?';
$params[] = $userId;
}
// Scope su un singolo import (come imported.php ?importref=)
if ($importref !== '') {
$conds[] = 'd.importreferencecode = ?';
$params[] = $importref;
}
/** Aggiunge una condizione "colonna id IN (lista risolta)". */
function addIdInCondition(array &$conds, array &$params, string $col, array $ids): void
{
if (empty($ids)) {
$conds[] = '1 = 0'; // termine dato ma nessun match → zero risultati
return;
}
$ph = implode(',', array_fill(0, count($ids), '?'));
$conds[] = "d.`{$col}` IN ($ph)";
foreach ($ids as $id) $params[] = (int)$id;
}
foreach ($filters as $f) {
$term = trim((string)($f['term'] ?? ''));
if ($term === '') continue;
$type = (string)($f['type'] ?? '');
$key = (string)($f['key'] ?? '');
$like = '%' . $term . '%';
switch ($type) {
case 'detail':
case 'main_field':
$mappingId = (int)$key;
if ($mappingId <= 0) break;
$mm = mappingMeta($pdo, $mappingId);
if (($mm['data_type'] ?? '') === 'SceltaMultipla') {
// Dropdown: field_value = ID valore → risolvi label→id, match IN (= non LIKE)
$vids = resolveCustomFieldValueIds($cacheDir, $mm['field_id'], $term);
if (empty($vids)) {
$conds[] = '1 = 0';
break;
}
$inph = implode(',', array_fill(0, count($vids), '?'));
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value IN ($inph))";
$params[] = $mappingId;
foreach ($vids as $vid) $params[] = $vid;
} else {
// Testo / Data: match testuale
$conds[] = "EXISTS (SELECT 1 FROM import_data_details x
WHERE x.id = d.iddatadb AND x.mapping_id = ? AND x.field_value LIKE ?)";
$params[] = $mappingId;
$params[] = $like;
}
break;
case 'idclient':
addIdInCondition($conds, $params, 'idclient', resolveClientIdsByTerm($cacheDir, $term));
break;
case 'cliente_fornitore_id':
addIdInCondition($conds, $params, 'cliente_fornitore_id', resolveClientIdsByTerm($cacheDir, $term));
break;
case 'tested_component':
$conds[] = 'd.tested_component LIKE ?';
$params[] = $like;
break;
case 'status':
// Il dropdown invia direttamente il codice ('i'/'P'/'l').
if (in_array($term, ['i', 'P', 'l'], true)) {
$conds[] = 'd.status = ?';
$params[] = $term;
} else {
$conds[] = '1 = 0';
}
break;
case 'fixed':
$col = $fixedAliasMap[$key] ?? null;
if ($col === null) break;
if ($key === 'ConsegnaRichiesta') {
// Data: match testuale sul valore data (es. "2026-03").
$conds[] = "CAST(d.`{$col}` AS CHAR) LIKE ?";
$params[] = $like;
} elseif ($key === 'ClienteResponsabile') {
// Match sulla coppia (idclient, id responsabile).
$pairs = resolveResponsabilePairs($cacheDir, $term);
if (empty($pairs)) {
$conds[] = '1 = 0';
break;
}
$rowvals = implode(',', array_fill(0, count($pairs), '(?,?)'));
$conds[] = "(d.idclient, d.`{$col}`) IN ($rowvals)";
foreach ($pairs as $pr) {
$params[] = $pr[0];
$params[] = $pr[1];
}
} else {
$ids = resolveFixedIds($cacheDir, $key, $term);
if ($ids === null) break; // non risolvibile → non filtrare (niente LIKE su id)
addIdInCondition($conds, $params, $col, $ids);
}
break;
case 'static':
if ($key === 'user_name') {
$needUserJoin = true;
$conds[] = "CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,'')) LIKE ?";
$params[] = $like;
} elseif (in_array($key, $staticCols, true)) {
$conds[] = "d.`{$key}` LIKE ?";
$params[] = $like;
}
break;
default:
break; // tracking/awb ecc. → non filtrabili
}
}
$whereSql = implode(' AND ', $conds);
$joinSql = $needUserJoin ? 'LEFT JOIN auth_users u ON d.user_id = u.id' : '';
try {
// Totale + pagine
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$joinSql} WHERE {$whereSql}");
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));
if ($page > $totalPages) $page = $totalPages;
$offset = ($page - 1) * $perPage;
// Pagina corrente
$pageStmt = $pdo->prepare("
SELECT d.iddatadb
FROM datadb d {$joinSql}
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT {$perPage} OFFSET {$offset}
");
$pageStmt->execute($params);
$pageIds = array_map('intval', $pageStmt->fetchAll(PDO::FETCH_COLUMN));
$config = buildGridConfig($pdo, $templateId);
$rows = buildGridRows($pdo, $pageIds, $config);
$out = [
'success' => true,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
'rows' => $rows,
];
// Tutti gli id del match (per "seleziona tutti"), solo su richiesta
if ($wantAllIds) {
$allStmt = $pdo->prepare("
SELECT d.iddatadb FROM datadb d {$joinSql}
WHERE {$whereSql}
ORDER BY d.excelrow ASC, d.iddatadb ASC
LIMIT " . FILTER_ALLIDS_CAP . "
");
$allStmt->execute($params);
$out['all_ids'] = array_map('intval', $allStmt->fetchAll(PDO::FETCH_COLUMN));
$out['all_ids_capped'] = $total > FILTER_ALLIDS_CAP;
}
echo json_encode($out);
} catch (Exception $e) {
error_log('filter_records error: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint: GET api/odata/AnagraficaCertestObject
$endpoint = 'AnagraficaCertestObject';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = []; // es: ['$top' => 100]
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/anagrafica_certest_object_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint: GET api/odata/AnagraficaCertestService
$endpoint = 'AnagraficaCertestService';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = []; // es: ['$top' => 100]
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/anagrafica_certest_service_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+50
View File
@@ -0,0 +1,50 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint per recuperare le Analisi
$endpoint = 'Analisi';
// Opzioni OData
$options = [
// Restituisce solo i campi principali utili
'$select' => 'IdAnalisi,Codice,NomeAnalisi,ClientiAbilitati,MatriciAbilitate,IsGenerico,Tipo,ParentKey,SelezionabileSuWeb',
// Solo analisi effettivamente selezionabili sul web
'$filter' => 'SelezionabileSuWeb eq true',
// Ordinamento alfabetico per nome analisi
'$orderby' => 'NomeAnalisi asc'
];
// Debug: salva URL usato
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_analisi_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/analisi_response.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/analisi_error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,68 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
if ($idCliente <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_cliente']);
exit;
}
$api = VisualLimsApiClient::getInstance();
if ($idMatrice > 0) {
$expandExpr = "AnalisiAbilitate(\$filter=Matrice/IdMatrice eq $idMatrice)";
} else {
$expandExpr = "AnalisiAbilitate";
}
// Encode only the $expand expression because it contains spaces, slash and parentheses
$expandEncoded = rawurlencode($expandExpr);
$endpoint = "Cliente($idCliente)?\$expand={$expandEncoded}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
$data = $api->get($endpoint, []);
file_put_contents(
__DIR__ . '/analisi_by_cliente_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
if ($debug === 1) {
echo json_encode([
'endpoint' => $endpoint,
'full_url' => $full_url,
'raw_response' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$analisi = $data['AnalisiAbilitate'] ?? [];
echo json_encode([
'value' => $analisi,
'count' => is_array($analisi) ? count($analisi) : 0
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,56 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
if ($idMatrice <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_matrice']);
exit;
}
$api = VisualLimsApiClient::getInstance();
/**
* OData hypothesis:
* - Expand enabled matrices
* - Return only selectable analyses
* - Include generic analyses OR analyses enabled for the given matrix
*
* This endpoint must be verified against the real VisualLims API metadata.
*/
$filter = rawurlencode("SelezionabileSuWeb eq true and (IsGenerico eq true or MatriciAbilitate/any(m:m/IdMatrice eq $idMatrice))");
$expand = rawurlencode("MatriciAbilitate");
$endpoint = "Analisi?\$top=10";
// Debug URL
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
$data = $api->get($endpoint, []);
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
$analisi = $data['value'] ?? [];
echo json_encode(['value' => $analisi], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,85 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idMatrice = isset($_GET['id_matrice']) ? (int)$_GET['id_matrice'] : 0;
$debug = isset($_GET['debug']) ? (int)$_GET['debug'] : 0;
if ($idMatrice <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_matrice']);
exit;
}
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
$webOnly = isset($_GET['web_only']) ? (int)$_GET['web_only'] : 1;
$refresh = isset($_GET['refresh']) ? (int)$_GET['refresh'] : 0;
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
$filterString = "Matrice/IdMatrice eq $idMatrice";
if ($webOnly === 1) {
$filterString .= " and SelezionabileSuWeb eq true";
}
$filter = rawurlencode($filterString);
$endpoint = "Analisi?\$filter={$filter}";
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
// Cache file matches the one warm_cache.php writes (web_only=1 is what the analysis modal asks for).
// Caching skipped in simulate mode to avoid polluting real-data cache files.
$cache_file = __DIR__ . '/cache/analisi_matrice_' . $idMatrice . ($webOnly === 1 ? '' : '_all') . '.json';
$data = null;
$fromCache = false;
if (!$simulate && !$refresh && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
$fromCache = is_array($data);
}
if (!$fromCache) {
$data = $api->get($endpoint, []);
if (!$simulate) {
file_put_contents($cache_file, json_encode(['value' => $data['value'] ?? []], JSON_UNESCAPED_UNICODE));
}
}
if ($debug === 1) {
file_put_contents(__DIR__ . '/last_url_analisi.txt', $full_url . PHP_EOL, FILE_APPEND);
file_put_contents(
__DIR__ . '/analisi_by_matrice_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
'endpoint' => $endpoint,
'full_url' => $full_url,
'from_cache' => $fromCache,
'raw_response' => $data
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
$analisi = $data['value'] ?? [];
echo json_encode([
'value' => $analisi,
'count' => is_array($analisi) ? count($analisi) : 0
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+95
View File
@@ -0,0 +1,95 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Analisi che stai provando ad assegnare
$targetRecordKey = '11;1218320';
$targetIdAnalisi = '1218320';
// Endpoint: recupera il campione espandendo le analisi abilitate
$endpoint = "Campione($idCampione)";
// ATTENZIONE:
// nella tua classe normalmente le opzioni vengono trasformate in query string.
// Qui serve: ?$expand=AnalisiAbilitate
$options = [
'$expand' => 'AnalisiAbilitate'
];
// Debug: salva URL usato
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$query = urldecode($query); // rende leggibile $expand invece di %24expand
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url_analisi_abilitate.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupera AnalisiAbilitate dalla risposta
$analisiAbilitate = $data['AnalisiAbilitate'] ?? [];
// Alcune API OData possono restituire collection dentro "value"
if (isset($analisiAbilitate['value']) && is_array($analisiAbilitate['value'])) {
$analisiAbilitate = $analisiAbilitate['value'];
}
// Cerca se il RecordKey / IdAnalisi che stai usando è effettivamente assegnabile
$matches = [];
foreach ($analisiAbilitate as $analisi) {
$recordKey = isset($analisi['RecordKey']) ? (string)$analisi['RecordKey'] : '';
$idAnalisi = isset($analisi['IdAnalisi']) ? (string)$analisi['IdAnalisi'] : '';
if ($recordKey === $targetRecordKey || $idAnalisi === $targetIdAnalisi) {
$matches[] = $analisi;
}
}
// Output diagnostico
$output = [
'success' => true,
'idCampione' => $idCampione,
'request_url' => $full_url,
'targetRecordKey' => $targetRecordKey,
'targetIdAnalisi' => $targetIdAnalisi,
'enabled_analyses_count' => is_array($analisiAbilitate) ? count($analisiAbilitate) : 0,
'target_found' => count($matches) > 0,
'target_matches' => $matches,
'analisi_abilitate' => $analisiAbilitate,
'raw_response' => $data
];
// Salva il JSON in locale
file_put_contents(
__DIR__ . '/analisi_abilitate_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_analisi_abilitate.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+88
View File
@@ -0,0 +1,88 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Campione hardcoded del tuo esempio
$idCampione = 749027;
// Matrice attesa dal log STEP 6
$expectedMatriceId = 6430;
// Endpoint con expand della Matrice
$endpoint = "Campione($idCampione)";
$options = [
'$expand' => 'Matrice'
];
// Debug URL
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$query = http_build_query($options);
$queryReadable = urldecode($query);
$full_url = $base_url . $endpoint . ($queryReadable ? '?' . $queryReadable : '');
file_put_contents(
__DIR__ . '/last_url_check_matrice.txt',
$full_url . PHP_EOL,
FILE_APPEND
);
// Chiamata API
$data = $api->get($endpoint, $options);
// Recupero Matrice dalla response
$matrice = $data['Matrice'] ?? null;
$actualMatriceId = null;
if (is_array($matrice)) {
// Provo i nomi più probabili
$actualMatriceId = $matrice['IdMatrice']
?? $matrice['idMatrice']
?? $matrice['Id']
?? $matrice['ID']
?? null;
}
$matrice_ok = ((int)$actualMatriceId === (int)$expectedMatriceId);
$output = [
'success' => true,
'idCampione' => $idCampione,
'expectedMatriceId' => $expectedMatriceId,
'actualMatriceId' => $actualMatriceId,
'matrice_ok' => $matrice_ok,
'request_url' => $full_url,
'matrice' => $matrice,
'raw_response' => $data
];
// Salva JSON completo
file_put_contents(
__DIR__ . '/check_matrice_campione_749027_response.json',
json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log_check_matrice.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
if ($idCliente <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid id_cliente']);
exit;
}
$api = VisualLimsApiClient::getInstance();
// Build endpoint with OData $expand WITHOUT encoding '$' as %24
$endpoint = "Cliente($idCliente)?\$expand=Responsabili";
// Debug URL (real final URL)
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$full_url = $base_url . $endpoint;
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Call API: options must be empty because expand is already in endpoint
$data = $api->get($endpoint, []);
file_put_contents(__DIR__ . '/cliente_responsabili_response.json', json_encode($data, JSON_PRETTY_PRINT));
// Return only the list (standard shape used by the frontend)
$responsabili = $data['Responsabili'] ?? [];
echo json_encode(['value' => $responsabili]);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+31 -3
View File
@@ -9,11 +9,28 @@ ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceNazioneFatturazione',
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
@@ -57,10 +74,21 @@ try {
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
echo json_encode($data);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
+127
View File
@@ -0,0 +1,127 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// ── DEBUG MODE ───────────────────────────────────────────────────────
// Chiamata: get_clienti.php?debug=1
// Bypassa cache e $select per vedere TUTTI i campi grezzi restituiti
// dall'API per l'entità Cliente. Usare solo per ispezione manuale,
// NON lasciare linkato/usato in produzione dal frontend.
if (($_GET['debug'] ?? '') === '1') {
$debugParams = [
'$top' => 5,
'$orderby' => 'Nominativo asc'
// Nessun $select: chiediamo tutti i campi disponibili
];
$debugEndpoint = "Cliente?" . http_build_query($debugParams);
$debugData = $api->get($debugEndpoint);
$debugClients = $debugData['value'] ?? $debugData;
echo json_encode([
'mode' => 'debug - no $select, cache bypassed',
'top_level_keys' => is_array($debugData) ? array_keys($debugData) : null,
'fields_found_in_first_record' => isset($debugClients[0]) ? array_keys($debugClients[0]) : null,
'sample_records' => $debugClients,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
// ── FINE DEBUG MODE ──────────────────────────────────────────────────
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
// Costruisce query string con encoding corretto
$queryString = http_build_query($params);
// Componi endpoint finale
$endpoint = "Cliente?$queryString";
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
// Tenta la chiamata API
$data = $api->get($endpoint);
// Salva risposta per debug
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
// Forza il refresh del token
try {
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
// Ritarda leggermente prima del retry
usleep(500000); // 500ms
continue;
}
// Altri errori non gestiti dal retry
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
+103
View File
@@ -0,0 +1,103 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("
SELECT DISTINCT idclient
FROM datadb
WHERE idclient IS NOT NULL
AND idclient > 0
ORDER BY idclient ASC
");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients], JSON_UNESCAPED_UNICODE);
exit;
}
// Endpoint senza filtri: recupera tutto
$endpoint = 'Cliente?$top=50';
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$data = $api->get($endpoint);
// Save response for debug
file_put_contents(
__DIR__ . '/clienti_response.json',
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Retry only for specific auth/token related issue
if (
strpos($errorMessage, 'HTTP 400') !== false &&
strpos($errorMessage, 'Cannot persist the object') !== false
) {
try {
if (method_exists($api, 'refreshToken')) {
$api->refreshToken();
error_log("Tentativo {$retry}: refresh token eseguito per endpoint {$endpoint}");
} else {
throw new Exception('Il metodo refreshToken() non esiste in VisualLimsApiClient');
}
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
usleep(500000); // 500ms
continue;
}
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per {$endpoint}");
}
// Esegui la chiamata
$data = makeApiRequest($api, $endpoint);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse, JSON_UNESCAPED_UNICODE));
echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE);
}
+102
View File
@@ -0,0 +1,102 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake clients built from idclient values already in datadb.
// This ensures client dropdowns auto-select the correct row idclient, which in turn
// triggers the ClienteResponsabile select to populate via the mock.
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
$stmt = $pdo->query("SELECT DISTINCT idclient FROM datadb WHERE idclient IS NOT NULL AND idclient > 0 ORDER BY idclient ASC");
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
$fakeClients = array_map(fn($id) => [
'IdCliente' => (int) $id,
'Nominativo' => "Cliente Simulato {$id}",
'CodiceCliente' => "SIM_{$id}",
], $ids);
echo json_encode(['value' => $fakeClients]);
exit;
}
// Parametri OData
$params = [
'$select' => 'IdCliente,Nominativo,CodiceCliente,Inattivo',
'$orderby' => 'Nominativo asc'
];
// Costruisce query string con encoding corretto
$queryString = http_build_query($params);
// Componi endpoint finale
$endpoint = "Cliente?$queryString";
// Funzione per eseguire la chiamata con retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
// Tenta la chiamata API
$data = $api->get($endpoint);
// Salva risposta per debug
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Controlla se l'errore è legato all'autenticazione (HTTP 400 con messaggio specifico)
if (strpos($errorMessage, 'HTTP 400') !== false && strpos($errorMessage, 'Cannot persist the object') !== false) {
// Forza il refresh del token
try {
// Assumi che VisualLimsApiClient abbia un metodo per il refresh del token
$api->refreshToken(); // Da implementare in VisualLimsApiClient se non esiste
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
// Ritarda leggermente prima del retry
usleep(500000); // 500ms
continue;
}
// Altri errori non gestiti dal retry
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/clienti.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
// Esegui la chiamata con retry
$data = makeApiRequest($api, $endpoint);
$json = json_encode($data);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_clienti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
+13 -4
View File
@@ -21,17 +21,26 @@ try {
}
$results = [];
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0777, true);
foreach ($fieldIds as $customFieldId) {
$cacheFile = $cacheDir . '/customfield_' . $customFieldId . '.json';
// Use cache if fresh (1 hour)
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$results[$customFieldId] = json_decode(file_get_contents($cacheFile), true);
continue;
}
$endpoint = "CustomField($customFieldId)?\$expand=CustomFieldsValues";
$data = $api->get($endpoint);
$values = $data['CustomFieldsValues'] ?? [];
$results[$customFieldId] = $values;
$results[$customFieldId] = $data['CustomFieldsValues'] ?? [];
file_put_contents($cacheFile, json_encode($values));
}
// Debug ფაილი
file_put_contents(__DIR__ . '/customfield_values_response.json', json_encode($results));
echo json_encode($results);
} catch (Exception $e) {
http_response_code(500);
+93
View File
@@ -0,0 +1,93 @@
<?php
// get_fixed_field_data.php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
$field = $_GET['field'] ?? ''; // es: MoltiplicatorePrezzo, AnagraficaCertestObject, ...
if (!$field) {
http_response_code(400);
echo json_encode(['error' => 'Parametro "field" mancante']);
exit;
}
$api = VisualLimsApiClient::getInstance(); // also loads dotenv as a side-effect
$simulate = ($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true';
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$data = null;
$endpoint = null;
$cache_file = null;
$options = []; // qui puoi aggiungere filtri/ordering per campo se serve
try {
switch ($field) {
case 'MoltiplicatorePrezzo':
$endpoint = 'MoltiplicatorePrezzi';
$cache_file = __DIR__ . '/cache/moltiplicatori_prezzo.json';
break;
case 'AnagraficaCertestObject':
$endpoint = 'AnagraficaCertestObject';
$cache_file = __DIR__ . '/cache/anagrafica_certest_object.json';
break;
case 'AnagraficaCertestService':
$endpoint = 'AnagraficaCertestService';
$cache_file = __DIR__ . '/cache/anagrafica_certest_service.json';
break;
case 'ClienteResponsabile':
$id_cliente = (int)($_GET['id_cliente'] ?? 0);
if ($id_cliente <= 0) {
if (!$simulate) {
http_response_code(400);
echo json_encode(['error' => 'Manca o invalido id_cliente']);
exit;
}
$id_cliente = 1; // dummy — mock ignores the actual ID
}
$endpoint = "Cliente($id_cliente)?\$expand=Responsabili";
$cache_file = __DIR__ . '/cache/cliente_responsabili_' . $id_cliente . '.json';
break;
// case 'FuturoCampo5':
// $endpoint = 'QualcosaElse';
// break;
default:
http_response_code(400);
echo json_encode(['error' => "Campo non supportato: $field"]);
exit;
}
// Caching skipped in simulate mode to avoid polluting real-data cache files
if (!$simulate && $cache_file && file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) { // 1 ora
$data = json_decode(file_get_contents($cache_file), true);
} else {
$data = $api->get($endpoint, $options);
if (!$simulate && $cache_file) {
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
}
}
// Log ultimo URL (opzionale, per debug)
$full_url = $base_url . $endpoint . ($options ? '?' . http_build_query($options) : '');
file_put_contents(__DIR__ . '/last_urls.log', date('c') . " - $field - $full_url\n", FILE_APPEND);
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . " [$field] " . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+37 -50
View File
@@ -1,59 +1,46 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
use Dotenv\Dotenv;
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
// Set JSON header
header('Content-Type: application/json');
// Debug: Log the path where we expect the .env file
$envPath = dirname(__DIR__, 2);
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Expected .env path: ' . $envPath . PHP_EOL, FILE_APPEND);
// Carica il file .env dalla root del progetto
try {
$dotenv = Dotenv::createImmutable($envPath);
$dotenv->load();
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore caricamento .env: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore caricamento configurazione: ' . $e->getMessage()]);
exit(1);
}
// Read from matrici cache (populated by get_matrici.php / warm_cache.php)
$cacheFile = __DIR__ . '/cache/matrici.json';
if (file_exists($cacheFile)) {
$data = json_decode(file_get_contents($cacheFile), true);
$matrici = $data['value'] ?? [];
} else {
// Fallback: trigger cache creation
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$apiData = $api->get('Matrice');
$matrici = [];
foreach (($apiData['value'] ?? []) as $item) {
$nome = $item['NomeMatrice'] ?? '';
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
$matrici[] = ['IdMatrice' => $item['IdMatrice'], 'NomeMatrice' => $nome, 'MacroMatrice' => $item['MacroMatrice'] ?? null];
}
}
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, json_encode(['success' => true, 'value' => $matrici]));
}
// Recupera le variabili d'ambiente
$dbHost = $_ENV['DB_HOST'];
$dbName = $_ENV['DB_DATABASE'];
$dbUser = $_ENV['DB_USERNAME'];
$dbPass = $_ENV['DB_PASSWORD'];
$dbPrefix = $_ENV['DB_PREFIX'];
// Extract unique MacroMatrice values
$macroValues = [];
foreach ($matrici as $m) {
$macro = $m['MacroMatrice'] ?? null;
if ($macro !== null && $macro !== '' && substr($macro, 0, 1) !== '*') {
$macroValues[$macro] = true;
}
}
$macroMatrici = array_keys($macroValues);
sort($macroMatrici);
// Debug: Log database connection details (excluding password)
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . " - DB Connection: host=$dbHost, dbname=$dbName, user=$dbUser, prefix=$dbPrefix" . PHP_EOL, FILE_APPEND);
// Connessione al database MySQL
try {
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore connessione DB: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore connessione al database: ' . $e->getMessage()]);
exit(1);
}
try {
// Query per recuperare i valori distinti di MacroMatrice, escludendo quelli che iniziano con '*' e ordinandoli
$query = "SELECT DISTINCT MacroMatrice FROM {$dbPrefix}matrici WHERE MacroMatrice IS NOT NULL AND MacroMatrice NOT LIKE '*%' ORDER BY MacroMatrice ASC";
$stmt = $pdo->prepare($query);
$stmt->execute();
$macroMatrici = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Debug: Log del numero di MacroMatrice recuperate
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($macroMatrici) . ' MacroMatrice from database' . PHP_EOL, FILE_APPEND);
// Restituisci risposta JSON
echo json_encode(['success' => true, 'value' => $macroMatrici]);
} catch (PDOException $e) {
// Log errore e restituisci risposta di errore
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - Errore nel recupero delle MacroMatrice: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle MacroMatrice: ' . $e->getMessage()]);
exit(1);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+47
View File
@@ -0,0 +1,47 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
if (!Auth::check()) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; }
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
// Cache file (1 hour TTL)
$cacheFile = __DIR__ . '/cache/matrici.json';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
readfile($cacheFile);
exit;
}
$api = VisualLimsApiClient::getInstance();
$data = $api->get('Matrice');
$matrici = [];
if (isset($data['value']) && is_array($data['value'])) {
foreach ($data['value'] as $item) {
$nome = $item['NomeMatrice'] ?? '';
if ($nome !== '' && substr($nome, 0, 1) !== '*') {
$matrici[] = [
'IdMatrice' => $item['IdMatrice'],
'NomeMatrice' => $nome,
'MacroMatrice' => $item['MacroMatrice'] ?? null,
];
}
}
usort($matrici, fn($a, $b) => strcasecmp($a['NomeMatrice'], $b['NomeMatrice']));
}
$json = json_encode(['success' => true, 'value' => $matrici]);
if (!is_dir(__DIR__ . '/cache')) mkdir(__DIR__ . '/cache', 0777, true);
file_put_contents($cacheFile, $json);
echo $json;
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,55 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint per recuperare i Moltiplicatori Prezzo
// (dal documento: GET api/odata/MoltiplicatorePrezzi)
$endpoint = 'MoltiplicatorePrezzi';
// Opzionale: parametri OData ($top, $filter, $orderby, ecc.)
$options = [
'$orderby' => 'Descrizione asc'
];
// Debug: salva URL usata
$base_url = 'https://93.43.5.102/limsapi/api/odata/';
$queryParts = [];
foreach ($options as $k => $v) {
// mantieni il $ nella chiave, encoda solo il valore
$queryParts[] = $k . '=' . rawurlencode($v);
}
$query = implode('&', $queryParts);
$full_url = $base_url . $endpoint . ($query ? '?' . $query : '');
file_put_contents(__DIR__ . '/last_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// ✅ Force sort locally by "Descrizione" (A→Z)
if (isset($data['value']) && is_array($data['value'])) {
usort($data['value'], function ($a, $b) {
$da = isset($a['Descrizione']) ? trim((string)$a['Descrizione']) : '';
$db = isset($b['Descrizione']) ? trim((string)$b['Descrizione']) : '';
return strcasecmp($da, $db); // case-insensitive alphabetical
});
}
// Salva il JSON in locale
file_put_contents(__DIR__ . '/moltiplicatori_prezzo_response.json', json_encode($data, JSON_PRETTY_PRINT));
echo json_encode($data);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/include/headscript.php'; // o il tuo bootstrap standard
header('Content-Type: application/json');
try {
if (!isset($_GET['iddatadb']) || !is_numeric($_GET['iddatadb'])) {
echo json_encode(['success' => true, 'field' => null]);
exit;
}
$iddatadb = (int)$_GET['iddatadb'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// 1) prendo templateid da datadb
// 2) cerco max 1 record in template_mapping con is_visible_parts=1
$sql = "
SELECT tm.field_id, tm.field_label, tm.data_type, tm.has_list
FROM datadb d
JOIN template_mapping tm ON tm.template_id = d.templateid
WHERE d.iddatadb = ?
AND tm.is_visible_parts = 1
ORDER BY tm.id ASC
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'field' => $row ? $row : null
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -0,0 +1,256 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
/**
* Uso:
* rapporto_full_by_codice.php?codice=2621521
* rapporto_full_by_codice.php?codice=2621521&download=1
*/
try {
$api = VisualLimsApiClient::getInstance();
$codiceRapporto = trim($_GET['codice'] ?? '');
$downloadPdf = isset($_GET['download']) && $_GET['download'] == '1';
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2621521");
}
$debugDir = __DIR__ . '/logs/lims_rapporti/';
$pdfDir = __DIR__ . '/pdf/rapporti/';
if (!is_dir($debugDir)) mkdir($debugDir, 0755, true);
if (!is_dir($pdfDir)) mkdir($pdfDir, 0755, true);
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
/**
* STEP 1 - Search IdRapporto
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$rapporti = $searchData['value'] ?? [];
if (empty($rapporti)) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $rapporti[0];
$idRapporto = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$idRapporto) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/**
* STEP 2 - Dettaglio Rapporto
*/
$clienteExpandError = null;
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=Cliente,RapportiFiles,CampioniDatiRapporto";
try {
$detailData = $api->get($detailEndpoint);
} catch (Exception $e) {
$clienteExpandError = $e->getMessage();
$detailEndpoint = "Rapporto({$idRapporto})?\$expand=RapportiFiles,CampioniDatiRapporto";
$detailData = $api->get($detailEndpoint);
}
file_put_contents(
$debugDir . "rapporto_{$codiceRapportoSafe}_light.json",
json_encode($detailData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
$cliente = $detailData['Cliente'] ?? null;
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
$campioniDatiRapporto = $detailData['CampioniDatiRapporto'] ?? [];
// ====================== CLIENTE ======================
$clienteInfo = [
'found' => is_array($cliente),
'id_cliente' => $cliente['IdCliente'] ?? $cliente['Id'] ?? null,
'codice_cliente' => $cliente['CodiceCliente'] ?? $cliente['Codice'] ?? null,
'nome_cliente' => $cliente['Nominativo'] ?? $cliente['RagioneSociale'] ?? $cliente['Nome'] ?? $cliente['Descrizione'] ?? null,
'partita_iva' => $cliente['PartitaIva'] ?? null,
'codice_fiscale' => $cliente['CodiceFiscale'] ?? null
];
// ====================== FILE PDF ======================
$selectedRapportoFile = null;
$idRapportoFile = null;
$pdfFileName = $codiceRapporto . '.pdf';
$pdfFullPath = $pdfDir . $pdfFileName;
if (is_array($rapportiFiles) && count($rapportiFiles) > 0) {
foreach ($rapportiFiles as $file) {
$fileName = $file['FileName'] ?? '';
$tipoRapporto = $file['TipoRapporto'] ?? '';
$categoria = $file['Categoria'] ?? '';
if (
stripos($fileName, '.pdf') !== false ||
stripos($tipoRapporto, 'Rapporto') !== false ||
stripos($categoria, 'Rapporti') !== false
) {
$selectedRapportoFile = $file;
$idRapportoFile = $file['IdRapportoFile'] ?? null;
break;
}
}
if (!$selectedRapportoFile && count($rapportiFiles) > 0) {
$selectedRapportoFile = $rapportiFiles[0];
$idRapportoFile = $rapportiFiles[0]['IdRapportoFile'] ?? null;
}
}
// ====================== DOWNLOAD PDF - DEBUG AVANZATO ======================
$pdfDownloaded = false;
$pdfError = null;
$endpointUsato = null;
if ($downloadPdf && $idRapportoFile) {
$tentativi = [
"RapportoFile({$idRapportoFile})/\$value",
"RapportoFile({$idRapportoFile})/Contenuto/\$value",
"RapportoFile({$idRapportoFile})/File/\$value",
"Rapporto({$idRapporto})/RapportiFiles({$idRapportoFile})/\$value",
"Rapporto({$idRapporto})/RapportoFile/\$value",
"GetRapportoFile?IdRapportoFile={$idRapportoFile}",
"RapportoFile/Download?Id={$idRapportoFile}",
"Rapporto/DownloadFile?IdRapporto={$idRapporto}"
];
foreach ($tentativi as $ep) {
try {
$pdfContent = $api->getRaw($ep);
if (strlen($pdfContent) > 2000) { // PDF decente deve essere più grande
file_put_contents($pdfFullPath, $pdfContent);
$pdfDownloaded = true;
$endpointUsato = $ep;
$pdfError = "SUCCESSO con: " . $ep;
break;
} else {
$pdfError = "Contenuto troppo piccolo con: " . $ep;
}
} catch (Exception $e) {
$pdfError = "Fallito con " . $ep . "" . $e->getMessage();
}
}
}
// ====================== CAMPIONI + RATING (ripristinato dal tuo originale) ======================
$campioniSintesi = [];
$ratingFinale = null;
$ratingSource = null;
$hasIrregolare = false;
$hasPositiveResults = false;
if (is_array($campioniDatiRapporto)) {
foreach ($campioniDatiRapporto as $campione) {
$giudizioRapporto = $campione['GiudizioRapporto'] ?? null;
$giudizioCertificato = $campione['GiudizioCertificato'] ?? null;
$esitoGiudizioLMR = $campione['EsitoGiudizioLMR'] ?? null;
$esitoGiudizioImpiego = $campione['EsitoGiudizioImpiego'] ?? null;
$risultatiIrregolari = ($campione['RisultatiIrregolari'] ?? false) === true;
$risultatiPositivi = ($campione['RisultatiPositivi'] ?? false) === true;
$campioniSintesi[] = [
'id_campione_dati_rapporto' => $campione['IdCampioneDatiRapporto'] ?? null,
'codice_campione' => $campione['CodiceCampione'] ?? null,
'stato_campione' => $campione['StatoCampione'] ?? null,
'stato_campione_web' => $campione['StatoCampioneWeb'] ?? null,
'matrice' => $campione['Matrice'] ?? null,
'macro_matrice' => $campione['MacroMatrice'] ?? null,
'giudizio_rapporto' => $giudizioRapporto,
'giudizio_certificato' => $giudizioCertificato,
'esito_giudizio_lmr' => $esitoGiudizioLMR,
'esito_giudizio_impiego' => $esitoGiudizioImpiego,
'risultati_positivi' => $risultatiPositivi,
'risultati_irregolari' => $risultatiIrregolari
];
if (!$ratingFinale && !empty($giudizioRapporto)) {
$ratingFinale = $giudizioRapporto;
$ratingSource = 'CampioniDatiRapporto.GiudizioRapporto';
}
if (!$ratingFinale && !empty($giudizioCertificato)) {
$ratingFinale = $giudizioCertificato;
$ratingSource = 'CampioniDatiRapporto.GiudizioCertificato';
}
if ($risultatiIrregolari) $hasIrregolare = true;
if ($risultatiPositivi) $hasPositiveResults = true;
}
}
// Fallback rating
if (!$ratingFinale) {
if ($hasIrregolare) {
$ratingFinale = 'Irregolare';
$ratingSource = 'RisultatiIrregolari';
} elseif ($hasPositiveResults || count($campioniSintesi) > 0) {
$ratingFinale = 'Regolare';
$ratingSource = 'fallback';
}
}
// ====================== RISPOSTA FINALE ======================
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $idRapporto,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'cliente_expand_error' => $clienteExpandError,
'rapporto_base' => $rapportoBase,
'cliente' => $clienteInfo,
'rating_finale' => $ratingFinale,
'rating_source' => $ratingSource,
'rapporti_files_count' => count($rapportiFiles),
'selected_rapporto_file' => $selectedRapportoFile,
'pdf_file_name' => $pdfFileName,
'pdf_path' => $pdfDownloaded ? $pdfFullPath : null,
'pdf_downloaded' => $pdfDownloaded,
'pdf_error' => $pdfError,
'campioni_count' => count($campioniSintesi),
'campioni_sintesi' => $campioniSintesi,
'download_requested' => $downloadPdf
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$commessa = trim($_GET['commessa'] ?? '');
if ($commessa === '') {
throw new Exception("Parametro commessa mancante");
}
$debugDir = __DIR__ . '/logs/lims_rapporto/';
if (!is_dir($debugDir)) {
mkdir($debugDir, 0755, true);
}
/**
* STEP 1
* Provo a cercare il rapporto con filtri leggeri.
* NIENTE expand pesanti.
*/
$attempts = [];
$filters = [
'CodiceCommessa' => "CodiceCommessa eq '{$commessa}'",
'Commessa_CodiceCommessa' => "Commessa/CodiceCommessa eq '{$commessa}'",
'Commessa_IdCommessa' => is_numeric($commessa) ? "Commessa/IdCommessa eq {$commessa}" : null,
'Codice' => "Codice eq '{$commessa}'"
];
foreach ($filters as $label => $filter) {
if (!$filter) {
continue;
}
try {
$options = [
'$filter' => $filter,
'$top' => 10
];
$data = $api->get('Rapporto', $options);
$attempts[$label] = [
'success' => true,
'filter' => $filter,
'records' => isset($data['value']) && is_array($data['value']) ? count($data['value']) : null,
'data' => $data
];
file_put_contents(
$debugDir . "rapporto_{$commessa}_{$label}.json",
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
} catch (Exception $e) {
$attempts[$label] = [
'success' => false,
'filter' => $filter,
'error' => $e->getMessage()
];
}
}
/**
* STEP 2
* Prendo solo eventuali rapporti trovati.
*/
$rapportiFound = [];
foreach ($attempts as $label => $attempt) {
if (!$attempt['success']) {
continue;
}
$items = $attempt['data']['value'] ?? [];
if (!is_array($items)) {
continue;
}
foreach ($items as $item) {
$rapportiFound[] = [
'matched_by' => $label,
'rapporto' => $item
];
}
}
echo json_encode([
'success' => true,
'input_commessa' => $commessa,
'message' => 'Ricerca leggera su Rapporto completata. Se trovi un rapporto, poi recuperiamo RapportiFiles solo per quello.',
'rapporti_found_count' => count($rapportiFound),
'rapporti_found' => $rapportiFound,
'attempts_summary' => array_map(function ($a) {
return [
'success' => $a['success'],
'filter' => $a['filter'],
'records' => $a['records'] ?? null,
'error' => $a['error'] ?? null
];
}, $attempts)
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+125 -10
View File
@@ -2,25 +2,140 @@
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$rapporto_id = 533329;
// Costruzione manuale dell'endpoint con espansione annidata
$endpoint = "Rapporto($rapporto_id)?\$expand=CampioniDatiRapporto(\$expand=AnalisiDatiRapporto,CustomFieldsDatiRapporto)";
// Esempi:
// rapporto_by_codice_expand_step.php?codice=2541111&step=base
// rapporto_by_codice_expand_step.php?codice=2541111&step=files
// rapporto_by_codice_expand_step.php?codice=2541111&step=campioni
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
// Non passiamo options, già incluso nell'endpoint
$data = $api->get($endpoint);
$codiceRapporto = trim($_GET['codice'] ?? '');
$step = trim($_GET['step'] ?? 'base');
file_put_contents(__DIR__ . '/rapporto_expanded.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
echo json_encode($data);
if ($codiceRapporto === '') {
throw new Exception("Parametro codice mancante. Usa ?codice=2541111");
}
$allowedSteps = [
'base' => '',
'files' => 'RapportiFiles',
'allegati' => 'RapportiAllegati',
'campioni' => 'CampioniDatiRapporto',
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
];
if (!array_key_exists($step, $allowedSteps)) {
throw new Exception("Step non valido. Usa: " . implode(', ', array_keys($allowedSteps)));
}
// Escape OData per eventuali apostrofi
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
/*
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
* Query leggera, con $select e $top=1.
*/
$searchParams = [
'$filter' => "CodiceRapporto eq '{$codiceRapportoSafe}'",
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$top' => 1
];
$searchEndpoint = "Rapporto?" . http_build_query($searchParams);
$searchData = $api->get($searchEndpoint);
$items = $searchData['value'] ?? [];
if (!is_array($items) || count($items) === 0) {
echo json_encode([
'success' => false,
'message' => 'Nessun rapporto trovato per questo CodiceRapporto.',
'codice_rapporto' => $codiceRapporto,
'search_endpoint' => $searchEndpoint,
'search_data' => $searchData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
$rapportoBase = $items[0];
$rapportoId = intval($rapportoBase['IdRapporto'] ?? 0);
if (!$rapportoId) {
throw new Exception("IdRapporto non trovato nella risposta.");
}
/*
* STEP 2 - Se step=base, restituisco solo la ricerca base.
*/
if ($step === 'base') {
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'rapporto_base' => $rapportoBase
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
/*
* STEP 3 - Espande SOLO il rapporto trovato.
*/
$expandValue = $allowedSteps[$step];
$detailParams = [
'$expand' => $expandValue
];
$detailEndpoint = "Rapporto({$rapportoId})?" . http_build_query($detailParams);
file_put_contents(
__DIR__ . '/last_rapporto_by_codice_expand_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] SEARCH: ' . $searchEndpoint . PHP_EOL .
'[' . date('Y-m-d H:i:s') . '] DETAIL: ' . $detailEndpoint . PHP_EOL,
FILE_APPEND
);
$detailData = $api->get($detailEndpoint);
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
'step' => $step,
'search_endpoint' => $searchEndpoint,
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// In simulate mode: return fake users
if (($_ENV['SIMULATE_EXPORT_LIMS'] ?? '') === 'true') {
$fakeUsers = [
[
'IdUtente' => 1001,
'Nominativo' => 'Utente Simulato 1001'
],
[
'IdUtente' => 1002,
'Nominativo' => 'Utente Simulato 1002'
]
];
echo json_encode(['value' => $fakeUsers]);
exit;
}
// OData parameters
$params = [];
// Build query string
$queryString = http_build_query($params);
// Final endpoint
$endpoint = "Utente?$queryString";
// Function to execute request with retry
function makeApiRequest($api, $endpoint, $maxRetries = 3)
{
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$data = $api->get($endpoint);
// Save response for debug
file_put_contents(__DIR__ . '/utenti_response.json', json_encode($data, JSON_PRETTY_PRINT));
return $data;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Retry only on token/auth-related issue
if (
strpos($errorMessage, 'HTTP 400') !== false &&
strpos($errorMessage, 'Cannot persist the object') !== false
) {
try {
$api->refreshToken(); // must exist in VisualLimsApiClient
error_log("Tentativo $retry: Refresh token eseguito per endpoint $endpoint");
} catch (Exception $refreshEx) {
error_log("Errore durante il refresh del token: " . $refreshEx->getMessage());
throw new Exception("Impossibile eseguire il refresh del token: " . $refreshEx->getMessage());
}
usleep(500000); // 500 ms
continue;
}
throw $e;
}
}
throw new Exception("Massimo numero di tentativi raggiunto per $endpoint");
}
// Execute request
$data = makeApiRequest($api, $endpoint);
echo json_encode($data);
} catch (Exception $e) {
http_response_code(500);
$errorResponse = [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
error_log("Errore in get_utenti.php: " . json_encode($errorResponse));
echo json_encode($errorResponse);
}
+844
View File
@@ -0,0 +1,844 @@
/**
* gridFilter.js — Selezione righe + filtri colonna a scomparsa per imported.php
* Dipende da window.gridRenderer (gridRenderer.js). Includere DOPO gridRenderer.js.
*/
(function () {
"use strict";
let filtersActive = false;
let restricted = false;
const selected = new Set(); // iddatadb selezionati
const colFilters = {}; // colKey -> testo filtro
let restrictedIds = null; // Set di iddatadb visibili quando ristretto
function R() {
return window.gridRenderer;
}
function data() {
return R().getData();
}
function meta() {
return R().getMeta();
}
// ── Filtro SERVER-SIDE (paginato, come la lista normale ma filtrata) ──────
// Il filtro gira su TUTTO il template; il server restituisce UNA PAGINA di righe
// filtrate + il totale, e navighiamo le pagine del set filtrato via AJAX.
const TEMPLATE_ID =
new URLSearchParams(location.search).get("id") || (meta().templateId ?? "");
const SHOW_ALL =
new URLSearchParams(location.search).get("all_users") === "1";
const IMPORTREF =
new URLSearchParams(location.search).get("importref") || "";
const FILTER_PER_PAGE =
parseInt(new URLSearchParams(location.search).get("limit"), 10) || 20;
let serverFiltered = false;
let filterPage = 1;
let filterTotalPages = 1;
let filterTotal = 0;
let originalData = null; // snapshot della pagina originale (per ripristino)
let filterReqSeq = 0;
let showingSelection = false; // true quando mostriamo l'UNIONE dei selezionati
let pagerFn = null; // paginazione corrente (applyServerFilter | showSelectedRecords)
function collectActiveFilters() {
const cols = meta().columns || [];
const out = [];
for (const [key, term] of Object.entries(colFilters)) {
if (!term || !String(term).trim()) continue;
const col = cols.find((c) => String(c.key) === String(key));
if (!col) continue;
out.push({ key: String(key), type: col.type, term: String(term).trim() });
}
return out;
}
// Sostituisce il dataset del renderer in-place e ridisegna.
function swapData(rows) {
const arr = data();
arr.length = 0;
(rows || []).forEach((r) => arr.push(r));
R().renderVisibleRows();
}
function hidePagination(hide) {
// Durante il filtro la paginazione server (per pagina, via URL) è fuorviante.
document.querySelectorAll(".pager-bar").forEach((el) => {
el.style.display = hide ? "none" : "";
});
}
// Pager del set filtrato (Prec / pag X di Y / Succ) nella toolbar.
function updateFilterPager() {
const el = document.getElementById("filterPager");
if (!el) return;
if (!serverFiltered) {
el.style.display = "none";
el.innerHTML = "";
return;
}
el.style.display = "inline-flex";
const label = showingSelection ? "selezionati" : "trovati";
el.innerHTML =
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterPrevBtn" ${filterPage <= 1 ? "disabled" : ""}></button>` +
`<span style="font-size:12px;color:#333;">${filterTotal} ${label} · pag ${filterPage}/${filterTotalPages}</span>` +
`<button type="button" class="btn btn-outline-secondary btn-sm" id="filterNextBtn" ${filterPage >= filterTotalPages ? "disabled" : ""}></button>`;
const go = pagerFn || applyServerFilter;
const prev = document.getElementById("filterPrevBtn");
const next = document.getElementById("filterNextBtn");
if (prev) prev.addEventListener("click", () => go(filterPage - 1));
if (next) next.addEventListener("click", () => go(filterPage + 1));
}
function restoreOriginal() {
serverFiltered = false;
filterPage = 1;
filterTotalPages = 1;
filterTotal = 0;
if (originalData) swapData(originalData);
updateFilterPager();
hidePagination(false);
updateToolbar();
}
async function applyServerFilter(page = 1) {
const active = collectActiveFilters();
if (active.length === 0) {
if (serverFiltered) restoreOriginal();
return;
}
if (!originalData) originalData = [...data()]; // snapshot pagina 1 originale
const seq = ++filterReqSeq;
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
filters: active,
page: Math.max(1, page),
per_page: FILTER_PER_PAGE,
}),
});
const json = await resp.json();
if (seq !== filterReqSeq) return; // risposta obsoleta: ignora
if (!json.success) {
console.error("[gridFilter] filtro:", json.message);
return;
}
serverFiltered = true;
showingSelection = false;
pagerFn = applyServerFilter;
filterPage = json.page;
filterTotalPages = json.total_pages;
filterTotal = json.total;
swapData(json.rows || []);
updateFilterPager();
hidePagination(true);
updateToolbar();
} catch (e) {
console.error("[gridFilter] fetch filtro fallita", e);
}
}
// ── Mostra i SELEZIONATI (unione tra filtri diversi) ──────────────────
// La selezione si accumula tra filtri; questo carica dal server TUTTI i record
// selezionati (per id) e li mostra, paginati, a prescindere dai filtri correnti.
async function showSelectedRecords(page = 1) {
const ids = [...selected];
if (!ids.length) {
showingSelection = false;
updateToolbar();
return;
}
pagerFn = showSelectedRecords;
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
ids: ids,
page: Math.max(1, page),
per_page: FILTER_PER_PAGE,
}),
});
const json = await resp.json();
if (!json.success) {
console.error("[gridFilter] mostra selezionati:", json.message);
return;
}
if (!originalData) originalData = [...data()];
showingSelection = true;
serverFiltered = true;
filterPage = json.page;
filterTotalPages = json.total_pages;
filterTotal = json.total;
swapData(json.rows || []);
updateFilterPager();
hidePagination(true);
updateToolbar();
} catch (e) {
console.error("[gridFilter] mostra selezionati fallita", e);
}
}
// Tumbler "Mostra selezionati" / "Torna al filtro".
function toggleShowSelected() {
if (!showingSelection) {
if (!selected.size) return;
showSelectedRecords(1);
} else {
exitSelectionView();
}
}
// Torna dal "mostra selezionati" al filtro corrente (o alla pagina originale).
function exitSelectionView() {
showingSelection = false;
pagerFn = applyServerFilter;
if (collectActiveFilters().length) {
applyServerFilter(1);
} else {
restoreOriginal();
}
updateToolbar();
}
// Deseleziona tutto.
function clearSelection() {
selected.clear();
document
.querySelectorAll("#gridRowContainer .grid-row.row-selected")
.forEach((el) => el.classList.remove("row-selected"));
document
.querySelectorAll(".filter-row-checkbox")
.forEach((cb) => (cb.checked = false));
const sa = document.getElementById("filterSelectAll");
if (sa) sa.checked = false;
if (showingSelection) {
exitSelectionView();
} else {
updateToolbar();
}
}
// "Seleziona tutti i filtrati": chiede al server TUTTI gli id del match e li seleziona.
async function selectAllMatching(on) {
if (!serverFiltered) return;
const active = collectActiveFilters();
try {
const resp = await fetch("filter_records.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template_id: parseInt(TEMPLATE_ID, 10),
all_users: SHOW_ALL,
importref: IMPORTREF,
status: "i",
filters: active,
page: filterPage,
per_page: FILTER_PER_PAGE,
want_all_ids: true,
}),
});
const json = await resp.json();
if (!json.success) return;
(json.all_ids || []).forEach((id) => (on ? selected.add(id) : selected.delete(id)));
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
rowEl.classList.toggle("row-selected", on);
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = on;
});
updateToolbar();
if (json.all_ids_capped) {
alert("Selezione limitata a " + (json.all_ids || []).length + " record.");
}
} catch (e) {
console.error("[gridFilter] select-all fallita", e);
}
}
function syncFilterRow() {
const fr = document.getElementById("gridFilterRow");
const header = document.getElementById("gridHeaderContainer");
if (!fr || !header) return;
const actHeader = header.querySelector(".button-header");
const actCell = fr.querySelector(".filter-actions-cell");
if (actHeader && actCell) {
const aw = actHeader.offsetWidth;
actCell.style.flex = `0 0 ${aw}px`;
actCell.style.minWidth = `${aw}px`;
}
const headerCells = header.querySelectorAll(
".grid-header:not(.button-header)",
);
const filterCells = fr.querySelectorAll(
".grid-cell:not(.filter-actions-cell)",
);
headerCells.forEach((hc, i) => {
const fc = filterCells[i];
if (fc) fc.style.flex = `0 0 ${hc.offsetWidth}px`;
});
}
function syncStickyLeft() {
const btnHeader = document.querySelector(
"#gridHeaderContainer .button-header",
);
if (!btnHeader) return;
const w1 = btnHeader.offsetWidth; // larghezza reale colonna Actions
const fixRow = (rowEl) => {
if (!rowEl) return;
const cells = rowEl.querySelectorAll(
":scope > .grid-cell, :scope > .grid-header",
);
const c2 = cells[1];
const c3 = cells[2];
if (c2 && getComputedStyle(c2).position === "sticky") {
c2.style.left = `${w1}px`;
}
if (c3 && getComputedStyle(c3).position === "sticky") {
c3.style.left = `${w1 + (c2 ? c2.offsetWidth : 0)}px`;
}
};
fixRow(document.getElementById("gridHeaderContainer"));
document.querySelectorAll(".grid-top").forEach(fixRow);
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach(fixRow);
fixRow(document.getElementById("gridFilterRow"));
}
let _headerObs = null;
let _rowObs = null;
let _syncRaf = null;
function reconnectObservers() {
const header = document.getElementById("gridHeaderContainer");
const rows = document.getElementById("gridRowContainer");
if (_headerObs && header)
_headerObs.observe(header, {
attributes: true,
attributeFilter: ["style"],
subtree: true,
});
if (_rowObs && rows) _rowObs.observe(rows, { childList: true });
}
function scheduleSync() {
if (_syncRaf) return;
_syncRaf = requestAnimationFrame(() => {
_syncRaf = null;
if (_headerObs) _headerObs.disconnect();
if (_rowObs) _rowObs.disconnect();
syncStickyLeft();
syncFilterRow();
reconnectObservers();
});
}
function hookGridSync() {
_headerObs = new MutationObserver(scheduleSync);
_rowObs = new MutationObserver(scheduleSync);
reconnectObservers();
scheduleSync(); // primo allineamento
}
function rowMatchesFilters(row) {
if (serverFiltered) return true;
for (const [key, term] of Object.entries(colFilters)) {
if (!term) continue;
const t = term.toLowerCase();
const val = getRowColValue(row, key);
if (!String(val).toLowerCase().includes(t)) return false;
}
return true;
}
function getRowColValue(row, key) {
const cols = meta().columns || [];
const col = cols.find((c) => String(c.key) === String(key));
if (!col) return "";
switch (col.type) {
case "detail":
case "main_field":
return row.details?.[String(key)] ?? "";
case "fixed":
return row.fixedFields?.[key] ?? "";
case "idclient":
return row.idclient ?? "";
case "cliente_fornitore_id":
return row.cliente_fornitore_id ?? "";
case "tested_component":
return row.tested_component ?? "";
case "static":
return row[key] ?? "";
case "status":
return row.status ?? "";
default:
return row[key] ?? "";
}
}
// ── Applica visibilità righe (filtri + restrizione) ──
function applyVisibility() {
const rows = document.querySelectorAll("#gridRowContainer .grid-row");
rows.forEach((rowEl) => {
const id = parseInt(rowEl.dataset.id, 10);
const row = data().find((r) => String(r.iddatadb) === String(id));
let show = true;
if (row && !rowMatchesFilters(row)) show = false;
if (restricted && restrictedIds && !restrictedIds.has(id))
show = false;
rowEl.style.display = show ? "" : "none";
});
window.dispatchEvent(new Event("resize")); // ricalcola scrollbar top
}
// ── Lista iddatadb attualmente visibili (per propagazione/export ecc.) ──
window.getVisibleGridIds = function () {
if (serverFiltered) {
return data()
.map((r) => parseInt(r.iddatadb, 10))
.filter(Boolean);
}
const ids = [];
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((el) => {
if (el.style.display !== "none")
ids.push(parseInt(el.dataset.id, 10));
});
return ids.filter(Boolean);
};
// ── Righe target per operazioni bulk (propaga/export/clona/save) ──
// Regola: filtri attivi + selezione → solo selezionate;
// filtri attivi senza selezione → solo visibili;
// filtri non attivi → null (= tutte, comportamento originale).
window.getTargetGridIds = function () {
if (!filtersActive) return null; // null = nessun vincolo
if (selected.size > 0) return new Set([...selected]);
return new Set(window.getVisibleGridIds());
};
// Helper: true se l'id è tra i target (o se non c'è vincolo)
window.isTargetGridId = function (id) {
const t = window.getTargetGridIds();
if (t === null) return true;
return t.has(parseInt(id, 10));
};
// ── Toggle selezione riga ──
function toggleRow(id, on) {
id = parseInt(id, 10);
if (on) selected.add(id);
else selected.delete(id);
const rowEl = document.querySelector(
`#gridRowContainer .grid-row[data-id="${id}"]`,
);
if (rowEl) {
rowEl.classList.toggle("row-selected", selected.has(id));
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = selected.has(id);
}
updateToolbar();
}
// ── Inietta checkbox nelle righe visibili ──
// ── Inietta checkbox dentro la cella Actions (già sticky) ──
function injectCheckboxes() {
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
const btnCell = rowEl.querySelector(".button-cell");
if (!btnCell) return;
if (btnCell.querySelector(".filter-row-checkbox")) return;
const id = parseInt(rowEl.dataset.id, 10);
const wrap = document.createElement("label");
wrap.className = "filter-cb-wrap";
wrap.innerHTML = `<input type="checkbox" class="filter-row-checkbox" ${selected.has(id) ? "checked" : ""}>`;
btnCell.insertBefore(wrap, btnCell.firstChild);
if (selected.has(id)) rowEl.classList.add("row-selected");
});
}
function removeCheckboxes() {
document
.querySelectorAll(".filter-cb-wrap")
.forEach((el) => el.remove());
document
.querySelectorAll(".grid-row.row-selected")
.forEach((el) => el.classList.remove("row-selected"));
}
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
// ── Riga input filtri colonna (allineata alle larghezze reali) ──
function injectFilterRow() {
if (document.getElementById("gridFilterRow")) return;
const top = document.getElementById("gridTopContainer");
const header = document.getElementById("gridHeaderContainer");
if (!top || !header) return;
const fr = document.createElement("div");
fr.className = "grid-row grid-filter-row";
fr.id = "gridFilterRow";
// cella allineata alla colonna Actions (sticky, legge larghezza reale)
const actHeader = header.querySelector(".button-header");
const actCell = document.createElement("div");
actCell.className = "grid-cell button-cell filter-actions-cell";
const aw = actHeader ? actHeader.offsetWidth : 220;
actCell.style.flex = `0 0 ${aw}px`;
actCell.style.minWidth = `${aw}px`;
actCell.innerHTML = `<label class="filter-cb-wrap" style="display:inline-flex;" title="Seleziona tutti i filtrati"><input type="checkbox" id="filterSelectAll"></label>`;
fr.appendChild(actCell);
// una cella per colonna, larghezza letta dall'header reale
const headerCells = header.querySelectorAll(
".grid-header:not(.button-header)",
);
(meta().columns || []).forEach((col, i) => {
const hc = headerCells[i];
const w = hc ? hc.offsetWidth : col.width || 150;
const cell = document.createElement("div");
cell.className = "grid-cell";
cell.style.flex = `0 0 ${w}px`;
if (col.type === "tracking" || col.type === "awb") {
// niente filtro
} else if (col.type === "status") {
// Status: dropdown (3 valori) invece del testo libero.
const cur = colFilters[col.key] || "";
const opt = (v, lbl) =>
`<option value="${v}" ${cur === v ? "selected" : ""}>${lbl}</option>`;
cell.innerHTML =
`<select class="filter-col-input" data-col-key="${col.key}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">` +
opt("", "— tutti —") +
opt("i", "Imported") +
opt("P", "In Progress") +
opt("l", "To LIMS") +
`</select>`;
} else {
cell.innerHTML = `<input type="text" class="filter-col-input" data-col-key="${col.key}" placeholder="🔍 ${col.label || ""}" value="${colFilters[col.key] || ""}" style="width:100%;padding:3px 6px;font-size:12px;border:1px solid #ced4da;border-radius:4px;">`;
}
fr.appendChild(cell);
});
top.parentNode.insertBefore(fr, top);
scheduleSync(); // allinea larghezze filtri + offset sticky
}
function removeFilterRow() {
const fr = document.getElementById("gridFilterRow");
if (fr) fr.remove();
}
// ── Toolbar (bottoni contestuali) ──
function updateToolbar() {
const bar = document.getElementById("filterActionBar");
if (!bar) return;
const count = selected.size;
bar.style.display = filtersActive ? "inline-flex" : "none";
const cntEl = document.getElementById("filterSelCount");
if (cntEl) cntEl.textContent = count;
document.getElementById("filterDeleteBtn").disabled = count === 0;
const showBtn = document.getElementById("filterRestrictBtn");
showBtn.disabled = count === 0 && !showingSelection;
showBtn.innerHTML = showingSelection
? '<i class="fas fa-eye"></i> Torna al filtro'
: '<i class="fas fa-list-check"></i> Mostra selezionati';
const clearSelBtn = document.getElementById("filterClearSelBtn");
if (clearSelBtn) clearSelBtn.disabled = count === 0;
}
// ── Main toggle ──
function toggleFilters() {
filtersActive = !filtersActive;
const grid = document.getElementById("gridContainer");
const btn = document.getElementById("filtersToggleBtn");
if (filtersActive) {
grid.classList.add("filters-on");
btn.classList.add("active");
injectCheckboxes();
injectFilterRow();
} else {
grid.classList.remove("filters-on");
btn.classList.remove("active");
removeCheckboxes();
removeFilterRow();
// reset filtri colonna + ripristina la pagina originale
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
if (serverFiltered) restoreOriginal();
}
updateToolbar();
}
// ── Batch delete ──
async function batchDelete() {
const ids = [...selected];
if (!ids.length) return;
if (
!confirm(
`Eliminare ${ids.length} righe dal database? Operazione irreversibile.`,
)
)
return;
const btn = document.getElementById("filterDeleteBtn");
btn.disabled = true;
const original = btn.innerHTML;
let ok = 0,
fail = 0;
for (const id of ids) {
try {
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${ok + fail + 1}/${ids.length}`;
const resp = await fetch("delete_record.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: id }),
});
const json = await resp.json();
if (json.success) {
ok++;
// rimuovi da gridData
const arr = data();
const idx = arr.findIndex(
(r) => String(r.iddatadb) === String(id),
);
if (idx !== -1) arr.splice(idx, 1);
selected.delete(id);
if (restrictedIds) restrictedIds.delete(id);
} else fail++;
} catch (e) {
fail++;
}
}
btn.innerHTML = original;
R().renderVisibleRows();
if (filtersActive) {
injectCheckboxes();
}
applyVisibility();
updateToolbar();
alert(`Eliminate: ${ok}${fail ? " — Falliti: " + fail : ""}`);
}
// ── Restringi / mostra tutti ──
function toggleRestrict() {
if (!restricted) {
if (!selected.size) return;
restrictedIds = new Set([...selected]);
restricted = true;
} else {
restricted = false;
restrictedIds = null;
}
applyVisibility();
updateToolbar();
}
// ── Costruzione UI toolbar (bottoni in cima) ──
function buildUI() {
const addBtn = document.getElementById("addRowBtn");
if (!addBtn) return;
// bottone Filters
const fBtn = document.createElement("button");
fBtn.type = "button";
fBtn.id = "filtersToggleBtn";
fBtn.className = "btn btn-outline-info btn-sm";
fBtn.style.flexShrink = "0";
fBtn.innerHTML = '<i class="fas fa-filter"></i> Filters';
addBtn.parentNode.insertBefore(fBtn, addBtn.nextSibling);
// barra azioni contestuali
const bar = document.createElement("span");
bar.id = "filterActionBar";
bar.style.cssText =
"display:none;align-items:center;gap:8px;flex-shrink:0;";
bar.innerHTML = `
<span id="filterPager" style="display:none;align-items:center;gap:6px;"></span>
<button type="button" id="filterClearBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times"></i> Pulisci filtri</button>
<span style="font-size:12px;color:#555;">Selezionate: <strong id="filterSelCount">0</strong></span>
<button type="button" id="filterRestrictBtn" class="btn btn-outline-primary btn-sm"><i class="fas fa-list-check"></i> Mostra selezionati</button>
<button type="button" id="filterClearSelBtn" class="btn btn-outline-secondary btn-sm"><i class="fas fa-square"></i> Deseleziona</button>
<button type="button" id="filterDeleteBtn" class="btn btn-outline-danger btn-sm"><i class="fas fa-trash"></i> Elimina selezionati</button>
`;
fBtn.parentNode.insertBefore(bar, fBtn.nextSibling);
fBtn.addEventListener("click", toggleFilters);
document
.getElementById("filterClearBtn")
.addEventListener("click", clearFilters);
document
.getElementById("filterDeleteBtn")
.addEventListener("click", batchDelete);
document
.getElementById("filterRestrictBtn")
.addEventListener("click", toggleShowSelected);
document
.getElementById("filterClearSelBtn")
.addEventListener("click", clearSelection);
}
// Svuota tutti i filtri di colonna (input + select) e torna alla pagina originale.
function clearFilters() {
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
document
.querySelectorAll("#gridFilterRow .filter-col-input")
.forEach((el) => {
el.value = "";
});
if (serverFiltered) restoreOriginal();
}
// ── Event delegation ──
function attachEvents() {
// checkbox click
document.addEventListener("change", function (e) {
if (e.target.classList.contains("filter-row-checkbox")) {
const rowEl = e.target.closest(".grid-row");
toggleRow(rowEl.dataset.id, e.target.checked);
}
});
// Seleziona / deseleziona tutti i FILTRATI
document.addEventListener("change", function (e) {
if (e.target.id !== "filterSelectAll") return;
const on = e.target.checked;
// Con filtro server-side: seleziona TUTTI i match (anche altre pagine).
if (serverFiltered) {
selectAllMatching(on);
return;
}
document
.querySelectorAll("#gridRowContainer .grid-row")
.forEach((rowEl) => {
if (rowEl.style.display === "none") return;
const id = parseInt(rowEl.dataset.id, 10);
if (!id) return;
if (on) selected.add(id);
else selected.delete(id);
rowEl.classList.toggle("row-selected", on);
const cb = rowEl.querySelector(".filter-row-checkbox");
if (cb) cb.checked = on;
});
updateToolbar();
});
// click su QUALSIASI punto della riga (tranne campi interattivi) = seleziona
document.addEventListener("click", function (e) {
if (!filtersActive) return;
const rowEl = e.target.closest("#gridRowContainer .grid-row");
if (!rowEl) return;
// ignora click su elementi interattivi
if (
e.target.closest(
"input, select, textarea, button, a, .select2-container, .action-btn, .propagate-btn, .add-part-btn",
)
) {
// ma se ho cliccato proprio la checkbox, lascia fare al change
return;
}
const cb = rowEl.querySelector(".filter-row-checkbox");
if (!cb) return;
cb.checked = !cb.checked;
toggleRow(rowEl.dataset.id, cb.checked);
});
// filtri colonna testo (debounce) → filtro server-side su tutto il template
let t = null;
document.addEventListener("input", function (e) {
if (!e.target.classList.contains("filter-col-input")) return;
if (e.target.tagName === "SELECT") return; // i select li gestisce 'change'
const key = e.target.dataset.colKey;
colFilters[key] = e.target.value;
clearTimeout(t);
t = setTimeout(applyServerFilter, 250);
});
// dropdown (es. Status): applica subito, senza debounce
document.addEventListener("change", function (e) {
if (!e.target.classList.contains("filter-col-input")) return;
if (e.target.tagName !== "SELECT") return;
colFilters[e.target.dataset.colKey] = e.target.value;
applyServerFilter();
});
}
// ── Ripristina stato filtri dopo un re-render (checkbox + restrizione) ──
let _restoring = false;
function restoreFilterState() {
if (!filtersActive || _restoring) return;
_restoring = true; // evita loop con l'observer mentre iniettiamo/nascondiamo
try {
injectCheckboxes();
applyVisibility(); // ri-applica filtri colonna E restringi selezione
} finally {
// rilascia dopo il tick così le nostre mutazioni non ci ri-triggerano subito
setTimeout(() => {
_restoring = false;
}, 0);
}
}
// ── Observer: qualunque cosa ricrei le righe (propagazione, save, ecc.)
// ripristina automaticamente checkbox + restrizione. Robusto perché
// non dipende da chi chiama renderVisibleRows. ──
function hookRerender() {
const rowC = document.getElementById("gridRowContainer");
if (!rowC) return;
const obs = new MutationObserver(() => {
if (!filtersActive || _restoring) return;
// debounce leggero: aspetta che il render finisca
clearTimeout(hookRerender._t);
hookRerender._t = setTimeout(restoreFilterState, 30);
});
obs.observe(rowC, { childList: true });
}
function init() {
if (!window.gridRenderer) {
console.error("[gridFilter] gridRenderer non trovato");
return;
}
buildUI();
attachEvents();
hookRerender();
hookGridSync();
}
// parte dopo che gridRenderer ha finito init
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () =>
setTimeout(init, 300),
);
} else {
setTimeout(init, 300);
}
})();
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -59,7 +59,8 @@ foreach ($allMappings as $mapping) {
}
}
if (!$mainFieldMapping) {
$mainFieldMapping = reset(array_filter($allMappings, fn($m) => !$m['is_manual']));
$filtered = array_filter($allMappings, fn($m) => !$m['is_manual']);
$mainFieldMapping = reset($filtered);
}
// Retrieve data from datadb
+379 -61
View File
@@ -10,70 +10,180 @@
<title>Template Buttons - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
/* Layout flessibile per gestire dimensioni diverse */
#templateButtons {
/* Main buttons container */
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
gap: 12px;
justify-content: flex-start;
/* Allinea a sinistra */
padding: 20px;
padding: 10px 0;
}
/* Definizione delle dimensioni */
/* Definizione delle dimensioni */
/* Button sizes */
.btn-small {
font-size: 12px;
padding: 6px 12px;
min-width: 100px;
min-height: 30px;
display: flex;
/* Aggiunto */
min-height: 36px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 8px;
text-align: center;
}
.btn-medium {
font-size: 16px;
padding: 10px 20px;
min-width: 130px;
min-height: 45px;
display: flex;
/* Aggiunto */
min-width: 140px;
min-height: 48px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 8px;
border-radius: 10px;
text-align: center;
}
.btn-large {
font-size: 20px;
padding: 14px 28px;
min-width: 180px;
min-height: 60px;
display: flex;
/* Aggiunto */
min-width: 190px;
min-height: 64px;
display: inline-flex;
justify-content: center;
/* Aggiunto */
align-items: center;
/* Aggiunto */
gap: 10px;
border-radius: 12px;
text-align: center;
}
/* Stile della barra di ricerca */
.template-icon {
font-size: 18px;
line-height: 1;
}
.btn-large .template-icon {
font-size: 22px;
}
.btn-small .template-icon {
font-size: 15px;
}
/* Search box */
#searchInput {
width: 100%;
padding: 10px;
padding: 10px 14px;
font-size: 16px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 18px;
border: 1px solid #d9d9d9;
border-radius: 8px;
outline: none;
}
#searchInput:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.15);
}
/* Tabs */
.custom-tabs {
border-bottom: 1px solid #e5e5e5;
margin-bottom: 20px;
gap: 6px;
}
.custom-tabs .nav-link {
border: none;
border-radius: 10px 10px 0 0;
color: #555;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
}
.custom-tabs .nav-link:hover {
background: #f8f9fa;
color: #0d6efd;
}
.custom-tabs .nav-link.active {
background: #0d6efd;
color: #fff;
}
.tab-pane {
min-height: 140px;
}
.empty-message {
color: #6c757d;
font-style: italic;
padding: 10px 0;
}
.loading-message {
color: #6c757d;
padding: 10px 0;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
width: 100%;
margin-top: 6px;
margin-bottom: 2px;
padding-bottom: 4px;
border-bottom: 1px solid #eee;
}
.section-title:first-child {
margin-top: 0;
}
.template-btn-wrapper {
position: relative;
display: inline-flex;
}
.favorite-star {
position: absolute;
top: -6px;
right: -6px;
width: 22px;
height: 22px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
font-size: 13px;
color: #ccc;
z-index: 2;
transition: color 0.15s, transform 0.15s;
}
.favorite-star:hover {
transform: scale(1.15);
}
.favorite-star.active {
color: #ffc107;
}
</style>
</head>
<body>
<?php include('include/popup.php'); ?>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
@@ -87,14 +197,54 @@
<h6 class="mb-0">Active Templates</h6>
</div>
<div class="card-body">
<!-- Barra di ricerca -->
<input type="text" id="searchInput" placeholder="Search template...">
<div id="templateButtons"></div>
<ul class="nav nav-tabs custom-tabs" id="templateTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="xls-tab" data-bs-toggle="tab" data-bs-target="#xls-pane" type="button" role="tab" aria-controls="xls-pane" aria-selected="true">
<i class="bx bx-spreadsheet template-icon"></i>
XLS
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api-pane" type="button" role="tab" aria-controls="api-pane" aria-selected="false">
<i class="bx bx-transfer-alt template-icon"></i>
JSON/API
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pdf-tab" data-bs-toggle="tab" data-bs-target="#pdf-pane" type="button" role="tab" aria-controls="pdf-pane" aria-selected="false">
<i class="bx bx-file-pdf template-icon"></i>
PDF
</button>
</li>
</ul>
<div class="tab-content" id="templateTabsContent">
<div class="tab-pane fade show active" id="xls-pane" role="tabpanel" aria-labelledby="xls-tab">
<div id="templateButtonsXLS" class="template-buttons">
<div class="loading-message">Loading XLS templates...</div>
</div>
</div>
<div class="tab-pane fade" id="api-pane" role="tabpanel" aria-labelledby="api-tab">
<div id="templateButtonsAPI" class="template-buttons">
<div class="loading-message">Loading API templates...</div>
</div>
</div>
<div class="tab-pane fade" id="pdf-pane" role="tabpanel" aria-labelledby="pdf-tab">
<div id="templateButtonsPDF" class="template-buttons">
<div class="loading-message">Loading PDF templates...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -104,46 +254,214 @@
<script>
document.addEventListener("DOMContentLoaded", function() {
const allTemplates = [];
const containers = {
XLS: document.getElementById("templateButtonsXLS"),
API: document.getElementById("templateButtonsAPI"),
PDF: document.getElementById("templateButtonsPDF")
};
function getSizeClass(buttonSize) {
if (buttonSize === "small") return "btn-small";
if (buttonSize === "large") return "btn-large";
return "btn-medium";
}
function getTemplateIcon(sourceType) {
switch ((sourceType || '').toUpperCase()) {
case 'XLS':
return 'bx bx-spreadsheet';
case 'API':
return 'bx bx-transfer-alt';
case 'PDF':
return 'bx bx-file-pdf';
default:
return 'bx bx-file';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function createButtonWrapper(template) {
const sizeClass = getSizeClass(template.button_size);
const sourceType = (template.source_type || '').toUpperCase();
const iconClass = getTemplateIcon(sourceType);
const isFavorite = !!parseInt(template.is_favorite);
const wrapper = document.createElement("div");
wrapper.className = "template-btn-wrapper";
const btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
btn.setAttribute("data-label", (template.button_label || '').toLowerCase());
btn.setAttribute("data-source-type", sourceType);
btn.innerHTML = `
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
const star = document.createElement("span");
star.className = "favorite-star" + (isFavorite ? " active" : "");
star.innerHTML = `<i class="bx ${isFavorite ? 'bxs-star' : 'bx-star'}"></i>`;
star.setAttribute("data-template-id", template.id);
star.title = isFavorite ? "Rimuovi dai preferiti" : "Aggiungi ai preferiti";
star.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
toggleFavorite(template.id);
});
wrapper.appendChild(btn);
wrapper.appendChild(star);
return wrapper;
}
function toggleFavorite(templateId) {
const template = allTemplates.find(t => String(t.id) === String(templateId));
if (!template) return;
const newState = !parseInt(template.is_favorite);
// Optimistic update
template.is_favorite = newState ? 1 : 0;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
fetch("toggle_template_favorite.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
template_id: templateId,
is_favorite: newState
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("Failed to toggle favorite:", data.message);
// Rollback in caso di errore
template.is_favorite = newState ? 0 : 1;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
}
})
.catch(error => {
console.error("Fetch error toggling favorite:", error);
template.is_favorite = newState ? 0 : 1;
renderTemplates(document.getElementById("searchInput").value.toLowerCase().trim());
});
}
function clearContainers() {
Object.values(containers).forEach(container => {
container.innerHTML = '';
});
}
function renderSection(container, title, templates) {
if (templates.length === 0) return;
const heading = document.createElement("div");
heading.className = "section-title";
heading.textContent = title;
container.appendChild(heading);
const group = document.createElement("div");
group.className = "template-buttons";
group.style.marginTop = "0";
group.style.paddingTop = "0";
templates.forEach(template => {
group.appendChild(createButtonWrapper(template));
});
container.appendChild(group);
}
function renderTemplates(searchValue = '') {
clearContainers();
const grouped = {
XLS: [],
API: [],
PDF: []
};
allTemplates.forEach(template => {
const sourceType = (template.source_type || '').toUpperCase();
const label = (template.button_label || '').toLowerCase();
if (searchValue && !label.includes(searchValue)) {
return;
}
if (grouped[sourceType]) {
grouped[sourceType].push(template);
}
});
Object.keys(grouped).forEach(type => {
const container = containers[type];
if (!container) return;
if (grouped[type].length === 0) {
container.innerHTML = `<div class="empty-message">No templates found in this section.</div>`;
return;
}
const favorites = grouped[type].filter(t => parseInt(t.is_favorite) === 1);
const others = grouped[type].filter(t => parseInt(t.is_favorite) !== 1);
renderSection(container, "★ Preferiti", favorites);
renderSection(container, "Altri", others);
});
}
fetch("load_active_templates.php")
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error("Error loading templates:", data.message);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Error loading templates.</div>`;
});
return;
}
let templateButtons = document.getElementById("templateButtons");
templateButtons.innerHTML = '';
data.data.forEach(template => {
let sizeClass = "btn-medium"; // Default
if (template.button_size === "small") sizeClass = "btn-small";
if (template.button_size === "large") sizeClass = "btn-large";
let btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color;
btn.style.color = template.button_text_color;
btn.textContent = template.button_label;
btn.setAttribute("data-label", template.button_label.toLowerCase()); // Attributo per ricerca
templateButtons.appendChild(btn);
});
})
.catch(error => console.error("Fetch error:", error));
// Funzione per la ricerca live
document.getElementById("searchInput").addEventListener("input", function() {
let searchValue = this.value.toLowerCase();
document.querySelectorAll("#templateButtons a").forEach(btn => {
let label = btn.getAttribute("data-label");
if (label.includes(searchValue)) {
btn.style.display = "inline-block";
} else {
btn.style.display = "none";
if (!Array.isArray(data.data)) {
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Invalid response format.</div>`;
});
return;
}
allTemplates.push(...data.data);
renderTemplates();
})
.catch(error => {
console.error("Fetch error:", error);
clearContainers();
Object.values(containers).forEach(container => {
container.innerHTML = `<div class="empty-message">Fetch error while loading templates.</div>`;
});
});
document.getElementById("searchInput").addEventListener("input", function() {
const searchValue = this.value.toLowerCase().trim();
renderTemplates(searchValue);
});
});
</script>
File diff suppressed because it is too large Load Diff
+99 -31
View File
@@ -20,10 +20,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['template_id']) || !i
$template_id = intval($_POST['template_id']);
$selected_rows = array_map('intval', $_POST['selected_rows']);
$columns = json_decode($_POST['columns'], true);
$rows = json_decode($_POST['rows'], true);
$excelrows = json_decode($_POST['excelrows'], true);
$newFilename = htmlspecialchars($_POST['filename']);
$columns = json_decode(urldecode($_POST['columns'] ?? '[]'), true);
$rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = $_POST['filename'];
$_SESSION['template_id'] = $template_id;
$_SESSION['selected_rows'] = $selected_rows;
@@ -39,6 +40,29 @@ error_log("Excelrows: " . json_encode($excelrows));
$user_id = $iduserlogin ?? 1;
/**
* Normalizza un nome colonna per il confronto header Excel <-> mapping.
* Risolve alla radice i mismatch causati da caratteri invisibili:
* - non-breaking space (U+00A0), narrow NBSP (U+202F), figure space (U+2007), ecc. -> spazio normale
* - tab, newline e altri whitespace -> spazio normale
* - spazi multipli consecutivi -> un solo spazio
* - trim ai bordi
* NB: usata SOLO per il matching, non altera i valori importati.
*/
function normalizeColName($s): string
{
$s = (string)$s;
// Converte i principali spazi Unicode "insidiosi" in spazio normale
$s = str_replace(
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
' ',
$s
);
// Qualsiasi sequenza di whitespace (spazi, tab, newline) -> singolo spazio
$s = preg_replace('/\s+/u', ' ', $s);
return trim((string)$s);
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
@@ -46,7 +70,7 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -104,7 +128,12 @@ foreach ($selected_rows as $rowIndex) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
// Match robusto: normalizza header e mapping (NBSP, spazi doppi, tab...)
// così un solo/doppio spazio o un non-breaking space non rompono il confronto.
$excelColumnIndex = array_search(
normalizeColName($excelColumn),
array_map('normalizeColName', $columns)
);
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
$fieldValue = $row[$excelColumnIndex];
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
@@ -125,7 +154,7 @@ foreach ($selected_rows as $rowIndex) {
case 'Testo':
case 'VARCHAR':
default:
$fieldValue = !empty($fieldValue) ? htmlspecialchars((string)$fieldValue) : ($mapping['manual_default'] ?? '');
$fieldValue = !empty($fieldValue) ? (string)$fieldValue : ($mapping['manual_default'] ?? '');
break;
}
} else {
@@ -134,6 +163,15 @@ foreach ($selected_rows as $rowIndex) {
$fieldValue = date('Y-m-d');
}
}
// Apply auto_value if field is still empty
if (($fieldValue === null || $fieldValue === '') && !empty($mapping['auto_value']) && $mapping['auto_value'] !== 'none') {
if ($mapping['auto_value'] === 'import_date') {
$fieldValue = date('Y-m-d');
} elseif ($mapping['auto_value'] === 'import_time') {
$fieldValue = date('H:i');
}
}
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
@@ -144,30 +182,60 @@ foreach ($selected_rows as $rowIndex) {
}
}
// ------------------------------------------------------------
// AUTO SET: schema field_id = 244 (Accettatore) con auth_users.lims_user_id
// Spostato dal vecchio flusso (import_edit.php) al flusso nuovo.
// Autocompila il custom field 244 con il lims_user_id dell'utente loggato.
// ------------------------------------------------------------
try {
// 1) Leggi il lims_user_id dell'utente loggato
$stmtUser = $pdo->prepare("SELECT lims_user_id FROM auth_users WHERE id = ? LIMIT 1");
$stmtUser->execute([(int)$user_id]);
$limsUserId = $stmtUser->fetchColumn();
// normalize
$limsUserId = ($limsUserId !== false && $limsUserId !== null && $limsUserId !== '') ? (string)$limsUserId : '';
if ($limsUserId !== '' && !empty($insertedIds)) {
// 2) Trova il mapping_id per field_id = 244 in questo template
$stmtMap = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ? AND field_id = 244 LIMIT 1");
$stmtMap->execute([(int)$template_id]);
$mappingId244 = (int)$stmtMap->fetchColumn();
if ($mappingId244 > 0) {
// 3) Upsert del valore in import_data_details per ogni riga inserita
$pdo->beginTransaction();
$updStmt = $pdo->prepare("UPDATE import_data_details SET field_value = ? WHERE id = ? AND mapping_id = ?");
$insStmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
foreach ($insertedIds as $iddatadb) {
$iddatadb = (int)$iddatadb;
if ($iddatadb <= 0) continue;
$updStmt->execute([$limsUserId, $iddatadb, $mappingId244]);
if ($updStmt->rowCount() === 0) {
$insStmt->execute([$iddatadb, $mappingId244, $limsUserId]);
}
}
$pdo->commit();
} else {
error_log("[AUTO FIELD 244] Nessun mapping field_id=244 per template $template_id");
}
} else {
error_log("[AUTO FIELD 244] limsUserId vuoto per user_id=$user_id o nessuna riga inserita");
}
} catch (Exception $e) {
// Non bloccare l'import se questo fallisce; logga soltanto
error_log("[AUTO FIELD 244] " . $e->getMessage());
if ($pdo->inTransaction()) $pdo->rollBack();
}
$_SESSION['inserted_ids'] = $insertedIds;
$params = [
'template_id' => $template_id,
'filename' => $newFilename,
];
?>
<form id="redirectForm" action="import_edit2.php" method="post">
<input type="hidden" name="template_id" value="<?= htmlspecialchars($template_id) ?>">
<input type="hidden" name="filename" value="<?= htmlspecialchars($newFilename) ?>">
<?php foreach ($selected_rows as $row): ?>
<input type="hidden" name="selected_rows[]" value="<?= htmlspecialchars($row) ?>">
<?php endforeach; ?>
<?php foreach ($insertedIds as $id): ?>
<input type="hidden" name="inserted_ids[]" value="<?= htmlspecialchars($id) ?>">
<?php endforeach; ?>
<input type="hidden" name="columns" value='<?= json_encode($columns) ?>'>
<input type="hidden" name="rows" value='<?= json_encode($rows) ?>'>
<input type="hidden" name="excelrows" value='<?= json_encode($excelrows) ?>'>
</form>
<script>
document.getElementById('redirectForm').submit();
</script>
<?php
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
exit;
?>
+232 -46
View File
@@ -40,6 +40,22 @@ error_log("Loaded template: " . print_r($template, true));
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
@@ -121,6 +137,16 @@ error_log("Loaded template: " . print_r($template, true));
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
@@ -133,16 +159,20 @@ error_log("Loaded template: " . print_r($template, true));
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="mb-3 text">
<a href="historical_trf.php?id=<?= $id ?>&status=i" class="btn btn-warning me-2">Imported (i)</a>
<a href="historical_trf.php?id=<?= $id ?>&status=P" class="btn btn-primary me-2">In Progress (P)</a>
<a href="historical_trf.php?id=<?= $id ?>&status=l" class="btn btn-success">To LIMS (l)</a>
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?></h6>
<small>Template ID: <?= $id ?>, Start Row: <?= $template['header_row'] ?>, Start Column: <?= $template['start_column'] ?></small>
<small>
Template ID: <?= $id ?>,
Sheet Number: <?= (int)($template['xls_sheet_index'] ?? 0) ?>,
Start Row: <?= $template['header_row'] ?>,
Start Column: <?= $template['start_column'] ?>
</small>
</div>
</div>
</div>
@@ -219,8 +249,9 @@ error_log("Loaded template: " . print_r($template, true));
const templateId = <?= $id ?>;
console.log('Template ID passed to formData:', templateId);
formData.append('template_id', templateId);
formData.append('header_row', <?= $template['header_row'] ?>);
formData.append('start_column', <?= $template['start_column'] ?>);
formData.append('header_row', <?= (int)$template['header_row'] ?>);
formData.append('start_column', <?= json_encode($template['start_column']) ?>);
formData.append('xls_sheet_index', <?= (int)($template['xls_sheet_index'] ?? 0) ?>);
fetch('process_import_xls2.php', {
method: 'POST',
@@ -303,57 +334,177 @@ error_log("Loaded template: " . print_r($template, true));
function showTable(data) {
console.log('Mostro tabella con dati:', data);
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value='${JSON.stringify(data.columns)}'>
<input type="hidden" name="rows" value='${JSON.stringify(data.rows)}'>
<input type="hidden" name="excelrows" value='${JSON.stringify(data.excel_data.map(row => row.excelrow))}'>
<input type="hidden" name="filename" value="${data.filename}">
<div class="search-container">
<input type="text" id="searchInput" class="form-control" placeholder="Cerca nelle righe...">
</div>
<div class="table-container">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Seleziona</th>
${data.columns.map(col => `<th>${col || 'Colonna senza nome'}<div class="resize-handle"></div></th>`).join('')}
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr>
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
${row.data.map(cell => `<td>${cell}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButton" disabled>Prosegui</button>
</form>
`;
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${data.template_id}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${data.filename}">
<!-- TOP BUTTON -->
<div class="d-flex justify-content-end mb-3">
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Seleziona</th>
${data.columns.map(col => {
const label = !col ? 'Colonna senza nome' : (col.match(/^__empty_\d+__$/) ? 'Colonna senza nome' : col);
return `<th>${label}<div class="resize-handle"></div></th>`;
}).join('')}
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr>
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${row.excelrow}"></td>
${row.data.map(cell => `<td>${cell}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
</div>
<!-- BOTTOM BUTTON -->
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
const proceedButton = document.getElementById('proceedButton');
const selectRowsForm = document.getElementById('selectRowsForm');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
proceedButton.disabled = !Array.from(checkboxes).some(cb => cb.checked);
const enabled = Array.from(checkboxes).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
const visibleRows = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked;
}
});
updateProceedButton();
});
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
console.log('Checkbox changed, checked:', this.checked, 'excelrow:', this.dataset.excelrow);
selectAllCheckbox.checked = Array.from(checkboxes).every(cb => cb.checked);
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
});
});
@@ -393,14 +544,49 @@ error_log("Loaded template: " . print_r($template, true));
}
});
const searchInput = document.getElementById('searchInput');
const rows = document.querySelectorAll('.table tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
// Stato filtri: key = colIndex, value = testo
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
const text = Array.from(row.cells).slice(1).map(cell => cell.textContent.toLowerCase()).join(' ');
row.style.display = text.includes(searchTerm) ? '' : 'none';
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
const visibleCheckboxes = Array.from(document.querySelectorAll('.table tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
}
filterInputs.forEach(input => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex; // string
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
<?php
if (!function_exists('gdbFixedDefaultValue')) {
/** Default di un fixed field (DATE 'today' → data odierna). */
function gdbFixedDefaultValue(array $f): string
{
$v = $f['default_value'] ?? '';
if (($f['data_type'] ?? '') === 'DATE' && $v === 'today') {
return date('Y-m-d');
}
return (string)$v;
}
}
if (!function_exists('buildGridConfig')) {
/**
* Config del template necessaria per costruire le righe: fixed fields (ordinati
* come in imported.php), main field mappings, alias map, default idclient.
*
* @return array{fixedFields:array, fixedAliasMap:array, mainFieldMappings:array, default_idclient:mixed}
*/
function buildGridConfig(PDO $pdo, int $templateId): array
{
// Mappa logica fixed_field_key → colonna reale su datadb (come imported.php)
$fixedAliasMap = [
'ClienteResponsabile' => 'cliente_responsabile_id',
'ClienteFornitore' => 'cliente_fornitore_id',
'ClienteAnalisi' => 'clienteAnalisi',
'ClienteFatturazione' => 'ClienteFatturazione',
'MoltiplicatorePrezzo' => 'moltiplicatore_prezzo_id',
'AnagraficaCertestObject' => 'anagrafica_certest_object_id',
'AnagraficaCertestService' => 'anagrafica_certest_service_id',
'ConsegnaRichiesta' => 'consegna_richiesta',
];
// Fixed fields visibili, ordinati come imported.php
$fixedStmt = $pdo->prepare("
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ? AND is_visible_import = 1
ORDER BY id
");
$fixedStmt->execute([$templateId]);
$fixedFieldsRaw = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
$desiredOrder = [
'ClienteResponsabile', 'ClienteFornitore', 'ClienteAnalisi', 'ClienteFatturazione',
'AnagraficaCertestObject', 'AnagraficaCertestService', 'MoltiplicatorePrezzo', 'ConsegnaRichiesta',
];
$excludeFromFixed = ['ClienteFornitore']; // reso come colonna a sé
$fixedFields = [];
$tempMap = [];
foreach ($fixedFieldsRaw as $f) {
if (in_array($f['fixed_field_key'], $excludeFromFixed, true)) continue;
$tempMap[$f['fixed_field_key']] = $f;
}
foreach ($desiredOrder as $key) {
if (isset($tempMap[$key])) {
$fixedFields[] = $tempMap[$key];
unset($tempMap[$key]);
}
}
foreach ($tempMap as $f) {
$fixedFields[] = $f;
}
// Main field mappings (max 2), come imported.php
$mapStmt = $pdo->prepare("
SELECT id, field_label, data_type, is_required, field_id, field_order,
main_field, is_visible_import, manual_default
FROM template_mapping
WHERE template_id = ?
ORDER BY field_order ASC, id ASC
");
$mapStmt->execute([$templateId]);
$allMappings = $mapStmt->fetchAll(PDO::FETCH_ASSOC);
$mainFieldMappings = [];
foreach ($allMappings as $mapping) {
if ((string)$mapping['main_field'] === '1' && (int)$mapping['is_visible_import'] === 1) {
$mainFieldMappings[] = $mapping;
}
if (count($mainFieldMappings) >= 2) break;
}
// Default idclient dal template
$tplStmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$tplStmt->execute([$templateId]);
$default_idclient = $tplStmt->fetchColumn();
$default_idclient = $default_idclient !== false ? $default_idclient : null;
return [
'fixedFields' => $fixedFields,
'fixedAliasMap' => $fixedAliasMap,
'mainFieldMappings' => $mainFieldMappings,
'default_idclient' => $default_idclient,
];
}
}
if (!function_exists('buildGridRows')) {
/**
* Costruisce le righe gridData per la lista di iddatadb data, nell'ordine passato.
*
* @param int[] $iddatadbList
* @param array $config output di buildGridConfig()
* @return array righe nella stessa forma di imported.php $gridDataArray
*/
function buildGridRows(PDO $pdo, array $iddatadbList, array $config): array
{
$iddatadbList = array_values(array_filter(array_map('intval', $iddatadbList), fn($v) => $v > 0));
if (empty($iddatadbList)) {
return [];
}
$fixedFields = $config['fixedFields'];
$fixedAliasMap = $config['fixedAliasMap'];
$mainFieldMappings = $config['mainFieldMappings'];
$default_idclient = $config['default_idclient'];
$ph = implode(',', array_fill(0, count($iddatadbList), '?'));
// Righe datadb + user_name
$stmt = $pdo->prepare("
SELECT d.*, CONCAT(u.first_name, ' ', u.last_name) AS user_name
FROM datadb d
LEFT JOIN auth_users u ON d.user_id = u.id
WHERE d.iddatadb IN ($ph)
");
$stmt->execute($iddatadbList);
$byId = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$byId[(int)$r['iddatadb']] = $r;
}
// Dettagli (import_data_details)
$detStmt = $pdo->prepare("
SELECT d.id AS datadb_id, d.mapping_id, d.field_value,
m.field_id, m.field_label, m.data_type, m.is_required, m.manual_default
FROM import_data_details d
JOIN template_mapping m ON d.mapping_id = m.id
WHERE d.id IN ($ph)
");
$detStmt->execute($iddatadbList);
$detailsByRow = [];
foreach ($detStmt->fetchAll(PDO::FETCH_ASSOC) as $d) {
$detailsByRow[(int)$d['datadb_id']][] = $d;
}
// Costruzione righe nell'ordine della lista passata
$rows = [];
foreach ($iddatadbList as $id) {
$row = $byId[$id] ?? null;
if ($row === null) continue;
$rowObj = [
'iddatadb' => (int)$row['iddatadb'],
'status' => $row['status'] ?? 'i',
'idclient' => $row['idclient'] ?? $default_idclient,
'cliente_fornitore_id' => $row['cliente_fornitore_id'] ?? null,
'tested_component' => $row['tested_component'] ?? '',
'commessaweb' => $row['commessaweb'] ?? null,
'user_name' => $row['user_name'] ?? '',
'importreferencecode' => $row['importreferencecode'] ?? '',
'filename_import' => $row['filename_import'] ?? '',
'importdate' => $row['importdate'] ?? '',
];
// Fixed fields
$rowObj['fixedFields'] = [];
foreach ($fixedFields as $f) {
$key = $f['fixed_field_key'];
$dbCol = $fixedAliasMap[$key] ?? $key;
$val = $row[$dbCol] ?? '';
if ($val === '' || $val === null) {
$val = gdbFixedDefaultValue($f);
}
$rowObj['fixedFields'][$key] = (string)$val;
}
// Details
$rowObj['details'] = [];
$rowDetails = $detailsByRow[$id] ?? [];
foreach ($rowDetails as $d) {
$rowObj['details'][(string)$d['mapping_id']] = $d['field_value'] ?? '';
}
// Main field values
foreach ($mainFieldMappings as $mainMapping) {
$found = null;
foreach ($rowDetails as $d) {
if ($d['mapping_id'] == $mainMapping['id']) {
$found = $d;
break;
}
}
$rowObj['details'][(string)$mainMapping['id']] =
($found['field_value'] ?? null) ?? ($mainMapping['manual_default'] ?? '');
}
if (!empty($mainFieldMappings)) {
$firstMain = $mainFieldMappings[0];
$rowObj['mainFieldValue'] = $rowObj['details'][(string)$firstMain['id']] ?? '';
}
$rowObj['_dirty'] = false;
$rows[] = $rowObj;
}
return $rows;
}
}
+8 -10
View File
@@ -5,11 +5,14 @@ $db = DBHandlerSelect::getInstance()->getConnection();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL | E_STRICT);
error_reporting(E_ALL);
// This should be equal to: PATH_TO_VANGUARD_FOLDER/extra/auth.php
include('../../extra/auth.php');
//require_once __DIR__ . '/extra/auth.php';
// Laravel bootstrap (loaded by auth.php) forces UTC via config/app.php — re-apply our TZ
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// Here we just check if user is not
// logged in, and in that case we redirect
// the user to vanguard login page.
@@ -26,6 +29,8 @@ $nameuser = $user->present()->first_name;
$surnameuser = $user->present()->last_name;
$emailuser = $user->present()->email;
$avatar = $user->present()->avatar;
$lims_user_id = $user->lims_user_id ?? '';
$lims_global_user_id = $user->lims_global_user_id ?? '';
$kindofrole = $user->present()->role_id;
@@ -35,8 +40,6 @@ $kindofrole = $user->present()->role_id;
//$iduserlogin="1";
//$nameuser="Claudio";
//$emailuser="info@claudiosironi.com";
?>
<?php
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
@@ -49,13 +52,8 @@ $_SESSION["emailuser"] = $emailuser;
$_SESSION["photouser"] = $avatar;
$photouser = $_SESSION["photouser"];
$photousername = basename($avatar);
?>
<?php //include files
//include files
require_once(__DIR__ . '/../../languages/en/general.php');
//include("generalsettings.php");
?>
//include("generalsettings.php");
+107 -46
View File
@@ -6,102 +6,163 @@
<div>
<h4 class="logo-text"><?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></h4>
</div>
<div class="toggle-icon ms-auto"><i class='bx bx-arrow-back'></i>
<div class="toggle-icon ms-auto">
<i class='bx bx-arrow-back'></i>
</div>
</div>
<!--navigation-->
<ul class="metismenu" id="menu">
<!-- user, admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('User')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
$currentUser = Auth::user();
$isAdmin = $currentUser->hasRole('Admin');
$isSuperUser = $currentUser->hasRole('SuperUser');
$isUser = $currentUser->hasRole('User');
$canAccessMainMenu = $isAdmin || $isSuperUser || $isUser;
$canAccessUserAdmin = $isAdmin || $isSuperUser;
$canAccessTemplates = $isAdmin || $isSuperUser;
?>
<!-- user, admin, superuser menu -->
<?php if ($canAccessMainMenu) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class='bx bx-home-alt'></i>
<div class="parent-icon">
<i class='bx bx-home-alt'></i>
</div>
<div class="menu-title">Dashboard</div>
</a>
<ul>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
</li> -->
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
<li>
<a href="import_dashboard.php">
<i class='bx bx-radio-circle'></i>XLS Import
</a>
</li>
</ul>
</li>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li> <a href="templates_dashboard.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<li> <a href="insert_template_xls.php"><i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
</ul>
</li>
<?php if ($canAccessTemplates) : ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Templates</div>
</a>
<ul>
<li>
<a href="templates_dashboard.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($dashtemplate, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<li>
<a href="insert_template_xls.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($insertnewtemplatexls, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</ul>
</li>
<?php endif; ?>
<li>
<a href="javascript:;" class="has-arrow">
<div class="parent-icon"><i class="bx bx-category"></i>
<div class="parent-icon">
<i class="bx bx-category"></i>
</div>
<div class="menu-title">Other Functions</div>
</a>
<ul>
<li> <a href="quotations.php"><i class='bx bx-radio-circle'></i><?php echo $quotationstitle; ?></a>
<li>
<a href="quotations.php">
<i class='bx bx-radio-circle'></i><?= htmlspecialchars($quotationstitle, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
</ul>
</li>
<?php if ($canAccessUserAdmin) : ?>
<li class="menu-label">Administration</li>
<li>
<a href="user-admin.php">
<div class="parent-icon">
<i class="bx bx-user-plus"></i>
</div>
<div class="menu-title">User Admin</div>
</a>
</li>
<li>
<a href="stats_export_lims.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Stats</div>
</a>
</li>
<li>
<a href="warm_cache_run.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Warm Cache</div>
</a>
</li>
<?php endif; ?>
<li class="menu-label">Others</li>
<li>
<a href="https://helpdesk.cesoft.io" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
<div class="parent-icon">
<i class="bx bx-support"></i>
</div>
<div class="menu-title">Support</div>
</a>
</li>
<?php
endif; ?>
<!-- admin, superuser menù -->
<?php if ((Auth::user()->hasRole('Admin')) || (Auth::user()->hasRole('Superuser'))) : ?>
<?php
endif; ?>
<!-- admin menù -->
<?php if (Auth::user()->hasRole('Admin')) : ?>
<?php endif; ?>
<!-- admin only menu -->
<?php if ($isAdmin) : ?>
<li class="menu-label">Admin Menù</li>
<li>
<a href="../" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
<div class="parent-icon">
<i class="bx bx-support"></i>
</div>
<div class="menu-title">User Management</div>
</a>
</li>
<!-- <li>
<!--
<li>
<a href="template/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-support"></i>
<div class="parent-icon">
<i class="bx bx-support"></i>
</div>
<div class="menu-title">Template</div>
<div class="menu-title">Template</div>
</a>
</li>
<li>
<a href="https://codervent.com/rocker/documentation/index.html" target="_blank">
<div class="parent-icon"><i class="bx bx-folder"></i>
<div class="parent-icon">
<i class="bx bx-folder"></i>
</div>
<div class="menu-title">Documentation</div>
</a>
</li> -->
<?php
endif; ?>
</li>
-->
<?php endif; ?>
</ul>
<!--end navigation-->
</div>
+55
View File
@@ -0,0 +1,55 @@
<?php
$popupEnabled = (($_ENV['POPUP_ENABLED'] ?? getenv('POPUP_ENABLED')) === 'true');
if ($popupEnabled):
?>
<div id="app-popup-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;z-index:99999;padding:20px;">
<div style="background:#fff;max-width:640px;width:100%;max-height:90vh;overflow-y:auto;padding:40px;border-radius:14px;position:relative;box-shadow:0 20px 60px rgba(0,0,0,.4);text-align:center;">
<div style="font-size:46px;line-height:1;margin-bottom:10px;">🚀</div>
<h2 style="margin:0 0 8px;font-size:26px;color:#111;">Nuovo sito in produzione</h2>
<p style="font-size:16px;color:#444;margin:0 0 20px;">Il portale si è spostato su un nuovo indirizzo.</p>
<div style="background:#f1f5ff;border:1px solid #c9d8ff;border-radius:10px;padding:16px;margin-bottom:24px;">
<p style="margin:0 0 4px;font-size:13px;color:#667;text-transform:uppercase;letter-spacing:.5px;">Nuovo indirizzo</p>
<p style="margin:0;font-size:19px;font-weight:700;color:#1d4ed8;word-break:break-all;">https://trfsmart-bv.cesoft.io/</p>
</div>
<a href="https://trfsmart-bv.cesoft.io/"
style="display:inline-block;padding:16px 36px;background:#2563eb;color:#fff;text-decoration:none;border-radius:8px;font-size:18px;font-weight:600;box-shadow:0 4px 14px rgba(37,99,235,.35);">
Clicca qui per accedere al nuovo sito
</a>
<p style="font-size:13px;color:#888;margin:16px 0 0;">Ti consigliamo di salvare il nuovo indirizzo nei preferiti.</p>
<hr style="border:none;border-top:1px solid #eee;margin:28px 0 20px;">
<div style="background:#fff8e6;border:1px solid #ffe08a;border-radius:10px;padding:16px 18px;text-align:left;margin-bottom:20px;">
<p style="margin:0;font-size:14px;color:#8a6d00;">
<strong>⚠️ Attenzione:</strong> chiudi questo messaggio <u>solo</u> se riscontri problemi
con il nuovo sito. Continuando su questo portale userai la versione precedente.
</p>
</div>
<button onclick="chiudiPopupMigrazione()"
style="padding:11px 22px;background:#f0f0f0;color:#555;border:1px solid #ddd;border-radius:8px;cursor:pointer;font-size:14px;">
Ho problemi col nuovo sito, resto su questo portale
</button>
</div>
</div>
<script>
function chiudiPopupMigrazione() {
var conferma = confirm(
"Sei sicuro di voler restare sul portale precedente?\n\n" +
"Il nuovo sito è quello ufficiale in produzione. " +
"Chiudi questo avviso solo se hai riscontrato un problema reale sul nuovo sito."
);
if (conferma) {
document.getElementById('app-popup-overlay').remove();
}
}
</script>
<?php endif; ?>
+24 -4
View File
@@ -1,8 +1,22 @@
<header>
<div class="topbar d-flex align-items-center">
<nav class="navbar navbar-expand gap-3">
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i>
<?php
// Controllo ambiente TEST
$testEnv = strtolower($_ENV['TEST_ENVIRONMENT'] ?? getenv('TEST_ENVIRONMENT') ?? 'true');
if ($testEnv === 'true'):
?>
<div class="alert alert-danger border-0 rounded-0 mb-0 py-2 text-center fw-bold fs-5 w-100 shadow-sm"
style="margin: 0 -15px 0 -15px;">
<i class='bx bx-test-tube me-2'></i>
AMBIENTE DI TEST NON È PRODUZIONE
</div>
<?php endif; ?>
<nav class="navbar navbar-expand gap-3 w-100">
<!-- Tutto il resto del tuo codice della topbar rimane identico -->
<div class="mobile-toggle-menu"><i class='bx bx-menu'></i></div>
<div class="search-bar d-lg-block d-none" data-bs-toggle="modal" data-bs-target="#SearchModal">
<!-- <a href="avascript:;" class="btn d-flex align-items-center"><i class='bx bx-search'></i>Search</a> -->
@@ -94,8 +108,14 @@
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item d-flex align-items-center" href="user-profile.php"><i class="bx bx-user fs-5"></i><span>Profile</span></a>
</li>
<li><a class="dropdown-item d-flex align-items-center" href="settings.php"><i class="bx bx-cog fs-5"></i><span>Settings</span></a>
</li>
<?php if ($user->hasRole('Admin') || $user->hasRole('SuperUser')): ?>
<li>
<a class="dropdown-item d-flex align-items-center" href="user-admin.php">
<i class="bx bx-user-plus fs-5"></i>
<span>User Admin</span>
</a>
</li>
<?php endif; ?>
<li>
<div class="dropdown-divider mb-0"></div>
</li>
+225 -16
View File
@@ -1,11 +1,22 @@
<?php include('include/headscript.php');
// Recupera tutte le routine dal database
// Retrieve all routines from database
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM routine");
$stmt->execute();
$routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Retrieve active API/JSON configurations
$stmt = $pdo->prepare("
SELECT id, name, provider_code, api_type, php_class_name
FROM api_configurations
WHERE is_active = 1
ORDER BY name ASC
");
$stmt->execute();
$apiConfigurations = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="en">
@@ -18,26 +29,30 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<?php include('cssinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<title>Insert XLS Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<title>Insert Template <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Insert new XLS Template</h5>
<h5 class="mb-0">Insert New Template</h5>
</div>
<div class="card-body">
<p class="mb-2">Fill the following form in order to create a new import XLS template</p>
<p class="mb-2">Fill the following form in order to create a new import template</p>
<p class="mb-2">Mandatory Fields</p>
<ul class="mb-0">
<li>Template Name</li>
<li>Row Header and Column Header: where the title of the excel starts</li>
<li>Schema and client</li>
<li>Source Type</li>
<li>Schema and Client</li>
<li>Row Header, Column Header and Sheet Number only for XLS templates</li>
<li>API / JSON Configuration only for API / JSON templates</li>
</ul>
</div>
</div>
@@ -50,22 +65,86 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
<div class="card-body">
<div class="col-12">
<form id="insertTemplateForm" method="POST">
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($templatename, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" class="form-control" required>
<label class="form-label">Source Type *</label>
<select name="source_type" id="sourceType" class="form-control" required>
<option value="XLS" selected>XLS</option>
<option value="API">API / JSON</option>
<option value="PDF">PDF</option>
</select>
<small class="text-muted">Choose the source used by this template</small>
</div>
<div class="mb-3">
<div class="mb-3" id="headerRowWrapper">
<label class="form-label"><?= htmlspecialchars($rowheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="number" name="header_row" id="headerRow" class="form-control" value="1" required>
</div>
<div class="mb-3" id="startColumnWrapper">
<label class="form-label"><?= htmlspecialchars($columnheader, ENT_QUOTES, 'UTF-8'); ?> *</label>
<input type="text" name="start_column" class="form-control" required>
<input type="text" name="start_column" id="startColumn" class="form-control" value="A" required>
</div>
<div class="mb-3" id="xlsSheetNumberWrapper">
<label class="form-label">XLS Sheet Number</label>
<input
type="number"
name="xls_sheet_index"
id="xlsSheetIndex"
class="form-control"
min="0"
value="0">
<small class="text-muted">
Use 0 for the first sheet, 1 for the second sheet, 2 for the third sheet, and so on.
</small>
</div>
<div class="mb-3" id="apiConfigWrapper" style="display: none;">
<label class="form-label">API / JSON Configuration *</label>
<select name="api_config_id" id="apiConfigSelect" class="form-control">
<option value="">Select an API configuration...</option>
<?php foreach ($apiConfigurations as $apiConfig): ?>
<?php
$apiLabelParts = [];
if (!empty($apiConfig['name'])) {
$apiLabelParts[] = $apiConfig['name'];
}
if (!empty($apiConfig['provider_code'])) {
$apiLabelParts[] = '[' . $apiConfig['provider_code'] . ']';
}
if (!empty($apiConfig['api_type'])) {
$apiLabelParts[] = '(' . $apiConfig['api_type'] . ')';
}
if (!empty($apiConfig['php_class_name'])) {
$apiLabelParts[] = '- ' . $apiConfig['php_class_name'];
}
$apiLabel = implode(' ', $apiLabelParts);
?>
<option value="<?php echo (int)$apiConfig['id']; ?>">
<?php echo htmlspecialchars($apiLabel, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
<small class="text-muted">
Select the API/JSON configuration linked to this template.
</small>
</div>
<div class="mb-3">
@@ -86,12 +165,12 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="mb-3">
<label class="form-label">Button Background Color</label>
<input type="color" name="button_bg_color" class="form-control" value="#007bff">
<input type="color" name="button_bg_color" class="form-control form-control-color" value="#007bff">
</div>
<div class="mb-3">
<label class="form-label">Button Text Color</label>
<input type="color" name="button_text_color" class="form-control" value="#ffffff">
<input type="color" name="button_text_color" class="form-control form-control-color" value="#ffffff">
</div>
<div class="mb-3">
@@ -125,6 +204,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</option>
<?php endforeach; ?>
</select>
<div id="routineDetails" class="mt-2" style="display: none;">
<h6>Routine Details</h6>
<p><strong>Name:</strong> <span id="routineName"></span></p>
@@ -142,8 +222,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
@@ -167,6 +249,18 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const routineAction2 = document.getElementById("routineAction2");
const routineAction3 = document.getElementById("routineAction3");
const sourceType = document.getElementById("sourceType");
const headerRowWrapper = document.getElementById("headerRowWrapper");
const startColumnWrapper = document.getElementById("startColumnWrapper");
const xlsSheetNumberWrapper = document.getElementById("xlsSheetNumberWrapper");
const apiConfigWrapper = document.getElementById("apiConfigWrapper");
const headerRow = document.getElementById("headerRow");
const startColumn = document.getElementById("startColumn");
const xlsSheetIndex = document.getElementById("xlsSheetIndex");
const apiConfigSelect = document.getElementById("apiConfigSelect");
if (!form || !clientLoadingStatus || !schemaLoadingStatus || !routineSelect || !routineDetails) {
alert("Errore: Uno o più elementi della pagina non sono stati trovati. Contatta l'amministratore.");
return;
@@ -187,30 +281,101 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
allowClear: true
});
$('#apiConfigSelect').select2({
placeholder: "Select an API configuration...",
allowClear: true
});
function updateSourceFields() {
const selectedSource = sourceType.value;
const isXls = selectedSource === 'XLS';
const isApiJson = selectedSource === 'API';
if (isXls) {
headerRowWrapper.style.display = 'block';
startColumnWrapper.style.display = 'block';
xlsSheetNumberWrapper.style.display = 'block';
headerRow.required = true;
startColumn.required = true;
xlsSheetIndex.required = true;
headerRow.disabled = false;
startColumn.disabled = false;
xlsSheetIndex.disabled = false;
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
} else {
headerRowWrapper.style.display = 'none';
startColumnWrapper.style.display = 'none';
xlsSheetNumberWrapper.style.display = 'none';
headerRow.required = false;
startColumn.required = false;
xlsSheetIndex.required = false;
headerRow.disabled = true;
startColumn.disabled = true;
xlsSheetIndex.disabled = true;
if (isApiJson) {
apiConfigWrapper.style.display = 'block';
apiConfigSelect.required = true;
apiConfigSelect.disabled = false;
} else {
apiConfigWrapper.style.display = 'none';
apiConfigSelect.required = false;
apiConfigSelect.disabled = true;
$('#apiConfigSelect').val(null).trigger('change');
}
}
}
sourceType.addEventListener('change', updateSourceFields);
updateSourceFields();
async function loadClients() {
try {
clientLoadingStatus.style.display = 'inline';
clientLoadingStatus.textContent = 'Recupero clienti in corso...';
const response = await fetch("get_clienti.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
const select = document.getElementById("clientSelect");
select.innerHTML = '<option value="">Select a client...</option>';
data.value.forEach(client => {
const nome = client.Nominativo || "Nome non disponibile";
const id = client.IdCliente || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
const codiceCliente = (client.CodiceCliente ?? client.codiceCliente ?? "").toString().trim();
const suffix = (codiceCliente.split("_")[1] || "").trim();
const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--");
const option = new Option(`${nome.trim()} - ${shortCode} (ID: ${id})`, id);
select.add(option);
});
$(select).trigger('change');
clientLoadingStatus.textContent = "Clienti caricati.";
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare i clienti: " + error.message,
@@ -226,26 +391,43 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
try {
schemaLoadingStatus.style.display = 'inline';
schemaLoadingStatus.textContent = 'Caricamento schemi in corso...';
const response = await fetch("get_schemi.php", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || `Errore HTTP: ${response.status}`);
if (!response.ok) {
throw new Error(data.error || `Errore HTTP: ${response.status}`);
}
const select = document.getElementById("schemaSelect");
select.innerHTML = '<option value="">Select a schema...</option>';
data.value.forEach(schema => {
const sortedSchemas = [...data.value].sort((a, b) => {
const nomeA = (a.Nome || "").trim().toLowerCase();
const nomeB = (b.Nome || "").trim().toLowerCase();
return nomeA.localeCompare(nomeB, 'it', {
sensitivity: 'base'
});
});
sortedSchemas.forEach(schema => {
const nome = schema.Nome || "Nome non disponibile";
const id = schema.IdSchemaCustomFields || "ID non disponibile";
const option = new Option(`${nome.trim()} (ID: ${id})`, id);
select.add(option);
});
$(select).trigger('change');
schemaLoadingStatus.textContent = "Schemi caricati.";
} catch (error) {
schemaLoadingStatus.textContent = "Errore nel caricamento.";
Swal.fire({
title: "Errore!",
text: "Impossibile caricare gli schemi: " + error.message,
@@ -270,6 +452,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
}
}
loadData();
const routines = <?php echo json_encode($routines); ?>;
@@ -277,8 +460,10 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
function updateRoutineDetails() {
const selectedId = routineSelect.value;
routineDetails.style.display = selectedId ? 'block' : 'none';
if (selectedId) {
const routine = routines.find(r => r.idroutine == selectedId);
if (routine) {
routineName.textContent = routine.name || 'N/A';
routineDescription.textContent = routine.description || 'N/A';
@@ -300,6 +485,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
routineAction3.textContent = '';
}
}
routineSelect.addEventListener('change', updateRoutineDetails);
updateRoutineDetails();
@@ -308,6 +494,28 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
let formData = new FormData(this);
const selectedSource = sourceType.value;
if (selectedSource === 'XLS' && xlsSheetIndex.value === '') {
Swal.fire({
title: "Errore!",
text: "Inserisci il numero del foglio XLS.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
if (selectedSource === 'API' && !apiConfigSelect.value) {
Swal.fire({
title: "Errore!",
text: "Seleziona una configurazione API / JSON.",
icon: "error",
confirmButtonText: "OK"
});
return;
}
const clientSelect = document.getElementById("clientSelect");
const clientId = clientSelect.value;
const selectedClientOption = clientSelect.options[clientSelect.selectedIndex];
@@ -350,6 +558,7 @@ $routines = $stmt->fetchAll(PDO::FETCH_ASSOC);
const nameMatch = optionText.match(/^(.+?)(?:\s*\(ID:\s*\d+\))?$/);
schemaName = nameMatch ? nameMatch[1].trim() : optionText;
}
formData.append("idschema", schemaId);
formData.append("schemaname", schemaName);
+20 -2
View File
@@ -1,5 +1,6 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
require_once 'class/db-functions.php';
$response = ["success" => false, "data" => [], "message" => ""];
@@ -12,8 +13,25 @@ try {
throw new Exception('Database connection failed.');
}
// Recupera solo i template attivi
$stmt = $pdo->query("SELECT id, button_label, button_bg_color, button_text_color, button_size FROM excel_templates WHERE status = 'active'");
$userId = $iduserlogin ?? 0;
// Recupera i template attivi, con flag is_favorite per l'utente corrente
$stmt = $pdo->prepare("
SELECT
et.id,
et.button_label,
et.button_size,
et.button_bg_color,
et.button_text_color,
et.source_type,
CASE WHEN uf.id IS NOT NULL THEN 1 ELSE 0 END AS is_favorite
FROM excel_templates et
LEFT JOIN user_template_favorites uf
ON uf.template_id = et.id AND uf.user_id = :user_id
WHERE et.status = 'active'
ORDER BY et.button_label ASC
");
$stmt->execute(['user_id' => $userId]);
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response["success"] = true;
+49 -3
View File
@@ -14,11 +14,57 @@ if (!$iddatadb) {
}
try {
$stmt = $pdo->prepare("SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC");
$stmt->execute([':iddatadb' => $iddatadb]);
// 1) prendo templateid da datadb
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = :iddatadb LIMIT 1");
$stmtTpl->execute([':iddatadb' => $iddatadb]);
$templateid = $stmtTpl->fetchColumn();
// 2) prendo (max 1) field_id visibile in parts
$extraFieldId = null;
if ($templateid) {
$stmtEF = $pdo->prepare("SELECT field_id FROM template_mapping WHERE template_id = :templateid AND is_visible_parts = 1 ORDER BY id ASC LIMIT 1");
$stmtEF->execute([':templateid' => $templateid]);
$extraFieldId = $stmtEF->fetchColumn();
if ($extraFieldId !== false) $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
}
// 3) carico parts + join su tabella figlia
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.mix, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
WHERE p.iddatadb = :iddatadb
ORDER BY p.part_number ASC
");
$stmt->execute([
':iddatadb' => $iddatadb,
':extraFieldId' => $extraFieldId
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
");
$stmt->execute([':iddatadb' => $iddatadb]);
}
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'parts' => $parts]);
echo json_encode([
'success' => true,
'parts' => $parts,
'extra_field_id' => $extraFieldId,
'debug_sql' => ($extraFieldId ? 'WITH_CF_JOIN' : 'NO_CF')
]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
}
+32
View File
@@ -0,0 +1,32 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
// Recupera l'ID dell'utente loggato
$user_id = $iduserlogin ?? 1;
if (!$user_id) {
echo json_encode(['success' => false, 'message' => "ID dell'utente autenticato mancante"]);
exit;
}
try {
$stmt = $pdo->prepare(
"SELECT DISTINCT q.*
FROM quotations q
INNER JOIN identification_parts ip
ON ip.idquotations = q.id
AND ip.iddatadb IS NULL
WHERE q.iduser = :iduser"
);
$stmt->execute([':iduser' => $user_id]);
$quotations = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'quotations' => $quotations]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento delle quotations: ' . $e->getMessage()]);
}
@@ -0,0 +1,60 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte AAA",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734582,
"CodiceCampione": "10978",
"CodiceCampioneWeb": "10978",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:02.1595894+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 563528,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "PARE BBB",
"ConsegnaRichiesta": "2026-01-30"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 734583,
"CodiceCampione": "10979",
"CodiceCampioneWeb": "10979",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-11T14:10:03.9972635+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-30T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "PARE BBB",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.76+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,44 @@
Photos for CommessaWeb 563528 (iddatadb=1259):
Total photos found: 2, campioni: 2
=== Campione 734582 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779561,
"FileName": "1259-20260311130930-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:06.7051549+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(734582)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1779562,
"FileName": "1259-20260311130930-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-11T14:10:08.5442313+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 563528,
"CodiceCommessa": "26C0029",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-11T14:10:00.897+01:00",
"CodiceCommessaWeb": "26C0029",
"DataInviatoWeb": "2026-03-11T14:10:11.7602299+01:00",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(563528)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23254249,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254250,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254251,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254252,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254253,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254254,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254260,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254261,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254262,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254264,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254265,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254266,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254267,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254268,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254228,
"Valore": "13526"
},
{
"IdCommesseCustomFields": 23254229,
"Valore": "20262"
},
{
"IdCommesseCustomFields": 23254230,
"Valore": "ART. PEACH"
},
{
"IdCommesseCustomFields": 23254269,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254270,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254271,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254272,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254273,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254274,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254275,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254276,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254277,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254278,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254279,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254280,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254281,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254231,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23254232,
"Valore": "BLACK"
},
{
"IdCommesseCustomFields": 23254233,
"Valore": "PE007J"
},
{
"IdCommesseCustomFields": 23254234,
"Valore": "262"
},
{
"IdCommesseCustomFields": 23254235,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254236,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254237,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254238,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254239,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254240,
"Valore": "Oggi"
},
{
"IdCommesseCustomFields": 23254241,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254242,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254243,
"Valore": "236"
},
{
"IdCommesseCustomFields": 23254244,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254245,
"Valore": "BBB"
},
{
"IdCommesseCustomFields": 23254246,
"Valore": "solocla"
},
{
"IdCommesseCustomFields": 23254247,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254248,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254255,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254256,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23254257,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254258,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254259,
"Valore": ""
},
{
"IdCommesseCustomFields": 23254263,
"Valore": ""
}
]
}'
RESPONSE:
null
@@ -0,0 +1,60 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564663,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte 1",
"ConsegnaRichiesta": "2026-03-31"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736244,
"CodiceCampione": "11004",
"CodiceCampioneWeb": "11004",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-17T14:52:30.3143366+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-03-31T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte 1",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564663,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "Parte 2",
"ConsegnaRichiesta": "2026-03-31"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736245,
"CodiceCampione": "11005",
"CodiceCampioneWeb": "11005",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-17T14:52:31.9631757+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-03-31T00:00:00+02:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "Parte 2",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"IdUtente": 285
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564663,
"CodiceCommessa": "26C0059",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Elaborata",
"DataCreazioneWeb": "2026-03-17T14:52:29.347+01:00",
"CodiceCommessaWeb": "26C0059",
"DataInviatoWeb": "2026-03-17T14:52:38.99+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,48 @@
Photos for CommessaWeb 564663 (iddatadb=1268):
Total photos found: 2, campioni: 2
=== Campione 736244 (main) ===
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736244)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1268-20260317134828-1e672dd9-5420-4432-b422-02d8d271c178.jpg' \
--form 'StampaNelRapporto=true' \
--form 'PrimaPagina=true'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1784768,
"FileName": "1268-20260317134828-1e672dd9-5420-4432-b422-02d8d271c178.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-17T14:52:33.9816658+01:00",
"Titolo": null
}
---
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione(736244)/UploadCampioneFile' \
--header 'Authorization: Bearer ••••••' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1268-20260317134828-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg' \
--form 'StampaNelRapporto=false' \
--form 'PrimaPagina=false'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CampioneFiles\/$entity",
"IdCampioneFile": 1784769,
"FileName": "1268-20260317134828-104a2ca5-8a4c-4df8-b2ca-a28f518b7b25.jpg",
"Web": false,
"AllegaAlRapporto": false,
"StampaNelRapporto": false,
"ReportGermania": false,
"PrimaPagina": false,
"Duplica": false,
"LastUpdate": "2026-03-17T14:52:35.2759882+01:00",
"Titolo": null
}
---
@@ -0,0 +1,18 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)/InviaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#CommessaWeb\/$entity",
"IdCommessa": 564663,
"CodiceCommessa": "26C0059",
"RiferimentoCertificato": null,
"StatoCommessaWeb": "Nuova",
"DataCreazioneWeb": "2026-03-17T14:52:29.347+01:00",
"CodiceCommessaWeb": "26C0059",
"DataInviatoWeb": "2026-03-17T14:52:38.9894528+01:00",
"Richiedente": "From TRFSmart Application",
"Descrizione": "From TRFSmart Application"
}
@@ -0,0 +1,226 @@
curl --location --request PATCH 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(564663)' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"CommesseCustomFields": [
{
"IdCommesseCustomFields": 23308665,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308666,
"Valore": "13:52"
},
{
"IdCommesseCustomFields": 23308667,
"Valore": "9299"
},
{
"IdCommesseCustomFields": 23308668,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308669,
"Valore": "aaa"
},
{
"IdCommesseCustomFields": 23308670,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308676,
"Valore": "673"
},
{
"IdCommesseCustomFields": 23308677,
"Valore": "674"
},
{
"IdCommesseCustomFields": 23308678,
"Valore": "4410"
},
{
"IdCommesseCustomFields": 23308680,
"Valore": "1468"
},
{
"IdCommesseCustomFields": 23308681,
"Valore": "1604"
},
{
"IdCommesseCustomFields": 23308682,
"Valore": "12875"
},
{
"IdCommesseCustomFields": 23308683,
"Valore": "8755"
},
{
"IdCommesseCustomFields": 23308684,
"Valore": ""
},
{
"IdCommesseCustomFields": 23308644,
"Valore": "13497"
},
{
"IdCommesseCustomFields": 23308645,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23308646,
"Valore": "Example Description"
},
{
"IdCommesseCustomFields": 23308685,
"Valore": "1892"
},
{
"IdCommesseCustomFields": 23308686,
"Valore": "1111"
},
{
"IdCommesseCustomFields": 23308687,
"Valore": "2026-03-02T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308688,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308689,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308690,
"Valore": "1978"
},
{
"IdCommesseCustomFields": 23308691,
"Valore": "13:52"
},
{
"IdCommesseCustomFields": 23308692,
"Valore": "14:44"
},
{
"IdCommesseCustomFields": 23308693,
"Valore": "14:00"
},
{
"IdCommesseCustomFields": 23308694,
"Valore": "2026-03-17T13:52:36+00:00"
},
{
"IdCommesseCustomFields": 23308695,
"Valore": "8596"
},
{
"IdCommesseCustomFields": 23308696,
"Valore": "8600"
},
{
"IdCommesseCustomFields": 23308697,
"Valore": "8605"
},
{
"IdCommesseCustomFields": 23308647,
"Valore": "L209A4M00130M7280"
},
{
"IdCommesseCustomFields": 23308648,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308649,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308650,
"Valore": "264"
},
{
"IdCommesseCustomFields": 23308651,
"Valore": "1589"
},
{
"IdCommesseCustomFields": 23308652,
"Valore": "AAA"
},
{
"IdCommesseCustomFields": 23308653,
"Valore": "3055"
},
{
"IdCommesseCustomFields": 23308654,
"Valore": "aaa"
},
{
"IdCommesseCustomFields": 23308655,
"Valore": "qqq"
},
{
"IdCommesseCustomFields": 23308656,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308657,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308658,
"Valore": "qqq"
},
{
"IdCommesseCustomFields": 23308659,
"Valore": "236"
},
{
"IdCommesseCustomFields": 23308660,
"Valore": "1208"
},
{
"IdCommesseCustomFields": 23308661,
"Valore": "ciao"
},
{
"IdCommesseCustomFields": 23308662,
"Valore": "bbb"
},
{
"IdCommesseCustomFields": 23308663,
"Valore": "12234"
},
{
"IdCommesseCustomFields": 23308664,
"Valore": "MONCLER"
},
{
"IdCommesseCustomFields": 23308671,
"Valore": "L209A4M00130M7280 - PACEY2"
},
{
"IdCommesseCustomFields": 23308672,
"Valore": "4678"
},
{
"IdCommesseCustomFields": 23308673,
"Valore": "1"
},
{
"IdCommesseCustomFields": 23308674,
"Valore": ""
},
{
"IdCommesseCustomFields": 23308675,
"Valore": "2450"
},
{
"IdCommesseCustomFields": 23308679,
"Valore": "744"
}
]
}'
RESPONSE:
null
@@ -0,0 +1,120 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "AAA",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736487,
"CodiceCampione": "11016",
"CodiceCampioneWeb": "11016",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:03.4537462+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "AAA",
"GruppiRicercati": null
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "CCC",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736488,
"CodiceCampione": "11017",
"CodiceCampioneWeb": "11017",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:04.8623753+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "CCC",
"GruppiRicercati": null
}
---
CAMPIONE #2
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "DDD",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736489,
"CodiceCampione": "11018",
"CodiceCampioneWeb": "11018",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:06.2840533+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "DDD",
"GruppiRicercati": null
}
---
CAMPIONE #3
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 564817,
"Matrice": 3028,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "EEE",
"ConsegnaRichiesta": "2026-01-29"
}'
RESPONSE:
{
"@odata.context": "https:\/\/bvcpsitaly-elims.com\/limsapi\/api\/odata\/$metadata#Campione\/$entity",
"IdCampione": 736490,
"CodiceCampione": "11019",
"CodiceCampioneWeb": "11019",
"StatoCampione": "Nuovo",
"DataCreazioneWeb": "2026-03-18T10:54:07.8853452+01:00",
"RiferimentoWeb": "",
"ConsegnaRichiesta": "2026-01-29T00:00:00+01:00",
"DataAccettazioneLims": null,
"Riferimento": null,
"NoteWeb": "EEE",
"GruppiRicercati": null
}
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More