Compare commits

...
Author SHA1 Message Date
RMubarakzyanov 1249ad3079 fix SceltaMultipla mapping 2026-10-26 19:45:10 +03:00
solocla 48ee511166 fixed filter order 2026-10-26 16:46:45 +01:00
RMubarakzyanov 6718e08d1f Merge branch 'main' into feature/xls-mapping 2026-09-23 16:03:43 +03:00
RMubarakzyanov b8b01cd5d5 xls binding 2026-09-23 15:50:03 +03:00
solocla f31ba24008 email send afetr export routine 2026-09-23 11:40:57 +02:00
solocla bb2ebcc122 fixed export to lims 2026-09-22 15:27:35 +02:00
solocla 955c760756 Stop tracking runtime files (error_log, schemi_base_response, logs, uploads) 2026-09-22 14:08:27 +02:00
solocla 5c12d97d0e added error page and logs linked 2026-09-22 13:58:14 +02:00
solocla 9bed740223 added log step 9 2026-09-22 12:13:02 +02:00
solocla 8be7ea0b85 fixed2 clientefatturazione 2026-09-18 07:59:49 +02:00
solocla dc0209dcf1 removed clientefatturazione on export 2026-09-16 10:12:58 +02:00
solocla 9cce361b90 added clientefatturazione 2026-09-16 09:22:08 +02:00
solocla c966096060 analysis in alphabetical order 2026-09-16 08:20:26 +02:00
solocla 55da10cea4 fixed routine error 2026-09-14 16:58:27 +02:00
solocla 0d3ed2505f fixed note with slash 2026-09-08 10:29:45 +02:00
solocla def7377447 fixed sticky bars with toggle 2026-09-08 09:18:17 +02:00
solocla 9453e7a01a sticky action button on topbar 2026-09-08 08:48:26 +02:00
solocla 8f4ac3d53e added routine engine 2026-09-05 13:41:26 +02:00
solocla 34f7490e94 session keep alive 2026-09-05 13:41:26 +02:00
solocla d9d6a7ab58 cambiato ttl warm cache 2026-09-05 13:41:26 +02:00
solocla 14a7cc2738 revert stats_export files to pre-a1e91f8 version 2026-08-04 20:10:27 +02:00
solocla 601e72ca61 fixed quotations parts 2026-08-04 20:02:50 +02:00
solocla 54c164478c fixed import XLS big 2026-08-04 14:42:05 +02:00
solocla 965d2fa5bb sticky bar import XLS 2026-08-04 14:21:14 +02:00
solocla a1e91f8d76 fixed stats with comeesaa campioni 2026-08-04 12:57:00 +02:00
solocla 153cdbca98 filter analysis withput NominativoCliente 2026-08-03 12:00:37 +02:00
solocla 2603c86c5b exlcuded login time and date from validation 2026-07-30 17:07:47 +02:00
solocla e7d733a136 fixed order lines 2026-07-30 13:18:27 +02:00
solocla eb34cba7bb added search on add fields ranking 2026-07-29 16:37:58 +02:00
solocla f01bfa06c9 fixed exxcel row column 2026-07-29 12:24:58 +02:00
solocla 71d6fe1d98 fixed sample arrival and date 2026-07-23 16:00:26 +02:00
solocla 33615f65d7 Merge branch 'filter' 2026-07-23 13:09:02 +02:00
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 9dda4630a4 fixed modal analysis 2026-07-20 15:41:29 +02:00
solocla 6d0b98bd41 analysis fixed matrix 2026-07-20 15:21:20 +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
solocla e92e107a3c search by ranking custom field 2026-07-20 11:30:26 +02:00
solocla 9330a49008 fixed stats 2 2026-07-20 10:58:12 +02:00
solocla 80dbd26929 fixed stats 2026-07-20 10:47:54 +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
solocla 8d6fe92481 fixed multiple parts mix and single line 2025-10-28 08:58:39 +01:00
solocla dbc66723a6 fixed color save 2025-10-27 15:53:12 +01:00
solocla 218fc14462 fixed country client and parts column matrice 2025-10-27 14:38:20 +01:00
solocla 29e4b41874 update export to lims 2025-10-11 20:19:43 +02:00
solocla eef9ae8d36 added note to export 2025-10-10 11:31:49 +02:00
solocla 68c867a3f4 change clienti to datadb and fixed column pages 2025-10-09 15:30:44 +02:00
solocla a9827e4e81 added note and date to identification parts 2025-10-08 17:34:21 +02:00
solocla b51936f784 various fixing modal 2025-10-07 20:56:57 +02:00
solocla 15b6f38e8b fixed matrici with db cron 2025-10-07 09:41:29 +02:00
solocla 12c6cc5f95 lazy load modal parts and matrici cron 2025-10-07 09:12:54 +02:00
solocla a0b12463c0 fixed for matrici 2025-10-03 08:52:37 +02:00
solocla 07ddcafd3f fixed nologin 2025-09-27 13:38:26 +02:00
solocla 7843d4b1fc added nologin 2025-09-27 13:37:37 +02:00
solocla 4eae855e23 added headscriptnologin 2025-09-27 13:36:26 +02:00
solocla c709f64a17 remove login smartphone 2025-09-27 13:33:23 +02:00
solocla d5f0690f59 background trasparent annotation 2025-09-27 13:28:21 +02:00
solocla 6bbd3fcae9 fixed get clienti with id, nominativo and country 2025-09-27 09:53:21 +02:00
solocla 9e19e9e1d4 fixed export to lims, fixed multiple upload, added calendar to Data 2025-09-27 09:44:00 +02:00
solocla 7caee9c994 prova 2025-09-26 11:27:33 +02:00
solocla f8320315f7 historical added commessaweb., edit added tested components 2025-09-26 09:28:49 +02:00
solocla 7397d86bc2 fixed template dashboard 2025-09-25 14:26:22 +02:00
solocla 2deb1f101a hide column and fixed edit template 2025-09-25 14:18:09 +02:00
solocla ed4467337f update moncler routine 2025-09-25 11:03:47 +02:00
solocla 864714d198 routine scripts 2025-09-24 14:18:31 +02:00
solocla 33aacfb469 env example 2025-09-23 17:39:44 +02:00
solocla e0e262fd32 Merge feature/lims-api into main, prefer lims-api version for conflicted files 2025-09-23 14:24:54 +02:00
solocla 5d6302fa9c added admin role to button export 2025-09-23 10:33:16 +02:00
solocla 3da8ff81c9 added record idcommessaweb and commessaweb and update status to export to lims 2025-09-23 09:44:58 +02:00
kapsona777 a36dd02771 Lims api working version without comment 2025-09-22 20:24:53 +04:00
kapsona777 0a6fb98476 Lims api working version with comments 2025-09-22 20:16:56 +04:00
solocla 57ddd4bb5a fixed photo quotations 2025-09-22 11:22:05 +02:00
solocla df5e6d5656 fixed save photos for modsecurity 2025-09-22 10:47:48 +02:00
solocla 78495880ca fixed quotations 2025-09-22 09:19:36 +02:00
solocla 960832efb1 Merge feature/quotations into main, prefer quotations version for modal_parts.php and parts.js 2025-09-22 08:24:05 +02:00
solocla 447a0d1dea fixed canvas dimension 2025-09-22 08:15:01 +02:00
solocla 5b47416841 fixed part.js 2025-09-22 07:59:13 +02:00
solocla 3e4a627ca7 fixed quotations 2025-09-20 22:00:16 +02:00
solocla 420b0a0405 added routine to insert templates 2025-09-19 12:15:41 +02:00
solocla b39d601ec9 Merge branch 'features/routine' 2025-09-19 12:10:01 +02:00
solocla 89d13699b4 added routine functionalites to template 2025-09-19 12:09:01 +02:00
solocla 9826331545 update 2025-09-19 11:45:21 +02:00
solocla 9d8718d110 only entraid provider 2025-09-19 09:50:57 +02:00
solocla 16e00f8573 entraid integration completa with docs 2025-09-19 09:31:20 +02:00
solocla baf3f6da32 photo size fixed 2025-09-18 17:02:38 +02:00
solocla 62bf4ebd92 export to lims 2025-09-18 12:00:43 +02:00
solocla 6e465e3010 fixed color parts 2025-09-17 09:41:51 +02:00
solocla 8b08969c69 get matrice 2025-09-16 11:39:44 +02:00
solocla 25d4519684 fixed quotations 2025-09-13 21:28:11 +02:00
solocla 34d4dc8660 quotations page 2025-09-13 12:31:31 +02:00
solocla 1510ef03f1 added level to collahge 2025-09-12 18:09:01 +02:00
solocla ce8c95921f remove .env from repository 2025-09-12 10:17:22 +02:00
solocla 095a6ae879 remove debug/log/json files from tracking 2025-09-12 10:03:50 +02:00
solocla 296143016a template mapping addes is visible as checkbox and is required as badge 2025-09-12 10:01:19 +02:00
kapsona777 412dce8941 refactored api code 2025-09-11 18:46:47 +04:00
kapsona777 586226ceaf fixed alert issue 2025-09-10 20:54:31 +04:00
kapsona777 ac09d8d0eb fixed issue, now works great. // ⚠️ Simulation ON (change it to false to enable real API calls)
$simulate = true;
2025-09-10 20:30:02 +04:00
kapsona777 33e3ae059d this commit is uploaded for testing only, no api calls are made 2025-09-09 17:28:48 +04:00
solocla 3aa2504f3c fixed cut 2025-09-08 14:01:17 +02:00
solocla c1a396f246 added canvas functionality 2025-09-08 11:40:43 +02:00
solocla a45ba1c8b3 added cut 2025-09-08 10:26:15 +02:00
solocla 7a944a73f7 get_commessaweb 2025-09-08 08:42:05 +02:00
solocla 71595cc8de added speech functions 2025-09-06 18:48:38 +02:00
solocla f89dbd0c23 added save all 2025-09-06 12:29:29 +02:00
solocla 9ba859e15b Merge remote-tracking branch 'origin/bugfix/warning-text-and-logic-change' 2025-09-05 21:44:38 +02:00
solocla 672e448e9a fixed renumerate parts 2025-09-04 15:16:19 +02:00
kapsona777 d692614f70 warning fix 2025-09-01 19:56:08 +04:00
293 changed files with 145000 additions and 24972 deletions
-47
View File
@@ -1,47 +0,0 @@
APP_ENV=production
APP_DEBUG=true
APP_KEY=base64:C+sutHm6xP5sE4QXhoZFhYjArlVN11s2mDU1F8beUkM=
APP_URL=http://vanguard.test
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST="localhost"
DB_DATABASE="trfcertest"
DB_USERNAME="solocla"
DB_PASSWORD="!Massarosa2"
DB_PREFIX="auth_"
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_DRIVER=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=mail
MAIL_FROM_NAME=Vanguard
MAIL_FROM_ADDRESS=vanguard@test.dev
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Credenziali API VisualLims
API_BASE_URL=https://93.43.5.102/limsapi
API_USERNAME=WebApiUser
API_PASSWORD=webapiuser01
BASE_URL=http://localhost:8000/userarea/
+14 -7
View File
@@ -1,16 +1,16 @@
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_DEBUG=true
APP_KEY=base64:C+sutHm6xP5sE4QXhoZFhYjArlVN11s2mDU1F8beUkM=
APP_URL=http://vanguard.test
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=vanguard
DB_USERNAME=homestead
DB_PASSWORD=secret
DB_PREFIX=vg_
DB_HOST="localhost"
DB_DATABASE="xxxx"
DB_USERNAME="xxxx"
DB_PASSWORD="xxxxx"
DB_PREFIX="auth_"
BROADCAST_DRIVER=log
CACHE_DRIVER=file
@@ -39,3 +39,10 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Credenziali API VisualLims
SIMULATE_EXPORT_LIMS=true
API_BASE_URL=https://bvcpsitaly-elims.com/limsapi
API_USERNAME=WebApiUser
API_PASSWORD=webapiuser01
BASE_URL=http://localhost:8000/userarea/
+48 -28
View File
@@ -1,54 +1,74 @@
.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
public/userarea/cache/
public/userarea/error_log.txt
# Ignora cartella photostrf in public/userarea
/public/userarea/photostrf/
public/userarea/customfield_values_response.json
# PDF/QR generati dai template (userarea)
/public/userarea/pdftemplates/
# Runtime / rigenerati - non tracciare
public/userarea/error_log.txt
public/userarea/schemi_base_response.json
public/userarea/logs/
public/userarea/imported_trf/
public/userarea/xlstemplates/
.well-known/
@@ -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');
}
+12 -1
View File
@@ -28,10 +28,21 @@ class AppServiceProvider extends ServiceProvider
\Illuminate\Database\Schema\Builder::defaultStringLength(191);
Factory::guessFactoryNamesUsing(function (string $modelName) {
return 'Database\Factories\\'.class_basename($modelName).'Factory';
return 'Database\Factories\\' . class_basename($modelName) . 'Factory';
});
\Illuminate\Pagination\Paginator::useBootstrap();
// Register Microsoft Socialite driver
$this->app->make('Laravel\Socialite\Contracts\Factory')->extend('microsoft', function ($app) {
$config = $app['config']['services.microsoft'];
return new \SocialiteProviders\Microsoft\Provider(
$app['request'],
$config['client_id'],
$config['client_secret'],
$config['redirect']
);
});
}
/**
+3
View File
@@ -34,6 +34,9 @@ class EventServiceProvider extends ServiceProvider
Verified::class => [
ActivateUser::class,
],
\SocialiteProviders\Manager\SocialiteWasMapped::class => [
\SocialiteProviders\Microsoft\MicrosoftExtendSocialite::class,
],
];
/**
+1 -1
View File
@@ -12,7 +12,7 @@
*/
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
realpath(__DIR__ . '/../')
);
/*
+4 -2
View File
@@ -38,12 +38,14 @@
"laravel/fortify": "^1.21",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"laravel/socialite": "^5.0",
"laravel/socialite": "^5.16",
"laravel/tinker": "^2.7",
"laravel/ui": "^4.0",
"phpmailer/phpmailer": "^6.9",
"phpmailer/phpmailer": "^7.1",
"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",
"vanguardapp/announcements": "^6.0",
Generated
+793 -23
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": "ef3e05e7260284f5b7c7b4b6f93b252b",
"content-hash": "e54cf3e0bbc04b94b563b01620ac3416",
"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",
@@ -2240,16 +2564,16 @@
},
{
"name": "laravel/socialite",
"version": "v5.15.1",
"version": "v5.16.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029"
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/cc02625f0bd1f95dc3688eb041cce0f1e709d029",
"reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029",
"url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
"shasum": ""
},
"require": {
@@ -2271,16 +2595,16 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Socialite\\SocialiteServiceProvider"
],
"aliases": {
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
}
},
"providers": [
"Laravel\\Socialite\\SocialiteServiceProvider"
]
},
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
@@ -2308,7 +2632,7 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
"time": "2024-06-28T20:09:34+00:00"
"time": "2024-09-03T09:46:57+00:00"
},
{
"name": "laravel/tinker",
@@ -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",
@@ -3758,16 +4166,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.9.3",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e"
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"shasum": ""
},
"require": {
@@ -3781,13 +4189,14 @@
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -3827,7 +4236,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
},
"funding": [
{
@@ -3835,7 +4244,7 @@
"type": "github"
}
],
"time": "2024-11-24T18:04:13+00:00"
"time": "2026-05-18T08:06:14+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
@@ -4980,6 +5389,218 @@
],
"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",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/8180ec14bef230ec2351cff993d5d2d7ca470ef4",
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4",
"shasum": ""
},
"require": {
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"laravel/socialite": "^5.5",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.2",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"SocialiteProviders\\Manager\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
},
{
"name": "Miguel Piedrafita",
"email": "soy@miguelpiedrafita.com"
},
{
"name": "atymic",
"email": "atymicq@gmail.com",
"homepage": "https://atymic.dev"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"homepage": "https://socialiteproviders.com",
"keywords": [
"laravel",
"manager",
"oauth",
"providers",
"socialite"
],
"support": {
"issues": "https://github.com/socialiteproviders/manager/issues",
"source": "https://github.com/socialiteproviders/manager"
},
"time": "2025-02-24T19:33:30+00:00"
},
{
"name": "socialiteproviders/microsoft",
"version": "4.7.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Microsoft.git",
"reference": "824ef97a4f6e3f363c21702b76676d54e8265573"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Microsoft/zipball/824ef97a4f6e3f363c21702b76676d54e8265573",
"reference": "824ef97a4f6e3f363c21702b76676d54e8265573",
"shasum": ""
},
"require": {
"ext-json": "*",
"firebase/php-jwt": "^6.8",
"php": "^8.0",
"socialiteproviders/manager": "^4.4"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Microsoft\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Faust",
"email": "hello@brianfaust.de"
}
],
"description": "Microsoft OAuth2 Provider for Laravel Socialite",
"keywords": [
"laravel",
"microsoft",
"oauth",
"provider",
"socialite"
],
"support": {
"docs": "https://socialiteproviders.com/microsoft",
"issues": "https://github.com/socialiteproviders/providers/issues",
"source": "https://github.com/socialiteproviders/providers"
},
"time": "2025-07-06T00:25:25+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.16.4",
@@ -5187,6 +5808,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",
@@ -5643,6 +6343,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",
@@ -11230,6 +12000,6 @@
"php": "^8.2.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
+1
View File
@@ -228,6 +228,7 @@ return [
Proengsoft\JsValidation\JsValidationServiceProvider::class,
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
\SocialiteProviders\Manager\ServiceProvider::class,
Webpatser\Countries\CountriesServiceProvider::class,
Intervention\Image\ImageServiceProvider::class,
Jenssegers\Agent\AgentServiceProvider::class,
+3 -3
View File
@@ -11,9 +11,9 @@ return [
|
*/
// 'social' => [
// 'providers' => ['facebook', 'twitter', 'google'],
// ],
'social' => [
'providers' => ['microsoft'],
],
/*
|--------------------------------------------------------------------------
+9 -3
View File
@@ -64,9 +64,15 @@ return [
'redirect' => env('GOOGLE_CALLBACK_URI'),
],
// 'authy' => [
// 'key' => env('AUTHY_KEY'),
// ],
'microsoft' => [
'client_id' => env('MICROSOFT_CLIENT_ID'),
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
'redirect' => env('MICROSOFT_REDIRECT_URI'),
],
// 'authy' => [
// 'key' => env('AUTHY_KEY'),
// ],
'resend' => [
'key' => env('RESEND_KEY'),
@@ -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();
}
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class ChangeExcelrowToVarchar extends AbstractMigration
{
public function up(): void
{
$this->table('datadb')
->changeColumn('excelrow', 'string', [
'limit' => 100,
'null' => true,
])
->update();
}
public function down(): void
{
$this->table('datadb')
->changeColumn('excelrow', 'integer', [
'null' => true,
])
->update();
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Aggiunge il flag `engine` alla tabella `routine` esistente (0 = legacy su file,
* 1 = motore generico basato su routine_step) e crea la tabella `routine_step`.
*
* Migrazione puramente additiva: le routine legacy esistenti (id 1..4) restano
* intatte e con engine = 0 continuano a funzionare come prima.
*/
final class AddEngineAndRoutineStep extends AbstractMigration
{
public function change(): void
{
// 1) Flag engine sulla tabella esistente `routine`
$routine = $this->table('routine');
if (!$routine->hasColumn('engine')) {
$routine
->addColumn('engine', 'boolean', [
'default' => 0,
'null' => false,
'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)',
'after' => 'filename',
])
->update();
}
// 2) Nuova tabella routine_step
if (!$this->hasTable('routine_step')) {
$table = $this->table('routine_step', [
'id' => 'idstep',
'signed' => false,
'engine' => 'InnoDB',
'collation' => 'utf8mb4_general_ci',
]);
$table
->addColumn('idroutine', 'integer', [
'null' => false,
'comment' => 'FK verso routine.idroutine',
])
->addColumn('step_order', 'integer', [
'default' => 1,
'null' => false,
'comment' => 'Ordine di esecuzione dello step nella routine',
])
->addColumn('operation', 'string', [
'limit' => 50,
'null' => false,
'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase',
])
->addColumn('params', 'text', [
'null' => true,
'comment' => 'Parametri operazione in formato JSON',
])
->addColumn('active', 'boolean', [
'default' => 1,
'null' => false,
'comment' => 'Se 0 lo step viene saltato',
])
->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order'])
->create();
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddLogFileToDatadb extends AbstractMigration
{
public function change(): void
{
$table = $this->table('datadb');
if (!$table->hasColumn('log_file')) {
$table->addColumn('log_file', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'commessaweb', // aggiusta se la colonna non esiste
])->update();
}
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddErrorStatusToDatadb extends AbstractMigration
{
public function up(): void
{
// Aggiunge 'x' (errore export) all'enum status, preservando i valori esistenti
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l','x') NULL DEFAULT NULL");
}
public function down(): void
{
// Rollback: riporta le eventuali 'x' a NULL prima di togliere il valore
$this->execute("UPDATE `datadb` SET `status` = NULL WHERE `status` = 'x'");
$this->execute("ALTER TABLE `datadb`
MODIFY `status` ENUM('i','P','l') NULL DEFAULT NULL");
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddNotifyExportEmailToAuthUsers extends AbstractMigration
{
public function change(): void
{
$table = $this->table('auth_users');
if (!$table->hasColumn('notify_export_email')) {
$table->addColumn('notify_export_email', 'boolean', [
'default' => true, // di default ricevono la mail
'null' => false,
'after' => 'lims_global_user_id',
])->update();
}
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddAnalysisPriceToPartAnalyses extends AbstractMigration
{
public function change(): void
{
$this->table('identification_parts_analyses')
->addColumn('analysis_price', 'decimal', [
'precision' => 10,
'scale' => 2,
'null' => true,
'default' => null,
'after' => 'analysis_level',
])
->update();
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Ex public/userarea/sql/lims_value_binding.sql (feature/xls-mapping)
*/
final class CreateLimsValueBinding extends AbstractMigration
{
public function up(): void
{
if ($this->hasTable('lims_value_binding')) {
return;
}
$this->execute("
CREATE TABLE `lims_value_binding` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_id` int(11) NOT NULL,
`mapping_id` int(11) NOT NULL,
`field_id` int(11) NOT NULL,
`source_value` varchar(500) NOT NULL,
`source_key` varchar(500) NOT NULL,
`lims_value_id` int(11) NOT NULL,
`lims_value` varchar(500) DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`),
KEY `idx_template` (`template_id`),
KEY `idx_field_source` (`field_id`, `source_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
");
}
public function down(): void
{
$this->execute("DROP TABLE IF EXISTS `lims_value_binding`");
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
/**
* Ex public/userarea/sql/fixed_field_binding.sql (feature/xls-mapping)
* Idempotente: salta colonne/indici gia' presenti.
*/
final class XlsFixedFieldBinding extends AbstractMigration
{
public function up(): void
{
// --- template_fixed_mapping ---
$tfm = $this->table('template_fixed_mapping');
if (!$tfm->hasColumn('json_node')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `json_node` varchar(500) DEFAULT NULL");
}
if (!$tfm->hasColumn('default_source')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
} else {
$this->execute("ALTER TABLE `template_fixed_mapping`
MODIFY COLUMN `default_source` enum('manual','xls','json') NOT NULL DEFAULT 'manual'");
}
if (!$tfm->hasColumn('excel_column')) {
$this->execute("ALTER TABLE `template_fixed_mapping`
ADD COLUMN `excel_column` varchar(255) DEFAULT NULL AFTER `json_node`");
}
// --- lims_value_binding ---
$lvb = $this->table('lims_value_binding');
if (!$lvb->hasColumn('binding_kind')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD COLUMN `binding_kind` enum('custom','fixed') NOT NULL DEFAULT 'custom' AFTER `template_id`");
}
$this->execute("ALTER TABLE `lims_value_binding`
MODIFY COLUMN `field_id` int(11) NOT NULL DEFAULT 0");
if ($lvb->hasIndexByName('uq_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_mapping_source`");
}
if (!$lvb->hasIndexByName('uq_kind_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD UNIQUE KEY `uq_kind_mapping_source` (`binding_kind`, `mapping_id`, `source_key`)");
}
}
public function down(): void
{
$lvb = $this->table('lims_value_binding');
if ($lvb->hasIndexByName('uq_kind_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP INDEX `uq_kind_mapping_source`");
}
if (!$lvb->hasIndexByName('uq_mapping_source')) {
$this->execute("ALTER TABLE `lims_value_binding`
ADD UNIQUE KEY `uq_mapping_source` (`mapping_id`, `source_key`)");
}
if ($lvb->hasColumn('binding_kind')) {
$this->execute("ALTER TABLE `lims_value_binding` DROP COLUMN `binding_kind`");
}
$this->execute("ALTER TABLE `lims_value_binding`
MODIFY COLUMN `field_id` int(11) NOT NULL");
$tfm = $this->table('template_fixed_mapping');
foreach (['excel_column', 'default_source', 'json_node'] as $col) {
if ($tfm->hasColumn($col)) {
$this->execute("ALTER TABLE `template_fixed_mapping` DROP COLUMN `$col`");
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
use Phinx\Seed\AbstractSeed;
/**
* Seed dimostrativo del motore generico.
*
* - Assicura che le routine legacy esistenti abbiano engine = 0 (nessun impatto).
* - Crea una routine di esempio "merge_D_F_H_into_J" con engine = 1
* e il relativo step: unisce le colonne D, F, H nella colonna J.
*
* Nota: filename per le routine engine punta al file motore condiviso _engine.php,
* così apply_routine.php lo risolve correttamente anche se il flusso invia il filename.
*/
final class RoutineEngineSeed extends AbstractSeed
{
public function run(): void
{
$pdo = $this->getAdapter()->getConnection();
// 1) Le legacy restano engine = 0 (idempotente).
$pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL");
// 2) Crea/aggiorna la routine engine di esempio.
$name = 'merge_D_F_H_into_J';
$description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione';
$filename = '_engine.php';
$check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1");
$check->execute([':name' => $name]);
$existing = $check->fetch(PDO::FETCH_ASSOC);
if ($existing) {
$idroutine = (int)$existing['idroutine'];
$upd = $pdo->prepare(
"UPDATE routine
SET description = :descr, filename = :fn, engine = 1, instruction = :instr
WHERE idroutine = :id"
);
$upd->execute([
':descr' => $description,
':fn' => $filename,
':instr' => 'Routine gestita dal motore generico (routine_step).',
':id' => $idroutine,
]);
} else {
$ins = $pdo->prepare(
"INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
VALUES (:name, :descr, :instr, :fn, 1, 1)"
);
$ins->execute([
':name' => $name,
':descr' => $description,
':instr' => 'Routine gestita dal motore generico (routine_step).',
':fn' => $filename,
]);
$idroutine = (int)$pdo->lastInsertId();
}
// 3) Ripulisci eventuali step precedenti di questa routine e reinserisci.
$del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
$del->execute([':id' => $idroutine]);
// Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità.
$params = json_encode([
'source_columns' => ['D', 'F', 'H'],
'target_column' => 'J',
'separator' => ' ',
'skip_empty' => true,
], JSON_UNESCAPED_UNICODE);
$insStep = $pdo->prepare(
"INSERT INTO routine_step (idroutine, step_order, operation, params, active)
VALUES (:id, 1, 'merge', :params, 1)"
);
$insStep->execute([':id' => $idroutine, ':params' => $params]);
}
}
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',
];
+2
View File
@@ -25,6 +25,7 @@ $templatedashboard = "Template Dashboard";
$newtemplate = "New Template";
$dashtemplate = "Dashboard Templates";
$insertnewtemplatexls = "Insert New Template XLS";
$bindingsmanage = "Value Bindings";
// Datatables
$langdatatables = [
"paginate_first" => "First",
@@ -32,3 +33,4 @@ $langdatatables = [
"paginate_next" => "Next",
"paginate_previous" => "Previous"
];
$quotationstitle = "Quotations";
+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()]);
}
+913
View File
@@ -0,0 +1,913 @@
(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,
analysis_price: rowEl.getAttribute("data-price") || "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,
analysis_price:
payload.analysis_price !== ""
? parseFloat(payload.analysis_price)
: null,
});
}
}
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,
analysis_price: payload.analysis_price,
},
})
.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 rawPrice = item.Prezzo;
const priceNum =
rawPrice === null ||
rawPrice === undefined ||
rawPrice === ""
? 0
: parseFloat(String(rawPrice).replace(",", "."));
const price = isNaN(priceNum) ? 0 : priceNum;
const hasPrice = price > 0;
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>";
}
if (!hasPrice) {
badges += '<span class="badge bg-danger">No price</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-price="${escapeHtml(price)}"
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"
);
})
: [];
analyses.sort(function (a, b) {
const nameA = (
a.NomeAnalisiTraduzione ||
a.NomeAnalisi ||
""
).toLowerCase();
const nameB = (
b.NomeAnalisiTraduzione ||
b.NomeAnalisi ||
""
).toLowerCase();
return nameA.localeCompare(nameB);
});
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", "");
}
});
})();
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
<?php
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 {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? intval($input['template_id']) : 0;
$filename = $input['routine_data']['filename'] ?? '';
$headerrow = $input['routine_data']['headerrow'] ?? 1;
$excelData = $input['excel_data'] ?? [];
$routineData = $input['routine_data'] ?? [];
if (!$filename || empty($excelData)) {
throw new Exception("Dati della routine mancanti.");
}
// --- NUOVO: risolvi la routine dal DB per decidere legacy vs motore ---
// Riusa la connessione PDO del progetto.
require_once __DIR__ . '/class/db-functions.php';
$pdo = DBHandlerSelect::getInstance()->getConnection();
// Identifica la routine: preferisci idroutine se passato, altrimenti risali dal filename.
$idroutine = isset($input['routine_data']['idroutine'])
? (int)$input['routine_data']['idroutine']
: 0;
if ($idroutine > 0) {
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE idroutine = :id LIMIT 1");
$stmt->execute([':id' => $idroutine]);
} else {
// Il matching per filename è ambiguo per le routine a motore: tutte
// condividono _engine.php. Senza idroutine non possiamo sapere quale
// routine engine applicare -> meglio fermarsi che eseguire quella sbagliata.
if ($filename === '_engine.php') {
throw new Exception("Routine a motore senza idroutine: impossibile determinare quale routine applicare. Verifica che il template inoltri routine_data.idroutine.");
}
$stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE filename = :fn LIMIT 1");
$stmt->execute([':fn' => $filename]);
}
$routineRow = $stmt->fetch();
// engine = 1 -> motore generico (_engine.php); 0 o record assente -> legacy come oggi.
$useEngine = $routineRow && (int)$routineRow['engine'] === 1;
if ($useEngine) {
// Il motore ha bisogno di sapere quale routine caricare dagli step.
$routineData['idroutine'] = (int)$routineRow['idroutine'];
$routineData['name'] = $routineData['name'] ?? ($routineRow['name'] ?? '');
$routineFile = __DIR__ . '/routines/_engine.php';
} else {
// Percorso legacy: il vecchio file della routine, identico a prima.
$legacyFilename = $routineRow['filename'] ?? $filename;
$routineFile = __DIR__ . '/routines/' . $legacyFilename;
}
if (file_exists($routineFile)) {
include_once $routineFile;
$routineData['xls_headers'] = $_SESSION['headers'] ?? [];
applyRoutine($excelData, $routineData); // Modifica $excelData in place
error_log("Routine " . ($routineData['name'] ?? '') . " applicata (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}");
} else {
throw new Exception("File della routine non trovato: $routineFile");
}
// Aggiorna la sessione con i dati modificati
$_SESSION['excel_data'] = $excelData;
$response['excel_data'] = $excelData;
$response['rows'] = array_column($excelData, 'data');
$response['columns'] = $_SESSION['headers'];
$response['template_id'] = $template_id;
$response['filename'] = $input['filename'] ?? '';
} else {
$response['error'] = "Richiesta non valida.";
}
} catch (Exception $e) {
$response['error'] = "Errore durante l'applicazione della routine: " . $e->getMessage();
error_log("Exception in apply_routine.php: " . $e->getMessage());
}
ob_end_clean();
header('Content-Type: application/json');
echo json_encode($response);
exit;
+299
View File
@@ -0,0 +1,299 @@
<?php
include('include/headscript.php');
require_once __DIR__ . '/class/binding-functions.php';
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$templateFilter = isset($_GET['template_id']) ? (int) $_GET['template_id'] : 0;
$search = trim($_GET['q'] ?? '');
$templates = $pdo->query("SELECT id, name FROM excel_templates ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$conds = [];
$params = [];
if ($templateFilter > 0) {
$conds[] = 'b.template_id = ?';
$params[] = $templateFilter;
}
if ($search !== '') {
$conds[] = '(b.source_value LIKE ? OR b.lims_value LIKE ? OR m.field_label LIKE ? OR f.fixed_field_key LIKE ?)';
$like = '%' . $search . '%';
array_push($params, $like, $like, $like, $like);
}
$where = $conds ? ('WHERE ' . implode(' AND ', $conds)) : '';
$stmt = $pdo->prepare(
"SELECT b.id, b.template_id, b.binding_kind, b.mapping_id, b.field_id, b.source_value,
b.lims_value_id, b.lims_value, b.updated_at,
t.name AS template_name,
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
COALESCE(m.excel_column, f.excel_column) AS excel_column,
f.fixed_field_key
FROM lims_value_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
$where
ORDER BY t.name ASC, field_label ASC, b.source_value ASC"
);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$groups = [];
foreach ($rows as $r) {
$kind = $r['binding_kind'] ?? 'custom';
$gkey = $kind . ':' . (int) $r['mapping_id'];
if (!isset($groups[$gkey])) {
$groups[$gkey] = [
'kind' => $kind,
'label' => $kind === 'fixed'
? binding_fixed_label((string) $r['fixed_field_key'])
: ($r['field_label'] ?: ('Mapping ' . $gkey)),
'excel_column' => $r['excel_column'] ?: '',
'template_name' => $r['template_name'] ?: ('Template ' . $r['template_id']),
'template_id' => (int) $r['template_id'],
'rows' => [],
];
}
$groups[$gkey]['rows'][] = $r;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<style>
.source-value {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
}
td .select2-container {
min-width: 240px;
}
.select2-container--default .select2-selection--single {
height: 38px;
border-color: #ced4da;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
<title>Gestione binding - <?= 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 radius-10">
<div class="card-header">
<h6 class="mb-0">Binding valori importati &rarr; LIMS</h6>
<small class="text-muted">
Le modifiche valgono dalla prossima importazione: i record gia' importati non vengono aggiornati.
</small>
</div>
<div class="card-body">
<form class="row g-2 align-items-end mb-3" method="get">
<div class="col-md-4">
<label class="form-label mb-1">Template</label>
<select class="form-select" name="template_id" onchange="this.form.submit()">
<option value="">Tutti</option>
<?php foreach ($templates as $t): ?>
<option value="<?= (int) $t['id'] ?>" <?= $templateFilter === (int) $t['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label mb-1">Cerca</label>
<input type="text" class="form-control" name="q" value="<?= htmlspecialchars($search) ?>"
placeholder="valore importato, valore LIMS, campo...">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">Filtra</button>
<a href="bindings_manage.php" class="btn btn-outline-secondary">Azzera</a>
</div>
</form>
<div id="bindingError" class="alert alert-danger" style="display:none;"></div>
<?php if (empty($rows)): ?>
<div class="alert alert-info mb-0">Nessun binding salvato per i filtri selezionati.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width:32px;"></th>
<th>Valore importato</th>
<th>Valore LIMS</th>
<th style="width:150px;">Aggiornato</th>
<th style="width:110px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $gkey => $g): ?>
<tr class="table-light">
<td colspan="5">
<strong><?= htmlspecialchars($g['label']) ?></strong>
<?php if (($g['kind'] ?? 'custom') === 'fixed'): ?><span class="badge bg-light text-dark border">campo fisso</span><?php endif; ?>
<span class="text-muted">
&middot; <?= htmlspecialchars($g['template_name']) ?>
<?php if ($g['excel_column'] !== ''): ?>
&middot; colonna &laquo;<?= htmlspecialchars($g['excel_column']) ?>&raquo;
<?php endif; ?>
&middot; <?= count($g['rows']) ?> valore/i
</span>
</td>
</tr>
<?php foreach ($g['rows'] as $r): ?>
<tr class="binding-row"
data-binding-id="<?= (int) $r['id'] ?>"
data-kind="<?= htmlspecialchars($r['binding_kind'] ?? 'custom', ENT_QUOTES) ?>"
data-fixed-key="<?= htmlspecialchars($r['fixed_field_key'] ?? '', ENT_QUOTES) ?>"
data-template-id="<?= (int) $r['template_id'] ?>"
data-mapping-id="<?= (int) $r['mapping_id'] ?>"
data-field-id="<?= (int) $r['field_id'] ?>"
data-source-value="<?= htmlspecialchars($r['source_value'], ENT_QUOTES) ?>">
<td class="text-muted ps-4">&rsaquo;</td>
<td class="source-value"><?= htmlspecialchars($r['source_value']) ?></td>
<td>
<select class="form-select binding-select">
<option value="<?= (int) $r['lims_value_id'] ?>" selected>
<?= htmlspecialchars($r['lims_value'] ?: ('#' . $r['lims_value_id'])) ?>
</option>
</select>
<div class="binding-status small mt-1"></div>
</td>
<td class="small text-muted"><?= htmlspecialchars((string) $r['updated_at']) ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger delete-binding-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</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'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(function() {
const $error = $('#bindingError');
$('.binding-select').each(function() {
const $row = $(this).closest('.binding-row');
const isFixed = $row.data('kind') === 'fixed';
$(this).select2({
placeholder: 'Seleziona valore LIMS...',
width: '100%',
ajax: {
url: isFixed ? 'search_fixed_field_values.php' : 'search_customfield_values.php',
dataType: 'json',
delay: 200,
data: params => isFixed ? {
field_key: $row.data('fixed-key'),
template_id: $row.data('template-id'),
q: params.term || '',
limit: 50
} : {
field_id: $row.data('field-id'),
q: params.term || '',
limit: 50
},
processResults: data => ({
results: data.results || []
})
},
minimumInputLength: 0
});
});
$('.binding-select').on('change', function() {
const $select = $(this);
const $row = $select.closest('.binding-row');
const $status = $row.find('.binding-status');
const selected = $select.select2('data')[0] || {};
if (!$select.val()) {
return;
}
$status.removeClass('text-success text-danger').addClass('text-muted').text('Salvataggio...');
$.post('save_binding.php', {
kind: $row.data('kind') || 'custom',
template_id: $row.data('template-id'),
mapping_id: $row.data('mapping-id'),
field_id: $row.data('field-id'),
source_value: String($row.data('source-value')),
lims_value_id: $select.val(),
lims_value: selected.text || ''
}).then(resp => {
if (resp && resp.success) {
$status.removeClass('text-muted').addClass('text-success').text('Salvato');
} else {
$status.removeClass('text-muted').addClass('text-danger').text((resp && resp.error) || 'Errore');
}
}).fail(() => {
$status.removeClass('text-muted').addClass('text-danger').text('Errore di rete');
});
});
$('.delete-binding-btn').on('click', function() {
const $row = $(this).closest('.binding-row');
$error.hide();
$.post('delete_binding.php', {
binding_id: $row.data('binding-id')
}).then(resp => {
if (resp && resp.success) {
$row.remove();
} else {
$error.text((resp && resp.error) || 'Errore durante l\'eliminazione.').show();
}
}).fail(() => {
$error.text('Errore di rete durante l\'eliminazione.').show();
});
});
});
</script>
</body>
</html>
+131
View File
@@ -0,0 +1,131 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
header("Content-Type: application/json");
// 🔹 Configura directory log (creala se non esiste)
$logDir = __DIR__ . '/logs/api/';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
// 🔹 Base URL API
$apiBaseUrl = 'https://93.43.5.102/limsapi/api/odata/';
// 🔹 Hardcoded values
$iddatadb = 846;
$commessaId = 533357;
try {
// 🔹 STEP 4: Fetch Field Values with Labels (usa dati reali per iddatadb=845)
$stmt = $pdo->prepare("
SELECT
idd.field_value,
m.field_label,
m.schema_id,
m.field_id
FROM
import_data_details as idd
JOIN template_mapping m ON idd.mapping_id = m.id
WHERE idd.id = :iddatadb
");
$stmt->execute(['iddatadb' => $iddatadb]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$fieldValues = [];
$valueMap = []; // Mappa per field_id -> valore
foreach ($rows as $row) {
$fieldValues[] = [
"IdCommesseCustomFields" => (int) $row['field_id'],
"Valore" => $row['field_value'],
"FieldLabel" => $row['field_label']
];
$valueMap[(int) $row['field_id']] = $row['field_value']; // Mappa per ID definizione
}
// Logga i fieldValues in error_log
$logFieldValues = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT);
error_log($logFieldValues);
// 🔹 Initialize API client
$api = VisualLimsApiClient::getInstance();
// 🔹 STEP A: GET iniziale per CommesseCustomFields con espansione CustomField
$expand = "CommesseCustomFields(\$expand=CustomField)"; // Espansione come da istruzioni fornitore
$commessaWithFields = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
// 🔹 STEP B: Prepara payload PATCH (tutti i campi, sovrascrivi se match su CustomField.IdCustomField == field_id)
$commessaCustomFields = [];
foreach ($commessaWithFields["CommesseCustomFields"] as $customField) {
$definitionId = (int) ($customField["CustomField"]["IdCustomField"] ?? 0); // Usa IdCustomField dal CustomField
$currentValue = $customField["Valore"] ?? '';
$newValue = isset($valueMap[$definitionId]) ? $valueMap[$definitionId] : $currentValue;
$commessaCustomFields[] = [
"IdCommesseCustomFields" => (int) $customField["IdCommesseCustomFields"],
"Valore" => $newValue
];
}
// 🔹 Unico file di log per tutto
$logFile = $logDir . "commessa_{$commessaId}_patch_and_get_" . time() . ".txt";
$logContent = "FieldValues dal DB (iddatadb={$iddatadb}):\n" . json_encode($fieldValues, JSON_PRETTY_PRINT) . "\n\n";
// Log curl-like per GET iniziale
$logContent .= "GET iniziale:\n" .
"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) . "\n\n---\n";
if (!empty($commessaCustomFields)) {
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
// Log curl-like per PATCH
$jsonPayload = json_encode($updatePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContent .= "PATCH:\n" .
"curl --location --request PATCH '{$apiBaseUrl}CommessaWeb({$commessaId})' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n\n";
$patchResponse = $api->patch("CommessaWeb({$commessaId})", $updatePayload);
$logContent .= "PATCH RESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT) . "\n\n---\n";
} else {
$logContent .= "PATCH: Nessun campo custom da aggiornare\n\n---\n";
}
// 🔹 STEP C: GET di controllo post-PATCH
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
// Log curl-like per GET di controllo
$logContent .= "GET di controllo:\n" .
"curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
// Salva log unico
file_put_contents($logFile, $logContent);
// 🔹 Output a schermo
echo json_encode([
"success" => true,
"message" => "PATCH eseguito su commessa {$commessaId} con dati da iddatadb {$iddatadb}",
"commessaAfterPatch" => $commessaAfterPatch,
"totalCustomFieldsUpdated" => count($commessaCustomFields),
"fieldValues" => $fieldValues,
"logFile" => $logFile
]);
} catch (Exception $e) {
error_log("Patch Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
echo json_encode([
"success" => false,
"message" => "Patch failed: " . $e->getMessage(),
"logFile" => $logFile ?? 'Nessun log generato'
]);
}
+50
View File
@@ -0,0 +1,50 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
require_once __DIR__ . '/../../../vendor/autoload.php'; // aggiusta il path se serve
class Mailer
{
public static function send(array $to, string $subject, string $htmlBody, ?string $altBody = null): array
{
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = env('MAIL_HOST');
$mail->SMTPAuth = true;
$mail->Username = env('MAIL_USERNAME');
$mail->Password = env('MAIL_PASSWORD');
$mail->Port = (int) env('MAIL_PORT', 465);
$enc = env('MAIL_ENCRYPTION', 'ssl');
$mail->SMTPSecure = ($enc === 'tls')
? PHPMailer::ENCRYPTION_STARTTLS
: PHPMailer::ENCRYPTION_SMTPS;
$mail->CharSet = 'UTF-8';
$mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME', 'SmartTRF'));
foreach ($to as $addr) {
$addr = trim((string)$addr);
if ($addr !== '' && filter_var($addr, FILTER_VALIDATE_EMAIL)) {
$mail->addAddress($addr);
}
}
if (!$mail->getToAddresses()) {
return ['ok' => false, 'error' => 'Nessun destinatario valido'];
}
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $htmlBody;
$mail->AltBody = $altBody ?: strip_tags($htmlBody);
$mail->send();
return ['ok' => true, 'error' => null];
} catch (PHPMailerException $e) {
error_log('Mailer error: ' . $mail->ErrorInfo);
return ['ok' => false, 'error' => $mail->ErrorInfo];
}
}
}
@@ -24,12 +24,22 @@ 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;
}
private function authenticate()
private function authenticate($retryCount = 0, $maxRetries = 3)
{
$ch = curl_init("{$this->baseUrl}/api/authentication/authenticate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@@ -45,16 +55,22 @@ class VisualLimsApiClient
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_VERBOSE, true);
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'w') ?: fopen('php://stderr', 'w');
$log = fopen(__DIR__ . '/curl_auth_debug.log', 'a') ?: fopen('php://stderr', 'w');
curl_setopt($ch, CURLOPT_STDERR, $log);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
$log_message = date('Y-m-d H:i:s') . " - Auth attempt {$retryCount}: HTTP {$http_code}, Error: {$curl_error}, Response: " . substr($response, 0, 1000) . "\n";
fwrite($log, $log_message);
fclose($log);
curl_close($ch);
if ($response === false || $http_code != 200) {
if ($http_code === 400 && strpos($response, 'Cannot persist the object') !== false && $retryCount < $maxRetries) {
usleep(500000); // Ritardo di 500ms
return $this->authenticate($retryCount + 1, $maxRetries); // Riprova
}
throw new Exception("Autenticazione fallita: HTTP {$http_code}, Errore cURL: {$curl_error}, Risposta: " . substr($response, 0, 1000));
}
@@ -120,4 +136,158 @@ class VisualLimsApiClient
return $data;
}
public function post($endpoint, $payload)
{
$token = $this->getToken();
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Content-Type: application/json",
"Accept: application/json"
]);
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: {$curl_error}");
}
if ($http_code < 200 || $http_code >= 300) {
throw new Exception("POST fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
}
return json_decode($response, true);
}
public function patch($endpoint, $payload)
{
$token = $this->getToken();
$url = "{$this->baseUrl}/api/odata/{$endpoint}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Content-Type: application/json",
"Accept: application/json"
]);
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 PATCH: {$curl_error}");
}
if ($http_code < 200 || $http_code >= 300) {
throw new Exception("PATCH fallito: HTTP {$http_code}, Risposta: " . substr($response, 0, 1000));
}
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];
}
}
+716
View File
@@ -0,0 +1,716 @@
<?php
function binding_normalize_value($value): string
{
$s = (string) $value;
$s = str_replace(
["\xC2\xA0", "\xE2\x80\xAF", "\xE2\x80\x87", "\xE2\x80\x89", "\xE2\x80\x8A", "\xEF\xBB\xBF"],
' ',
$s
);
$s = preg_replace('/\s+/u', ' ', $s);
return mb_strtolower(trim((string) $s));
}
function binding_is_list_field(array $mapping): bool
{
return strcasecmp(trim((string) ($mapping['data_type'] ?? '')), 'SceltaMultipla') === 0;
}
function binding_get_lims_values(int $fieldId): array
{
static $memo = [];
if ($fieldId <= 0) {
return [];
}
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$cacheDir = dirname(__DIR__) . '/cache';
$cacheFile = $cacheDir . '/customfield_' . $fieldId . '.json';
try {
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 3600)) {
$values = json_decode(file_get_contents($cacheFile), true);
} else {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
$data = $api->get("CustomField($fieldId)?\$expand=CustomFieldsValues");
$values = $data['CustomFieldsValues'] ?? [];
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
file_put_contents($cacheFile, json_encode($values));
}
} catch (Throwable $e) {
error_log("binding_get_lims_values failed for field $fieldId: " . $e->getMessage());
$values = [];
}
if (!is_array($values)) {
$values = [];
}
return $memo[$fieldId] = $values;
}
function binding_lims_value_index(int $fieldId): array
{
static $memo = [];
if (array_key_exists($fieldId, $memo)) {
return $memo[$fieldId];
}
$index = [];
foreach (binding_get_lims_values($fieldId) as $v) {
$id = (int) ($v['IdCustomFieldsValue'] ?? 0);
if ($id > 0) {
$index[$id] = (string) ($v['Valore'] ?? '');
}
}
return $memo[$fieldId] = $index;
}
function binding_has_lims_values(int $fieldId): bool
{
return binding_get_lims_values($fieldId) !== [];
}
function binding_is_lims_id(int $fieldId, $value): bool
{
$raw = trim((string) $value);
if ($raw === '' || !ctype_digit($raw)) {
return false;
}
$index = binding_lims_value_index($fieldId);
return $index === [] ? false : isset($index[(int) $raw]);
}
function binding_auto_match(int $fieldId, string $sourceValue): ?array
{
$needle = binding_normalize_value($sourceValue);
if ($needle === '') {
return null;
}
$matches = [];
foreach (binding_get_lims_values($fieldId) as $v) {
if (binding_normalize_value($v['Valore'] ?? '') === $needle) {
$matches[] = [
'id' => (int) ($v['IdCustomFieldsValue'] ?? 0),
'text' => (string) ($v['Valore'] ?? ''),
];
}
}
return count($matches) === 1 && $matches[0]['id'] > 0 ? $matches[0] : null;
}
function binding_lookup(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): ?array
{
$stmt = $pdo->prepare(
"SELECT id, lims_value_id, lims_value, source_value
FROM lims_value_binding
WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?
LIMIT 1"
);
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_upsert(
PDO $pdo,
int $templateId,
int $mappingId,
int $fieldId,
string $sourceValue,
int $limsValueId,
string $limsValue,
?int $createdBy,
string $kind = 'custom'
): void {
$stmt = $pdo->prepare(
"INSERT INTO lims_value_binding
(template_id, binding_kind, mapping_id, field_id, source_value, source_key, lims_value_id, lims_value, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
lims_value_id = VALUES(lims_value_id),
lims_value = VALUES(lims_value),
field_id = VALUES(field_id),
template_id = VALUES(template_id)"
);
$stmt->execute([
$templateId,
$kind,
$mappingId,
$fieldId,
mb_substr(trim($sourceValue), 0, 500),
mb_substr(binding_normalize_value($sourceValue), 0, 500),
$limsValueId,
mb_substr($limsValue, 0, 500),
$createdBy,
]);
}
function binding_delete(PDO $pdo, int $mappingId, string $sourceValue, string $kind = 'custom'): int
{
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE binding_kind = ? AND mapping_id = ? AND source_key = ?");
$stmt->execute([$kind, $mappingId, binding_normalize_value($sourceValue)]);
return $stmt->rowCount();
}
function binding_delete_by_id(PDO $pdo, int $bindingId): int
{
$stmt = $pdo->prepare("DELETE FROM lims_value_binding WHERE id = ?");
$stmt->execute([$bindingId]);
return $stmt->rowCount();
}
function binding_suggest(PDO $pdo, int $fieldId, string $sourceValue, int $excludeMappingId): ?array
{
if ($fieldId <= 0) {
return null;
}
$stmt = $pdo->prepare(
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
FROM lims_value_binding b
LEFT JOIN excel_templates t ON t.id = b.template_id
WHERE b.binding_kind = 'custom' AND b.field_id = ? AND b.source_key = ? AND b.mapping_id <> ?
ORDER BY b.updated_at DESC
LIMIT 1"
);
$stmt->execute([$fieldId, binding_normalize_value($sourceValue), $excludeMappingId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_apply_to_details(PDO $pdo, int $mappingId, string $value, array $datadbIds): int
{
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$stmt = $pdo->prepare(
"UPDATE import_data_details
SET field_value = ?
WHERE mapping_id = ? AND id IN ($placeholders)"
);
$stmt->execute(array_merge([$value, $mappingId], $datadbIds));
return $stmt->rowCount();
}
function binding_find_column_index(string $column, array $columns)
{
$needle = binding_normalize_value($column);
if ($needle === '') {
return false;
}
foreach ($columns as $i => $c) {
if (binding_normalize_value($c) === $needle) {
return $i;
}
}
return false;
}
function binding_fixed_alias_map(): array
{
return [
'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',
];
}
function binding_fixed_column(string $key): ?string
{
return binding_fixed_alias_map()[$key] ?? null;
}
function binding_fixed_is_list(string $key): bool
{
return in_array($key, [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'ClienteFatturazione',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_is_date(string $key): bool
{
return $key === 'ConsegnaRichiesta';
}
function binding_fixed_auto_matchable(string $key): bool
{
return in_array($key, [
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService',
], true);
}
function binding_fixed_label(string $key): string
{
$labels = [
'AnagraficaCertestObject' => 'Anagrafica Certest Object',
'AnagraficaCertestService' => 'Anagrafica Certest Service',
'MoltiplicatorePrezzo' => 'Moltiplicatore Prezzo',
'ClienteResponsabile' => 'Cliente Responsabile',
'ClienteFornitore' => 'Cliente Fornitore',
'ClienteAnalisi' => 'Buyer',
'ClienteFatturazione' => 'Cliente Fatturazione',
'ConsegnaRichiesta' => 'Consegna Richiesta',
];
return $labels[$key] ?? $key;
}
function binding_parse_date($value): ?string
{
$raw = trim((string) $value);
if ($raw === '') {
return null;
}
foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'd.m.Y', 'Y/m/d', 'd/m/y', 'Y-m-d H:i:s'] as $format) {
$date = DateTime::createFromFormat($format, $raw);
if ($date && $date->format($format) === $raw) {
return $date->format('Y-m-d');
}
}
$ts = strtotime($raw);
return $ts ? date('Y-m-d', $ts) : null;
}
function binding_fixed_default(array $fixedMapping): string
{
$value = trim((string) ($fixedMapping['default_value'] ?? ''));
if ($value === '') {
return '';
}
$isDate = strcasecmp((string) ($fixedMapping['data_type'] ?? ''), 'DATE') === 0
|| binding_fixed_is_date((string) ($fixedMapping['fixed_field_key'] ?? ''));
if ($isDate && preg_match('/^today\s*(?:\+\s*(\d+))?$/i', $value, $m)) {
return date('Y-m-d', strtotime('+' . (int) ($m[1] ?? 0) . ' days'));
}
return $value;
}
function binding_cached_get($api, string $cacheFile, string $endpoint, int $ttl = 3600): array
{
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
$data = json_decode(file_get_contents($cacheFile), true);
if (is_array($data)) {
return $data;
}
}
$data = $api->get($endpoint);
if (is_array($data)) {
$dir = dirname($cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($cacheFile, json_encode($data));
}
return is_array($data) ? $data : [];
}
function binding_template_idclient(PDO $pdo, int $templateId): int
{
$stmt = $pdo->prepare("SELECT idclient FROM excel_templates WHERE id = ?");
$stmt->execute([$templateId]);
return (int) ($stmt->fetchColumn() ?: 0);
}
function binding_client_label(array $client): string
{
$name = trim((string) ($client['Nominativo'] ?? ''));
$code = trim((string) ($client['CodiceCliente'] ?? ''));
return $code !== '' ? ($name . ' - ' . $code) : $name;
}
function binding_get_fixed_values(PDO $pdo, string $fieldKey, int $templateId): array
{
static $memo = [];
$memoKey = $fieldKey . '|' . $templateId;
if (isset($memo[$memoKey])) {
return $memo[$memoKey];
}
$cacheDir = dirname(__DIR__) . '/cache';
$out = [];
try {
require_once __DIR__ . '/VisualLimsApiClient.class.php';
$api = VisualLimsApiClient::getInstance();
switch ($fieldKey) {
case 'MoltiplicatorePrezzo':
$data = binding_cached_get($api, "$cacheDir/moltiplicatori_prezzo.json", 'MoltiplicatorePrezzi');
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdMoltiplicatorePrezzo'] ?? 0), 'text' => (string) ($r['Descrizione'] ?? '')];
}
break;
case 'AnagraficaCertestObject':
case 'AnagraficaCertestService':
$file = $fieldKey === 'AnagraficaCertestObject'
? 'anagrafica_certest_object.json'
: 'anagrafica_certest_service.json';
$data = binding_cached_get($api, "$cacheDir/$file", $fieldKey);
foreach (($data['value'] ?? []) as $r) {
$code = trim((string) ($r['Codice'] ?? ''));
$name = (string) ($r['NomeAnagrafica'] ?? '');
$out[] = [
'id' => (int) ($r['IdAnagrafica'] ?? 0),
'text' => $name,
'aliases' => array_values(array_filter([$code, $code !== '' ? "$code - $name" : ''])),
];
}
break;
case 'ClienteResponsabile':
$idCliente = binding_template_idclient($pdo, $templateId);
if ($idCliente > 0) {
$data = binding_cached_get(
$api,
"$cacheDir/cliente_responsabili_$idCliente.json",
"Cliente($idCliente)?\$expand=Responsabili"
);
foreach (($data['Responsabili'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdClienteResponsabile'] ?? 0), 'text' => (string) ($r['Nominativo'] ?? '')];
}
}
break;
case 'ClienteFornitore':
case 'ClienteAnalisi':
case 'ClienteFatturazione':
$endpoint = 'Cliente?' . http_build_query([
'$select' => 'IdCliente,Nominativo,CodiceCliente',
'$orderby' => 'Nominativo asc',
]);
$data = binding_cached_get($api, "$cacheDir/clienti.json", $endpoint);
foreach (($data['value'] ?? []) as $r) {
$out[] = ['id' => (int) ($r['IdCliente'] ?? 0), 'text' => binding_client_label($r)];
}
break;
}
} catch (Throwable $e) {
error_log("binding_get_fixed_values($fieldKey) failed: " . $e->getMessage());
$out = [];
}
return $memo[$memoKey] = array_values(array_filter($out, fn($v) => $v['id'] > 0));
}
function binding_fixed_has_values(PDO $pdo, string $fieldKey, int $templateId): bool
{
return binding_get_fixed_values($pdo, $fieldKey, $templateId) !== [];
}
function binding_fixed_is_lims_id(PDO $pdo, string $fieldKey, int $templateId, $value): bool
{
$raw = trim((string) $value);
if ($raw === '' || !ctype_digit($raw)) {
return false;
}
foreach (binding_get_fixed_values($pdo, $fieldKey, $templateId) as $v) {
if ((int) $v['id'] === (int) $raw) {
return true;
}
}
return false;
}
function binding_auto_match_fixed(array $values, string $sourceValue): ?array
{
$needle = binding_normalize_value($sourceValue);
if ($needle === '') {
return null;
}
$matches = [];
foreach ($values as $v) {
$candidates = array_merge([$v['text'] ?? ''], $v['aliases'] ?? []);
foreach ($candidates as $candidate) {
if (binding_normalize_value($candidate) === $needle) {
$matches[] = $v;
break;
}
}
}
return count($matches) === 1 ? $matches[0] : null;
}
function binding_suggest_fixed(PDO $pdo, string $fieldKey, string $sourceValue, int $excludeMappingId): ?array
{
$stmt = $pdo->prepare(
"SELECT b.lims_value_id, b.lims_value, b.template_id, t.name AS template_name
FROM lims_value_binding b
JOIN template_fixed_mapping f ON f.id = b.mapping_id
LEFT JOIN excel_templates t ON t.id = b.template_id
WHERE b.binding_kind = 'fixed' AND f.fixed_field_key = ?
AND b.source_key = ? AND b.mapping_id <> ?
ORDER BY b.updated_at DESC
LIMIT 1"
);
$stmt->execute([$fieldKey, binding_normalize_value($sourceValue), $excludeMappingId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function binding_apply_to_datadb(PDO $pdo, string $fieldKey, $value, array $datadbIds): int
{
$column = binding_fixed_column($fieldKey);
if ($column === null) {
throw new InvalidArgumentException("Campo fisso sconosciuto: $fieldKey");
}
$datadbIds = array_values(array_filter(array_map('intval', $datadbIds)));
if (empty($datadbIds)) {
return 0;
}
$placeholders = implode(',', array_fill(0, count($datadbIds), '?'));
$stmt = $pdo->prepare("UPDATE datadb SET `$column` = ? WHERE iddatadb IN ($placeholders)");
$stmt->execute(array_merge([$value === '' ? null : $value], $datadbIds));
return $stmt->rowCount();
}
function binding_write_fixed_columns(PDO $pdo, int $iddatadb, array $valuesByKey): int
{
$map = binding_fixed_alias_map();
$sets = [];
$params = [];
foreach ($valuesByKey as $key => $value) {
if (!isset($map[$key]) || $value === null || $value === '') {
continue;
}
$sets[] = "`{$map[$key]}` = ?";
$params[] = $value;
}
if (empty($sets)) {
return 0;
}
$params[] = $iddatadb;
$stmt = $pdo->prepare("UPDATE datadb SET " . implode(', ', $sets) . " WHERE iddatadb = ?");
$stmt->execute($params);
return $stmt->rowCount();
}
function binding_resolve_for_import(
PDO $pdo,
array $mapping,
int $templateId,
string $sourceValue,
int $iddatadb,
?int $userId,
array &$saved,
array &$auto,
array &$pending
): string {
$mappingId = (int) $mapping['id'];
$fieldId = (int) $mapping['field_id'];
$key = 'cf:' . $mappingId . '|' . binding_normalize_value($sourceValue);
$collect = function (array &$bucket, array $extra = []) use ($key, $mapping, $mappingId, $fieldId, $sourceValue, $iddatadb) {
if (!isset($bucket[$key])) {
$bucket[$key] = array_merge([
'kind' => 'custom',
'mapping_id' => $mappingId,
'field_id' => $fieldId,
'field_label' => $mapping['field_label'] ?? ('Mapping ' . $mappingId),
'source_value' => $sourceValue,
'datadb_ids' => [],
], $extra);
}
$bucket[$key]['datadb_ids'][] = $iddatadb;
};
if (!binding_has_lims_values($fieldId)) {
return $sourceValue;
}
if (binding_is_lims_id($fieldId, $sourceValue)) {
return $sourceValue;
}
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue)) {
$collect($saved, [
'lims_value_id' => (int) $existing['lims_value_id'],
'lims_value' => (string) $existing['lims_value'],
]);
return (string) $existing['lims_value_id'];
}
if ($match = binding_auto_match($fieldId, $sourceValue)) {
binding_upsert($pdo, $templateId, $mappingId, $fieldId, $sourceValue, $match['id'], $match['text'], $userId);
$collect($auto, [
'lims_value_id' => $match['id'],
'lims_value' => $match['text'],
]);
return (string) $match['id'];
}
$collect($pending);
return $sourceValue;
}
function binding_resolve_fixed_for_import(
PDO $pdo,
array $fixedMapping,
int $templateId,
string $sourceValue,
int $iddatadb,
?int $userId,
array &$saved,
array &$auto,
array &$pending
): ?string {
$fieldKey = (string) $fixedMapping['fixed_field_key'];
$mappingId = (int) $fixedMapping['id'];
$key = 'fx:' . $mappingId . '|' . binding_normalize_value($sourceValue);
$collect = function (array &$bucket, array $extra = []) use ($key, $fieldKey, $mappingId, $sourceValue, $iddatadb) {
if (!isset($bucket[$key])) {
$bucket[$key] = array_merge([
'kind' => 'fixed',
'mapping_id' => $mappingId,
'fixed_field_key' => $fieldKey,
'field_id' => 0,
'field_label' => binding_fixed_label($fieldKey),
'source_value' => $sourceValue,
'datadb_ids' => [],
], $extra);
}
$bucket[$key]['datadb_ids'][] = $iddatadb;
};
if (binding_fixed_is_date($fieldKey)) {
return binding_parse_date($sourceValue);
}
if (!binding_fixed_is_list($fieldKey)) {
return $sourceValue;
}
if (!binding_fixed_has_values($pdo, $fieldKey, $templateId)) {
error_log("[BINDING] campo fisso $fieldKey: lista LIMS non disponibile, valore non scritto");
return null;
}
if (binding_fixed_is_lims_id($pdo, $fieldKey, $templateId, $sourceValue)) {
return (string) (int) $sourceValue;
}
if ($existing = binding_lookup($pdo, $mappingId, $sourceValue, 'fixed')) {
$collect($saved, [
'lims_value_id' => (int) $existing['lims_value_id'],
'lims_value' => (string) $existing['lims_value'],
]);
return (string) $existing['lims_value_id'];
}
if (binding_fixed_auto_matchable($fieldKey)) {
$match = binding_auto_match_fixed(binding_get_fixed_values($pdo, $fieldKey, $templateId), $sourceValue);
if ($match) {
binding_upsert($pdo, $templateId, $mappingId, 0, $sourceValue, (int) $match['id'], (string) $match['text'], $userId, 'fixed');
$collect($auto, [
'lims_value_id' => (int) $match['id'],
'lims_value' => (string) $match['text'],
]);
return (string) $match['id'];
}
}
$collect($pending);
return null;
}
function binding_merge_buckets(array $bucket, array $items): array
{
foreach ($items as $key => $item) {
if (!isset($bucket[$key])) {
$bucket[$key] = $item;
continue;
}
$bucket[$key]['datadb_ids'] = array_values(array_unique(array_merge(
$bucket[$key]['datadb_ids'] ?? [],
$item['datadb_ids'] ?? []
)));
}
return $bucket;
}
function binding_list_for_template(PDO $pdo, int $templateId): array
{
$stmt = $pdo->prepare(
"SELECT b.*,
COALESCE(m.field_label, f.fixed_field_key) AS field_label,
COALESCE(m.excel_column, f.excel_column) AS excel_column,
f.fixed_field_key
FROM lims_value_binding b
LEFT JOIN template_mapping m ON m.id = b.mapping_id AND b.binding_kind = 'custom'
LEFT JOIN template_fixed_mapping f ON f.id = b.mapping_id AND b.binding_kind = 'fixed'
WHERE b.template_id = ?
ORDER BY field_label ASC, b.source_value ASC"
);
$stmt->execute([$templateId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
-36
View File
@@ -1,36 +0,0 @@
* Trying 93.43.5.102:443...
* Connected to 93.43.5.102 (93.43.5.102) port 443
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN: server accepted h2
* Server certificate:
* subject: C=FR; ST=Île-de-France; O=Bureau Veritas; CN=bvcpsitaly-elims.it
* start date: Feb 17 00:00:00 2025 GMT
* expire date: Feb 17 23:59:59 2026 GMT
* issuer: C=US; O=Corporation Service Company; CN=Corporation Service Company RSA OV SSL CA
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://93.43.5.102/limsapi/api/authentication/authenticate
* [HTTP/2] [1] [:method: POST]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: 93.43.5.102]
* [HTTP/2] [1] [:path: /limsapi/api/authentication/authenticate]
* [HTTP/2] [1] [content-type: application/json]
* [HTTP/2] [1] [accept: application/json]
* [HTTP/2] [1] [content-length: 51]
> POST /limsapi/api/authentication/authenticate HTTP/2
Host: 93.43.5.102
Content-Type: application/json
Accept: application/json
Content-Length: 51
< HTTP/2 200
< cache-control: max-age=0
< content-type: application/json; charset=utf-8
< server: Microsoft-IIS/10.0
< strict-transport-security: max-age=2592000
< x-powered-by: ASP.NET
< x-content-type-options: nosniff
< date: Thu, 04 Sep 2025 12:59:20 GMT
<
* Connection #0 to host 93.43.5.102 left intact
@@ -1,35 +0,0 @@
* Trying 93.43.5.102:443...
* Connected to 93.43.5.102 (93.43.5.102) port 443
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN: server accepted h2
* Server certificate:
* subject: C=FR; ST=Île-de-France; O=Bureau Veritas; CN=bvcpsitaly-elims.it
* start date: Feb 17 00:00:00 2025 GMT
* expire date: Feb 17 23:59:59 2026 GMT
* issuer: C=US; O=Corporation Service Company; CN=Corporation Service Company RSA OV SSL CA
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://93.43.5.102/limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: 93.43.5.102]
* [HTTP/2] [1] [:path: /limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues]
* [HTTP/2] [1] [authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjQ5MiIsIlhhZlNlY3VyaXR5QXV0aFBhc3NlZCI6IlhhZlNlY3VyaXR5QXV0aFBhc3NlZCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJXZWJBcGlVc2VyIiwiWGFmU2VjdXJpdHkiOiJYYWZTZWN1cml0eSIsIlhhZkxvZ29uUGFyYW1zIjoicTFZS0xVNHQ4a3ZNVFZXeVVncFBUWElzeUFRSktPa29CU1FXRjVmbkY2VUF4Y3RUa3hJTE1rdUI0Z2FHU3JVQSIsImV4cCI6MTc1Njk5Nzk2MCwiaXNzIjoiTXkiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.zHug3m3GFz-uMMbRXi70DlHNfw0Islrxyy5vdJ7RKtA]
* [HTTP/2] [1] [accept: application/json]
> GET /limsapi/api/odata/CustomField(1083)?$expand=CustomFieldsValues HTTP/2
Host: 93.43.5.102
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjQ5MiIsIlhhZlNlY3VyaXR5QXV0aFBhc3NlZCI6IlhhZlNlY3VyaXR5QXV0aFBhc3NlZCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJXZWJBcGlVc2VyIiwiWGFmU2VjdXJpdHkiOiJYYWZTZWN1cml0eSIsIlhhZkxvZ29uUGFyYW1zIjoicTFZS0xVNHQ4a3ZNVFZXeVVncFBUWElzeUFRSktPa29CU1FXRjVmbkY2VUF4Y3RUa3hJTE1rdUI0Z2FHU3JVQSIsImV4cCI6MTc1Njk5Nzk2MCwiaXNzIjoiTXkiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.zHug3m3GFz-uMMbRXi70DlHNfw0Islrxyy5vdJ7RKtA
Accept: application/json
< HTTP/2 200
< cache-control: max-age=0
< content-type: application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8
< server: Microsoft-IIS/10.0
< strict-transport-security: max-age=2592000
< odata-version: 4.0
< x-powered-by: ASP.NET
< x-content-type-options: nosniff
< date: Thu, 04 Sep 2025 12:59:23 GMT
<
* Connection #0 to host 93.43.5.102 left intact
+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";
File diff suppressed because one or more lines are too long
+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
@@ -0,0 +1,105 @@
<?php
require_once dirname(__DIR__, 3) . '/vendor/autoload.php'; // Risale a root/vendor/
require_once dirname(__FILE__) . '/../class/VisualLimsApiClient.class.php'; // In root/public/userarea/class/
use Dotenv\Dotenv;
// Debug: Log the path where we expect the .env file
$envPath = dirname(__DIR__, 3);
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);
exit(1);
}
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome');
// 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'];
// 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);
exit(1);
}
try {
$api = VisualLimsApiClient::getInstance();
// Endpoint per recuperare le Matrici
$endpoint = 'Matrice';
// (Opzionale) aggiungi parametri
$options = []; // es. ['$top' => 100]
// 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_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Debug: Log the API response size
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - API response received, value count: ' . (isset($data['value']) ? count($data['value']) : 0) . PHP_EOL, FILE_APPEND);
// Salva il JSON in locale (opzionale, per debug)
file_put_contents(__DIR__ . '/matrici_response.json', json_encode($data, JSON_PRETTY_PRINT));
// Svuota la tabella (dump rapido)
$pdo->exec("TRUNCATE TABLE {$dbPrefix}matrici");
// Debug: Log after truncate
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Table truncated: ' . $dbPrefix . 'matrici' . PHP_EOL, FILE_APPEND);
// Prepara l'insert
$stmt = $pdo->prepare("
INSERT INTO {$dbPrefix}matrici (
IdMatrice, NomeMatriceTraduzione, DescrizioneTraduzione, MacroMatrice, NomeMatrice, Descrizione
) VALUES (
:IdMatrice, :NomeMatriceTraduzione, :DescrizioneTraduzione, :MacroMatrice, :NomeMatrice, :Descrizione
)
");
// Inserisci i dati
$insertedRows = 0;
if (isset($data['value']) && is_array($data['value'])) {
foreach ($data['value'] as $item) {
$stmt->execute([
':IdMatrice' => $item['IdMatrice'],
':NomeMatriceTraduzione' => $item['NomeMatriceTraduzione'],
':DescrizioneTraduzione' => $item['DescrizioneTraduzione'] ?? null,
':MacroMatrice' => $item['MacroMatrice'] ?? null,
':NomeMatrice' => $item['NomeMatrice'],
':Descrizione' => $item['Descrizione'] ?? null,
]);
$insertedRows++;
// Debug: Log each inserted row
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Inserted row with IdMatrice: ' . $item['IdMatrice'] . PHP_EOL, FILE_APPEND);
}
}
// Log successo
file_put_contents(__DIR__ . '/success_log.txt', date('Y-m-d H:i:s') . ' - Aggiornamento completato: ' . $insertedRows . ' record inseriti.' . PHP_EOL, FILE_APPEND);
} catch (Exception $e) {
file_put_contents(__DIR__ . '/error_log.txt', date('Y-m-d H:i:s') . ' - ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
exit(1);
}
exit(0); // Esci con successo per cron
+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";
+40
View File
@@ -0,0 +1,40 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/db-functions.php';
require_once __DIR__ . '/class/binding-functions.php';
include dirname(__DIR__) . '/../extra/auth.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
if (!Auth::check()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$bindingId = intval($_POST['binding_id'] ?? 0);
if ($bindingId <= 0) {
http_response_code(422);
echo json_encode(['success' => false, 'error' => 'Missing binding_id']);
exit;
}
try {
$pdo = DBHandlerSelect::getInstance()->getConnection();
$deleted = binding_delete_by_id($pdo, $bindingId);
echo json_encode(['success' => $deleted > 0, 'deleted' => $deleted]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
+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()
]);
}
+28
View File
@@ -0,0 +1,28 @@
<?php
header('Content-Type: application/json');
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$data = json_decode(file_get_contents('php://input'), true);
$partId = $data['part_id'] ?? null;
if (!$partId) {
echo json_encode(['success' => false, 'message' => 'ID parte mancante']);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM identification_parts WHERE id = :part_id");
$stmt->execute([':part_id' => $partId]);
$rowCount = $stmt->rowCount();
if ($rowCount > 0) {
echo json_encode(['success' => true, 'message' => 'Parte eliminata con successo']);
} else {
echo json_encode(['success' => false, 'message' => 'Nessuna parte trovata con ID ' . $partId]);
}
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nell\'eliminazione: ' . $e->getMessage()]);
}
Binary file not shown.
@@ -0,0 +1,23 @@
# Swagger Codegen Ignore
# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
@@ -0,0 +1 @@
3.0.34
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
<?php
include('include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
$iddatadb = (int)($_GET['iddatadb'] ?? 0);
if ($iddatadb <= 0) {
http_response_code(400);
exit('iddatadb mancante');
}
$stmt = $pdo->prepare("SELECT log_file, commessaweb FROM datadb WHERE iddatadb = :id LIMIT 1");
$stmt->execute(['id' => $iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || empty($row['log_file'])) {
http_response_code(404);
exit('Nessun log per questa commessa');
}
// solo basename: niente path traversal
$path = $logDir . basename($row['log_file']);
if (!is_file($path)) {
http_response_code(404);
exit('File di log non trovato: ' . basename($row['log_file']));
}
while (ob_get_level() > 0) {
ob_end_clean();
}
$label = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string)($row['commessaweb'] ?: $iddatadb));
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="log_' . $label . '_' . $iddatadb . '.log"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
+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);
}
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
2025-06-10 12:21:44 - Errore nel recupero dati: HTTP 404, Risposta:
2025-06-16 11:15:30 - Errore nel recupero dati: HTTP 404, Risposta: {"title":"Not Found","status":404,"detail":"Not Found","instance":"GET /api/odata/Rapporto(2523026)","errorCode":"d25cbd678"}
2025-06-16 11:34:59 - Autenticazione fallita: HTTP 0, Errore cURL: Failed to connect to 93.43.5.102 port 443 after 21033 ms: Couldn't connect to server, Risposta:
2025-06-16 11:42:03 - Errore nella richiesta: Recv failure: Connection was reset
2025-07-04 10:42:49 - 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"}
2025-07-04 10:44:13 - 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"}
2025-07-04 10:48:07 - 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"}
2025-08-19 16:29:25 - 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"}
2025-08-26 16:47:19 - 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:48:15 - 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:48:44 - 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: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
+363
View File
@@ -0,0 +1,363 @@
<?php
include('include/headscript.php');
$template_id = intval($_GET['id'] ?? 0);
if (!$template_id) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Template ID mancante"));
exit;
}
$user_id = $iduserlogin ?? 1;
$show_all_users = isset($_GET['all_users']) && $_GET['all_users'] == '1';
$allowedLimits = [20, 40, 60, 100];
$rawLimit = (int)($_GET['limit'] ?? 20);
$perPage = in_array($rawLimit, $allowedLimits) ? $rawLimit : 20;
$page = max(1, (int)($_GET['page'] ?? 1));
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$logDir = __DIR__ . '/logs/api/';
// Estrae un riassunto breve dell'errore dal file di log della commessa
function extractErrorSummary(string $logDir, ?string $logFile): string
{
if (empty($logFile)) return '';
$path = $logDir . basename($logFile);
if (!is_file($path)) return '';
$content = @file_get_contents($path);
if ($content === false || $content === '') return '';
// 1) prova col "detail" del JSON del 500
if (preg_match('/"detail"\s*:\s*"([^"]+)"/', $content, $m)) {
return trim(stripcslashes($m[1]));
}
// 2) fallback: ultima riga MESSAGE:
if (preg_match_all('/^MESSAGE:\s*(.+)$/m', $content, $m) && !empty($m[1])) {
return trim(end($m[1]));
}
return '';
}
// Query: solo le righe in errore per questo template
$userFilter = $show_all_users ? '' : 'AND d.user_id = ?';
$baseWhere = "WHERE d.templateid = ? AND d.status = 'x' {$userFilter}";
$baseParams = [$template_id];
if (!$show_all_users) $baseParams[] = $user_id;
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM datadb d {$baseWhere}");
$countStmt->execute($baseParams);
$totalRows = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalRows / $perPage));
if ($page > $totalPages) $page = $totalPages;
$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
{$baseWhere}
ORDER BY d.export_date DESC, d.iddatadb DESC
LIMIT {$perPage} OFFSET " . (($page - 1) * $perPage) . "
");
$stmt->execute($baseParams);
$rowsData = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.err-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.err-table th,
.err-table td {
padding: 8px 10px;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
}
.err-table th {
background: #f8f9fa;
text-align: left;
font-weight: 600;
color: #495057;
}
.err-table tr:hover {
background: #fff8f8;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
border-radius: 12px;
}
.status-x {
background: #dc3545;
color: #fff;
}
.err-msg {
color: #b02a37;
max-width: 420px;
white-space: normal;
word-break: break-word;
}
.commessa-code {
font-size: 0.8em;
color: #555;
display: block;
margin-top: 2px;
}
.action-btn {
padding: 5px 8px;
margin-right: 4px;
border: none;
border-radius: 5px;
cursor: pointer;
color: #fff;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.btn-resend {
background: #28a745;
}
.btn-log {
background: #17a2b8;
}
.btn-revert {
background: #fd7e14;
}
.pager-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 6px 14px;
font-size: 13px;
margin-bottom: 10px;
}
.pager-num,
.pager-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 6px;
border: 1px solid #ced4da;
border-radius: 4px;
text-decoration: none;
color: #495057;
margin-left: 4px;
}
.pager-num.active {
background: #0d6efd;
color: #fff;
border-color: #0d6efd;
}
</style>
<title>Export Errors - <?= 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="mb-3 d-flex align-items-center gap-2">
<a href="imported.php?id=<?= $template_id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $template_id ?>" class="btn btn-success me-2">To LIMS (l)</a>
<a href="errors.php?id=<?= $template_id ?>" class="btn btn-danger">Errors (x)</a>
<span class="ms-3">
<label class="form-check-label" style="font-size:13px; cursor:pointer;">
<?php $tp = $_GET;
unset($tp['all_users']);
$toggleBase = 'errors.php?' . http_build_query($tp); ?>
<input type="checkbox" class="form-check-input" <?= $show_all_users ? 'checked' : '' ?>
onchange="window.location.href='<?= htmlspecialchars($toggleBase) ?>' + (this.checked ? '&all_users=1' : '')">
Show all users
</label>
</span>
<span class="text-muted" style="font-size:12px;">(<?= count($rowsData) ?> of <?= $totalRows ?> errori<?= !$show_all_users ? ' — my records' : '' ?>)</span>
</div>
<?php if ($totalPages > 1):
$pageQuery = $_GET;
unset($pageQuery['page']);
$pageBase = 'errors.php?' . http_build_query($pageQuery); ?>
<div class="pager-bar">
<span>Pagina <?= $page ?> di <?= $totalPages ?> — <?= $totalRows ?> righe</span>
<span>
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
<a href="<?= htmlspecialchars($pageBase . '&page=' . $p) ?>" class="pager-num <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php endfor; ?>
</span>
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<span style="font-weight:600;"><i class="fas fa-triangle-exclamation" style="color:#dc3545;"></i> Export falliti verso il LIMS</span>
</div>
<div class="card-body">
<?php if (empty($rowsData)): ?>
<p class="text-muted mb-0">Nessun errore di export. 🎉</p>
<?php else: ?>
<div style="overflow-x:auto;">
<table class="err-table">
<thead>
<tr>
<th style="width:170px;">Azioni</th>
<th>Status</th>
<th>Client</th>
<th>Errore</th>
<th style="width:150px;">Data export</th>
<th>File import</th>
</tr>
</thead>
<tbody>
<?php foreach ($rowsData as $row):
$errMsg = extractErrorSummary($logDir, $row['log_file'] ?? null); ?>
<tr data-id="<?= (int)$row['iddatadb'] ?>">
<td>
<button type="button" class="action-btn btn-resend resend-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Reinvia al LIMS"><i class="fas fa-paper-plane"></i></button>
<?php if (!empty($row['log_file'])): ?>
<a href="download_logs.php?iddatadb=<?= (int)$row['iddatadb'] ?>" class="action-btn btn-log" title="Scarica log"><i class="fas fa-file-download"></i></a>
<?php endif; ?>
<button type="button" class="action-btn btn-revert revert-btn" data-iddatadb="<?= (int)$row['iddatadb'] ?>" title="Riporta a Imported"><i class="fas fa-rotate-left"></i></button>
</td>
<td>
<span class="status-badge status-x">Error</span>
<?php if (!empty($row['commessaweb'])): ?>
<span class="commessa-code"><?= htmlspecialchars($row['commessaweb']) ?></span>
<?php endif; ?>
</td>
<td><span class="client-text" data-client-id="<?= htmlspecialchars((string)($row['idclient'] ?? '')) ?>"><?= htmlspecialchars((string)($row['idclient'] ?? '')) ?></span></td>
<td class="err-msg"><?= htmlspecialchars($errMsg ?: '—') ?></td>
<td><?= htmlspecialchars((string)($row['export_date'] ?? '')) ?></td>
<td><?php if (!empty($row['filename_import'])): ?><a href="imported_trf/<?= htmlspecialchars($row['filename_import']) ?>" target="_blank">File</a><?php endif; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<?php include('include/footer.php'); ?>
</div>
<?php include('jsinclude.php'); ?>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
// Risoluzione nomi cliente (come in tolims.php)
(function() {
const ids = new Set();
document.querySelectorAll('.client-text').forEach(el => {
const id = el.dataset.clientId;
if (id) ids.add(id);
});
ids.forEach(id => {
fetch('search_clienti.php?id=' + encodeURIComponent(id))
.then(r => r.json())
.then(j => {
const it = (j.results || [])[0];
if (it) document.querySelectorAll('.client-text[data-client-id="' + id + '"]').forEach(el => el.textContent = it.text);
})
.catch(() => {});
});
})();
// Reinvia: richiama export_to_lims.php da capo (create incluso)
$(document).on('click', '.resend-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
if (!confirm('Reinviare questa commessa al LIMS? Verrà creata una nuova commessa nel LIMS.')) return;
const original = $btn.html();
$btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
$.ajax({
url: 'export_to_lims.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb
},
success: function(res) {
if (res && res.success) {
const $tr = $('tr[data-id="' + iddatadb + '"]');
$tr.css('background', '#d4edda');
setTimeout(() => $tr.fadeOut(300, () => $tr.remove()), 500);
} else {
alert('Ancora errore: ' + ((res && res.message) ? res.message : 'invio non riuscito'));
$btn.prop('disabled', false).html(original);
}
},
error: function() {
alert('Errore di rete durante il reinvio.');
$btn.prop('disabled', false).html(original);
}
});
});
// Revert a Imported (i)
$(document).on('click', '.revert-btn', function() {
const $btn = $(this);
const iddatadb = $btn.data('iddatadb');
if (!confirm('Riportare questo record a Imported (i)?')) return;
$.ajax({
url: 'revert_status.php',
method: 'POST',
dataType: 'json',
data: {
iddatadb: iddatadb,
status: 'i'
},
success: function(res) {
if (res && res.success) {
const $tr = $('tr[data-id="' + iddatadb + '"]');
$tr.fadeOut(300, () => $tr.remove());
} else {
alert('Errore: ' + ((res && res.message) ? res.message : 'operazione non riuscita'));
}
},
error: function() {
alert('Errore di rete.');
}
});
});
});
</script>
</body>
</html>
+836
View File
@@ -0,0 +1,836 @@
/**
* 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 escHtml(v) {
return String(v ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Badge "No price" con tooltip dei nomi analisi (vuoto se count = 0)
function noPriceBadgeHTML(count, names) {
const n = parseInt(count, 10) || 0;
if (n <= 0) return "";
const title = (names || []).join("\n");
return `<span class="badge bg-danger no-price-badge" title="${escHtml(title)}" style="margin-left:6px; font-size:0.85em; cursor:help; vertical-align:middle;"><i class="fas fa-exclamation-triangle"></i> No price (${n})</span>`;
}
window.noPriceBadgeHTML = noPriceBadgeHTML;
function getGridRow(iddatadb) {
return document.querySelector(`.grid-row[data-id="${iddatadb}"]`);
}
function getRowIndexByIddatadb(iddatadb) {
return (window.gridData || []).findIndex(
(r) => String(r.iddatadb) === String(iddatadb),
);
}
// Invia la mail di riepilogo per gli iddatadb processati (fire-and-forget).
// Una sola chiamata a fine export (singolo o batch).
function sendExportSummaryMail(iddatadbList) {
const ids = (iddatadbList || [])
.map((v) => parseInt(v, 10))
.filter(Boolean);
if (!ids.length) return;
const body = new URLSearchParams();
ids.forEach((id) => body.append("ids[]", id));
const tid = window.gridMeta && window.gridMeta.templateId;
if (tid) body.append("template_id", tid);
fetch("send_export_summary.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
})
.then((r) => r.json())
.then((res) => console.log("Summary mail:", res))
.catch((err) => console.warn("Summary mail failed:", err));
}
// ── 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;
window.gridData[idx].noPriceCount = data.noPriceCount || 0;
window.gridData[idx].noPriceAnalyses =
data.noPriceAnalyses || [];
}
// 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 npHtml = noPriceBadgeHTML(
data.noPriceCount,
data.noPriceAnalyses,
);
if (npHtml) cwSpan.insertAdjacentHTML("beforeend", npHtml);
}
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: ${escHtml(data.commessaweb)}` +
noPriceBadgeHTML(data.noPriceCount, data.noPriceAnalyses) +
(data.totalPhotos > 0 ? `<br>Foto: ${data.totalPhotos}` : "") +
(data.noPriceCount > 0
? `<div class="mt-2 small text-danger fw-semibold">Analisi senza prezzo:</div>` +
`<ul class="mb-0 small text-danger">` +
(data.noPriceAnalyses || [])
.map((n) => `<li>${escHtml(n)}</li>`)
.join("") +
`</ul>`
: "");
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);
// Mail di riepilogo per la singola commessa
sendExportSummaryMail([iddatadb]);
} 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;
const exportedList = []; // { commessaweb, noPriceCount, noPriceAnalyses }
const processedIds = []; // iddatadb processati (ok o errore) per la mail
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);
processedIds.push(iddatadb);
try {
const data = await sendExport(iddatadb, batchUuid);
processed++;
if (data.success) {
succeeded++;
exportedList.push({
commessaweb: data.commessaweb,
noPriceCount: data.noPriceCount || 0,
noPriceAnalyses: data.noPriceAnalyses || [],
});
} 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";
// Mail di riepilogo unica per tutto il batch processato
sendExportSummaryMail(processedIds);
// 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>`;
}
if (exportedList.length) {
const npCommesse = exportedList.filter(
(x) => x.noPriceCount > 0,
).length;
msgEl.innerHTML +=
`<hr class="my-2">` +
(npCommesse > 0
? `<div class="small text-danger fw-semibold mb-1"><i class="fas fa-exclamation-triangle"></i> ${npCommesse} commesse con analisi senza prezzo</div>`
: "") +
`<div class="small fw-semibold mb-1">Commesse esportate:</div>` +
`<ul class="mb-0 ps-3">` +
exportedList
.map(
(x) =>
`<li><span style="font-family:monospace;">${escHtml(x.commessaweb || "—")}</span>` +
noPriceBadgeHTML(
x.noPriceCount,
x.noPriceAnalyses,
) +
`</li>`,
)
.join("") +
`</ul>`;
}
const modalEl = document.getElementById("exportResponseModal");
new bootstrap.Modal(modalEl, { keyboard: false }).show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, {
once: true,
});
})();
}
})();
+800
View File
@@ -0,0 +1,800 @@
document.addEventListener("DOMContentLoaded", () => {
console.log("export_to_lims.js loaded");
const exportButtons = document.querySelectorAll(".export-lims-btn");
console.log(`Found ${exportButtons.length} export-lims-btn buttons`);
// Tracks the active confirm handler so it can be replaced on re-open
let pendingConfirmHandler = null;
let batchRunning = false;
// Expose for Save All to check
Object.defineProperty(window, "batchRunning", {
get: () => batchRunning,
});
// ── 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";
}
// ── Validation ───────────────────────────────────────────────────────────
/**
* Call the validation endpoint for an array of { iddatadb, index } objects.
* Returns the parsed JSON response.
*/
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();
}
/**
* Clear all validation-error highlights from the grid.
*/
function clearValidationErrors() {
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");
});
// Also clear batch-row-error that came from validation
clearAllRowErrors();
}
/**
* Highlight specific cells on a row based on validation errors.
* Each error has { field, message }.
* field can be: a data-col value, "parts", "field_label:SomeLabel", or null (row-level).
*/
function showValidationErrors(row, iddatadb, errors) {
row.classList.add("validation-row-error");
const messages = [];
errors.forEach((err) => {
messages.push(err.message);
if (!err.field) return; // row-level error, no specific cell
let cell = null;
if (err.field === "parts") {
// No specific cell to highlight, but we highlight the parts button area
// just add to messages
return;
} else if (err.field.startsWith("field_label:")) {
// Match by field_label text — find the header with this label, get its index
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) {
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-row="${rowIndex}"][data-index="${targetIndex}"]`);
}
}
} else {
// Direct data-col match
const rowIndex = row.querySelector(".grid-cell[data-row]")?.getAttribute("data-row");
if (rowIndex !== null) {
cell = row.querySelector(`.grid-cell[data-col="${err.field}"][data-row="${rowIndex}"]`);
}
}
if (cell) {
cell.classList.add("validation-error");
// Mark the input/select inside the cell
cell.querySelectorAll("input, select").forEach((el) => {
el.classList.add("input-validation-error");
});
// Add tooltip with error message
let tooltip = cell.querySelector(".validation-tooltip");
if (!tooltip) {
tooltip = document.createElement("div");
tooltip.className = "validation-tooltip";
cell.appendChild(tooltip);
}
tooltip.textContent = err.message;
}
});
// Show aggregated error on the row using existing mechanism
showRowError(row, iddatadb, messages.join("\n"));
}
/**
* Send a single export request and update the row UI on success.
* Returns the parsed JSON response.
*/
async function sendExport(iddatadb, gridRow, batchUuid = null) {
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 && gridRow) {
// Update status badge
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";
}
// Insert/update CommessaWeb code span
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;";
cwSpan.title = "CommessaWeb";
const hiddenInput = statusCell.querySelector(
'input[type="hidden"]',
);
hiddenInput
? statusCell.insertBefore(cwSpan, hiddenInput)
: statusCell.appendChild(cwSpan);
}
cwSpan.textContent = data.commessaweb;
}
// Disable export button for this row
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";
exportBtn.title = "Già esportato";
}
}
return data;
}
/**
* Show the result of a single export in the response modal.
*/
function showExportResult(data) {
const responseModalElement =
document.getElementById("exportResponseModal");
if (!responseModalElement) {
alert("Errore: Modale di risposta non trovato");
return;
}
const responseModal = new bootstrap.Modal(responseModalElement, {
keyboard: false,
});
const responseMessage = document.getElementById(
"exportResponseMessage",
);
if (data.success) {
responseMessage.innerHTML =
`${data.message.replace(/\n/g, "<br>")}` +
`<br>ID CommessaWeb: ${data.idcommessaweb}` +
`<br>Codice CommessaWeb: ${data.commessaweb}` +
(data.totalPhotos > 0
? `<br>Foto trovate: ${data.totalPhotos}`
: "");
document.getElementById("exportResponseModalLabel").textContent =
"Esportazione Completata";
} else {
responseMessage.textContent = `Errore durante la generazione dei payload: ${data.message}`;
document.getElementById("exportResponseModalLabel").textContent =
"Errore Esportazione";
}
responseModal.show();
responseModalElement.addEventListener(
"hidden.bs.modal",
cleanupBackdrop,
{ once: true },
);
}
// ── Row button helpers (disable/enable during batch) ────────────────────
function setRowExporting(row, active) {
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");
row.classList.add("batch-disabled");
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) {
row.classList.add("batch-row-error");
const btnCell = row.querySelector(".button-cell");
if (btnCell) {
// Remove existing error msg
const old = btnCell.querySelector(".batch-error-msg");
if (old) old.remove();
const errorEl = document.createElement("div");
errorEl.className = "batch-error-msg";
errorEl.textContent = "⚠ Errore — clicca per dettagli";
errorEl.addEventListener("click", () => {
document.getElementById("exportResponseMessage").innerHTML = message.replace(/\n/g, "<br>");
document.getElementById("exportResponseModalLabel").textContent = "Errore Validazione (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");
});
// Disable Actions dropdown
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 modal, then send ───────────────
function startExportConfirmFlow(iddatadb, btn) {
const gridRow = btn.closest(".grid-row");
const rowIndex = btn.dataset.row;
// Validate first
clearValidationErrors();
// Show validating state on the row
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: parseInt(rowIndex) }])
.then((validationData) => {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
if (!validationData.success) {
showExportResult({ success: false, message: validationData.message || "Errore di validazione" });
return;
}
const result = validationData.results[rowIndex];
if (result && !result.valid) {
// Show validation errors on the row
showValidationErrors(gridRow, iddatadb, result.errors);
return;
}
// Validation passed — show confirm modal
showConfirmAndExport(iddatadb, btn);
})
.catch((error) => {
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
console.error("Validation error:", error);
showExportResult({ success: false, message: "Errore di validazione: " + error.message });
});
}
function showConfirmAndExport(iddatadb, btn) {
const confirmModalElement =
document.getElementById("exportConfirmModal");
if (!confirmModalElement) {
alert("Errore: Modale di conferma non trovato");
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();
alert("Errore: Pulsante di conferma non trovato");
return;
}
const confirmHandler = async () => {
pendingConfirmHandler = null;
console.log(`Confirmed export for iddatadb: ${iddatadb}`);
confirmModal.hide();
const gridRow = btn.closest(".grid-row");
setRowExporting(gridRow, true);
try {
const data = await sendExport(iddatadb, gridRow);
console.log("Export response:", data);
// Stop spinner, fully restore row
setRowExporting(gridRow, false);
gridRow.classList.remove("batch-disabled");
if (!data.success) {
showRowError(gridRow, iddatadb, data.message || "Errore sconosciuto");
}
showExportResult(data);
} catch (error) {
console.error("Export error:", error);
// Stop spinner, fully restore row so user can retry
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 handler ────────────────────────────────────────────
exportButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (batchRunning) return;
const rowIndex = btn.dataset.row;
const iddatadb = btn.dataset.iddatadb;
console.log(
`Export to LIMS clicked for row ${rowIndex}, iddatadb: ${iddatadb}`,
);
const gridRow = btn.closest(".grid-row");
if (gridRow && gridRow.querySelector(".cell-changed")) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show();
document.getElementById("saveAndExportBtn").addEventListener(
"click",
() => {
unsavedModal.hide();
const saveBtn = gridRow.querySelector(".save-btn");
if (!saveBtn) return;
const observer = new MutationObserver(() => {
if (!gridRow.querySelector(".cell-changed")) {
observer.disconnect();
startExportConfirmFlow(iddatadb, btn);
}
});
observer.observe(gridRow, {
subtree: true,
attributes: true,
attributeFilter: ["class"],
});
saveBtn.click();
},
{ once: true },
);
return;
}
// No unsaved changes — go straight to validate + export confirm
startExportConfirmFlow(iddatadb, btn);
});
});
// ── Batch export (Export All) ───────────────────────────────────────────
const exportAllBtn = document.querySelector(".export-all-lims-btn");
if (!exportAllBtn) return;
let batchCancelled = false;
let pendingBatchConfirmHandler = null;
function collectEligibleRows() {
const allRows = document.querySelectorAll(".grid-row[data-id]");
const eligible = [];
allRows.forEach((row) => {
const statusBadge = row.querySelector(
'.grid-cell[data-col="status"] .status-badge',
);
if (statusBadge && !statusBadge.classList.contains("status-l")) {
const iddatadb = row.dataset.id;
if (iddatadb) {
eligible.push({ iddatadb, row });
}
}
});
return eligible;
}
/**
* Get the data-row index for a grid row element.
*/
function getRowIndex(row) {
const cell = row.querySelector(".grid-cell[data-row]");
return cell ? parseInt(cell.getAttribute("data-row")) : null;
}
function hasUnsavedChanges() {
return !!document.querySelector(".grid-row[data-id] .cell-changed");
}
/**
* Validate all eligible rows, show errors, and return only the valid ones.
*/
async function validateAndFilter(eligibleRows) {
const rowsToValidate = eligibleRows.map(({ iddatadb, row }) => ({
iddatadb: parseInt(iddatadb),
index: getRowIndex(row),
}));
const validationData = await validateRows(rowsToValidate);
if (!validationData.success) {
throw new Error(validationData.message || "Errore di validazione");
}
const validRows = [];
let invalidCount = 0;
for (const { iddatadb, row } of eligibleRows) {
const rowIdx = getRowIndex(row);
const result = validationData.results[rowIdx];
if (result && !result.valid) {
showValidationErrors(row, iddatadb, result.errors);
invalidCount++;
} else {
validRows.push({ iddatadb, row });
}
}
return { validRows, invalidCount };
}
function showValidationSpinner(show) {
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
if (show) {
bar.style.display = "";
statusEl.textContent = "Validazione in corso...";
cancelBtn.style.display = "none";
} else {
bar.style.display = "none";
cancelBtn.style.display = "";
}
}
function showBatchConfirm(eligibleRows) {
clearValidationErrors();
showValidationSpinner(true);
// Validate before showing confirm
validateAndFilter(eligibleRows)
.then(({ validRows, invalidCount }) => {
showValidationSpinner(false);
if (validRows.length === 0) {
document.getElementById("exportResponseMessage").innerHTML =
`Nessuna riga valida per l'esportazione.<br>` +
`<strong>${invalidCount}</strong> righe con errori di validazione.`;
document.getElementById("exportResponseModalLabel").textContent =
"Validazione Fallita";
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} escluse per errori di validazione)`;
}
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);
console.error("Batch validation error:", error);
document.getElementById("exportResponseMessage").textContent =
"Errore di validazione: " + error.message;
document.getElementById("exportResponseModalLabel").textContent =
"Errore Validazione";
new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
).show();
});
}
exportAllBtn.addEventListener("click", (e) => {
e.preventDefault();
if (batchRunning) return;
// Check unsaved changes first
if (hasUnsavedChanges()) {
const unsavedModal = new bootstrap.Modal(
document.getElementById("exportBatchUnsavedModal"),
{ keyboard: false },
);
unsavedModal.show();
document
.getElementById("batchSaveAndExportBtn")
.addEventListener(
"click",
() => {
unsavedModal.hide();
// Trigger Save All, then proceed
const saveAllEl =
document.querySelector(".save-all-btn");
if (!saveAllEl) return;
// Watch for all .cell-changed to disappear
const observer = new MutationObserver(() => {
if (!hasUnsavedChanges()) {
observer.disconnect();
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById(
"exportResponseMessage",
).textContent =
"Tutte le righe sono già state esportate al LIMS.";
document.getElementById(
"exportResponseModalLabel",
).textContent = "Export All";
new bootstrap.Modal(
document.getElementById(
"exportResponseModal",
),
{ keyboard: false },
).show();
return;
}
showBatchConfirm(eligibleRows);
}
});
observer.observe(
document.querySelector(".grid-container"),
{
subtree: true,
attributes: true,
attributeFilter: ["class"],
},
);
saveAllEl.click();
},
{ once: true },
);
return;
}
const eligibleRows = collectEligibleRows();
if (eligibleRows.length === 0) {
document.getElementById("exportResponseMessage").textContent =
"Tutte le righe sono già state esportate al LIMS.";
document.getElementById("exportResponseModalLabel").textContent =
"Export All";
const modal = new bootstrap.Modal(
document.getElementById("exportResponseModal"),
{ keyboard: false },
);
modal.show();
return;
}
showBatchConfirm(eligibleRows);
});
function startBatchExport(eligibleRows) {
batchCancelled = false;
batchRunning = true;
// Don't clear validation errors — they should stay visible for invalid rows
const batchUuid = crypto.randomUUID();
const total = eligibleRows.length;
let processed = 0;
let succeeded = 0;
let failed = 0;
// Disable all row buttons
disableAllRowButtons();
// Show inline status bar
const bar = document.getElementById("batchExportBar");
const statusEl = document.getElementById("batchExportStatus");
const cancelBtn = document.getElementById("exportBatchCancelBtn");
bar.style.display = "";
cancelBtn.disabled = false;
statusEl.textContent = `Esportazione 0 / ${total}...`;
// Cancel handler
cancelBtn.addEventListener(
"click",
() => {
batchCancelled = true;
statusEl.textContent =
"Annullamento... (attendi riga corrente)";
cancelBtn.disabled = true;
},
{ once: true },
);
(async () => {
for (let i = 0; i < eligibleRows.length; i++) {
if (batchCancelled) break;
const { iddatadb, row } = eligibleRows[i];
statusEl.textContent = `Esportazione ${processed + 1} / ${total} (id: ${iddatadb})...`;
// Highlight current row
setRowExporting(row, true);
try {
const data = await sendExport(iddatadb, row, batchUuid);
processed++;
if (data.success) {
succeeded++;
} else {
failed++;
showRowError(row, iddatadb, data.message || "Errore sconosciuto");
}
} catch (error) {
processed++;
failed++;
showRowError(row, iddatadb, error.message);
}
// Remove highlight from current row
setRowExporting(row, false);
}
// Finished
batchRunning = false;
enableAllRowButtons();
bar.style.display = "none";
// Show result modal
const msgEl = document.getElementById("exportResponseMessage");
const labelEl = document.getElementById("exportResponseModalLabel");
if (batchCancelled) {
labelEl.textContent = "Export All — Annullato";
msgEl.innerHTML =
`Esportate: <strong>${succeeded}</strong><br>` +
`Errori: <strong>${failed}</strong><br>` +
`Non processate: <strong>${total - processed}</strong>`;
} else if (failed === 0) {
labelEl.textContent = "Export All — Completato";
msgEl.innerHTML = `Tutte le <strong>${succeeded}</strong> righe esportate con successo.`;
} else {
labelEl.textContent = "Export All — Completato con errori";
msgEl.innerHTML =
`Esportate: <strong>${succeeded}</strong><br>` +
`Errori: <strong>${failed}</strong>`;
}
const modalEl = document.getElementById("exportResponseModal");
const modal = new bootstrap.Modal(modalEl, { keyboard: false });
modal.show();
modalEl.addEventListener("hidden.bs.modal", cleanupBackdrop, { once: true });
})();
}
});
+891
View File
@@ -0,0 +1,891 @@
<?php
require_once "class/VisualLimsApiClient.class.php";
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
header("Content-Type: application/json");
// 🔹 Configura directory log (creala se non esiste)
$logDir = __DIR__ . '/logs/api/';
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;
// File unico per questa commessa (nominato per iddatadb, che conosciamo sempre)
$iddatadbForLog = $_POST['iddatadb'] ?? 'unknown';
$commessaLogFile = $logDir . "commessa_{$iddatadbForLog}_" . time() . ".log";
// File di lotto: resta come INDICE (quali commesse nel lotto), non più tutti gli step
$batchIndexFile = $batchUuid ? $logDir . "batch_export_{$batchUuid}.log" : null;
// $writeLog ora appende sempre sul file unico della commessa, con header di step
$writeLog = function ($individualPath, $content, $stepLabel = null) use ($commessaLogFile) {
$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($commessaLogFile, $header . $content . "\n", FILE_APPEND);
};
// Helper per registrare una riga nell'indice di lotto
$writeBatchIndex = function ($line) use ($batchIndexFile) {
if ($batchIndexFile) {
file_put_contents($batchIndexFile, "[" . date('Y-m-d H:i:s') . "] " . $line . "\n", FILE_APPEND);
}
};
// 🔹 Funzione per validare e convertire date
function validateDate($value)
{
// Prova a validare come data (accetta formati comuni)
$date = DateTime::createFromFormat('Y-m-d', $value) ?: DateTime::createFromFormat('Y-m-d H:i:s', $value);
if ($date) {
return $date->format('Y-m-d\TH:i:sP'); // Formato ISO 8601
}
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, 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
LIMIT 1
");
$stmt->execute(['iddatadb' => $iddatadb]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
throw new Exception("No Cliente/Schema found for iddatadb {$iddatadb}");
}
$clienteId = (int) $result['clienteId'];
$schemaId = (int) $result['schemaId'];
$templateId = (int) $result['templateId'];
// 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 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_order,
m.is_addnote
FROM
import_data_details as idd
JOIN template_mapping m ON idd.mapping_id = m.id
WHERE idd.id = :iddatadb
");
$stmt->execute(['iddatadb' => $iddatadb]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$fieldValues = [];
$valueMap = [];
foreach ($rows as $row) {
$fieldValues[] = [
"IdCommesseCustomFields" => (int) $row['field_id'],
"Valore" => $row['field_value'],
"FieldLabel" => $row['field_label']
];
$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'] ?? ''));
});
// Valori "vuoti convenzionali" da NON includere nelle note.
// Il cliente usa "/" (e simili) come placeholder di campo vuoto.
$addnotePlaceholders = ['/', '-', 'n/a', 'na', 'n.a.', '//'];
foreach ($addnoteRows as $r) {
$val = trim((string)($r['field_value'] ?? ''));
if ($val === '') {
continue;
}
// Scarta i placeholder (confronto case-insensitive)
if (in_array(mb_strtolower($val), $addnotePlaceholders, true)) {
continue;
}
$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);
// 🔹 Initialize API client
$api = VisualLimsApiClient::getInstance();
// 🔹 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" => "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
$jsonPayload = json_encode($commessaWebPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContentStep5 = "curl --location --request POST '{$apiBaseUrl}CommessaWeb' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'";
$commessaWeb = $api->post("CommessaWeb", $commessaWebPayload);
$logContentStep5 .= "\n\nRESPONSE:\n" . json_encode($commessaWeb, JSON_PRETTY_PRINT);
// Salva log
$logFileStep5 = $logDir . "commessa_create_step5_" . $iddatadb . "_" . time() . ".txt";
$writeLog($logFileStep5, $logContentStep5, "STEP 5 - Create CommessaWeb (iddatadb={$iddatadb})");
$commessaId = $commessaWeb["IdCommessa"];
$commessaWebCode = substr($commessaWeb["CodiceCommessa"] ?? "TEST CommessaWeb", 0, 30); // Limite a 30 caratteri
// 🔹 STEP 6: Create Campioni (Samples) for each part
$campioni = [];
$logContentStep6 = "";
foreach ($parts as $index => $part) {
$matriceId = (int) ($part["idmatrice"] ?? 0);
if ($matriceId <= 0) {
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,
"SchemaCustomField" => $schemaId,
// "Riferimento" => $part["part_description"] ?? "",
"NoteWeb" => $noteWebValue,
"ConsegnaRichiesta" => !empty($part["dateexpiry"]) ? $part["dateexpiry"] : $consegnaRichiesta,
];
// Costruisci curl-like per questo campione
$jsonPayload = json_encode($campionePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContentStep6 .= "CAMPIONE #{$index}\n" .
"curl --location --request POST '{$apiBaseUrl}Campione' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'\n\n";
$campione = $api->post("Campione", $campionePayload);
$logContentStep6 .= "RESPONSE:\n" . json_encode($campione, JSON_PRETTY_PRINT) . "\n\n---\n";
$campione["PartNumber"] = $part["part_number"] ?? "";
$campione["Material"] = $part["material"] ?? "";
$campione["Color"] = $part["color"] ?? "";
$campione["Mix"] = $part["mix"] ?? "";
$campioni[] = $campione;
}
// Salva log per STEP 6
$logFileStep6 = $logDir . "commessa_{$commessaId}_campioni_step6_" . time() . ".txt";
$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, analysis_price
FROM identification_parts_analyses
WHERE iddatadb = :iddatadb
ORDER BY part_id, id
");
$stmt->execute(['iddatadb' => $iddatadb]);
$analysesRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Analisi senza prezzo (NULL o 0), una sola volta per recordkey
$noPriceAnalyses = [];
foreach ($analysesRows as $a) {
$price = $a['analysis_price'];
if ($price === null || (float)$price <= 0) {
$rk = (string)($a['analysis_recordkey'] ?? '');
$noPriceAnalyses[$rk] = $a['analysis_name'] ?: 'Unnamed analysis';
}
}
$noPriceAnalyses = array_values($noPriceAnalyses);
$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}");
// 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";
$writeLog($logFileGet, $logContentGet, "STEP 7 - GET CustomFields (commessa={$commessaId})");
// Prepara payload PATCH
$commessaCustomFields = [];
foreach ($commessaWithFields["CommesseCustomFields"] as $customField) {
$definitionId = (int) ($customField["CustomField"]["IdCustomField"] ?? 0);
$fieldId = (int) $customField["IdCommesseCustomFields"];
$currentValue = $customField["Valore"] ?? '';
$fieldType = $customField["CustomField"]["Tipo"] ?? '';
$newValue = isset($valueMap[$definitionId]) ? $valueMap[$definitionId] : $currentValue;
// Valida se il campo è di tipo Data
if ($fieldType === 'Data' && $newValue !== $currentValue) {
$newValue = formatDateToExport($newValue);
}
$commessaCustomFields[] = [
"IdCommesseCustomFields" => $fieldId,
"Valore" => $newValue
];
}
$logFileStep7 = null;
if (!empty($commessaCustomFields)) {
$updatePayload = ["CommesseCustomFields" => $commessaCustomFields];
// Logga payload e response
$jsonPayload = json_encode($updatePayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$logContentStep7 = "curl --location --request PATCH '{$apiBaseUrl}CommessaWeb({$commessaId})' \\\n" .
"--header 'Content-Type: application/json' \\\n" .
"--header 'Authorization: Bearer ••••••' \\\n" .
"--data '{$jsonPayload}'";
$patchResponse = $api->patch("CommessaWeb({$commessaId})", $updatePayload);
$logContentStep7 .= "\n\nRESPONSE:\n" . json_encode($patchResponse, JSON_PRETTY_PRINT);
$logFileStep7 = $logDir . "commessa_{$commessaId}_update_step7_" . time() . ".txt";
$writeLog($logFileStep7, $logContentStep7, "STEP 7 - PATCH CustomFields (commessa={$commessaId})");
}
}
// 🔹 STEP 8: Update datadb with idcommessaweb, commessaweb, log_file and export date
// NB: lo status 'l' viene impostato SOLO dopo invio+import ok (vedi più sotto),
// così una riga rimasta indietro non risulta "To LIMS" per errore.
$stmt = $pdo->prepare("
UPDATE datadb
SET idcommessaweb = :idcommessaweb, commessaweb = :commessaweb,
log_file = :log_file, export_date = NOW()
WHERE iddatadb = :iddatadb
");
$stmt->execute([
'idcommessaweb' => $commessaId,
'commessaweb' => $commessaWebCode,
'log_file' => basename($commessaLogFile),
'iddatadb' => $iddatadb
]);
// 🔹 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);
$logFileStep9 = $logDir . "commessa_{$commessaId}_send_step9_" . time() . ".txt";
$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)";
$commessaAfterPatch = $api->get("CommessaWeb(" . $commessaId . ")?\$expand=" . $expand);
// Logga il GET di controllo
$logContentStep10 = "curl --location --request GET '{$apiBaseUrl}CommessaWeb({$commessaId})?\$expand=CommesseCustomFields(\$expand=CustomField)' \\\n" .
"--header 'Authorization: Bearer ••••••'\n\n" .
"RESPONSE:\n" . json_encode($commessaAfterPatch, JSON_PRETTY_PRINT);
$logFileStep10 = $logDir . "commessa_{$commessaId}_get_step10_" . time() . ".txt";
$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
]);
}
// Indice di lotto: esito OK di questa commessa
$writeBatchIndex("OK iddatadb={$iddatadb} commessa={$commessaId} codice={$finalCodiceCommessa} log=" . basename($commessaLogFile));
// 🔹 STEP 11: Prepare final response
$finalCommessa = [
"Cliente" => $clienteId,
"SchemaCustomField" => $schemaId,
"Richiedente" => $commessaWeb["Richiedente"] ?? "Web Import",
"Descrizione" => $commessaWeb["Descrizione"] ?? "",
"CommesseCustomFields" => $commessaAfterPatch["CommesseCustomFields"] ?? [],
"Campioni" => $campioni,
"Inviata" => 0 // Non inviato, come richiesto
];
echo json_encode([
"success" => true,
"idcommessaweb" => $commessaId,
"commessaweb" => $finalCodiceCommessa ?: $commessaWebCode,
"commessaWeb" => $finalCommessa,
"commessaWebApiResponse" => $commessaWeb, // Incluso per debug
"totalCampioni" => count($campioni),
"totalCustomFields" => count($commessaAfterPatch["CommesseCustomFields"] ?? []),
"totalPhotos" => count($photos),
"totalAnalyses" => $totalAnalyses,
"addedAnalyses" => $addedAnalyses,
"failedAnalyses" => $failedAnalyses,
"noPriceCount" => count($noPriceAnalyses),
"noPriceAnalyses" => $noPriceAnalyses,
"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) {
// --- LOG ERRORE DEDICATO (accanto agli altri log in logs/api/) ---
$errIddatadb = $iddatadb ?? 'unknown';
$errCommessaId = $commessaId ?? null;
$errContent = "TIMESTAMP: " . date('Y-m-d H:i:s') . "\n";
$errContent .= "iddatadb: {$errIddatadb}\n";
$errContent .= "IdCommessa: " . ($errCommessaId ?? 'non ancora creata') . "\n";
$errContent .= "MESSAGE: " . $e->getMessage() . "\n\n";
$errContent .= "TRACE:\n" . $e->getTraceAsString() . "\n";
// Appendi l'errore in coda al file unico della commessa (se esiste),
// così resta tutto in un posto solo.
if (isset($commessaLogFile)) {
$header = "\n" . str_repeat("=", 60) . "\n[ERROR] " . date('Y-m-d H:i:s') . "\n" . str_repeat("=", 60) . "\n";
@file_put_contents($commessaLogFile, $header . $errContent . "\n", FILE_APPEND);
$errFileToSave = basename($commessaLogFile);
} else {
// fallback: file d'errore dedicato
$errName = "commessa_ERROR_{$errIddatadb}_" . time() . ".txt";
@file_put_contents($logDir . $errName, $errContent);
$errFileToSave = $errName;
}
// Segna la riga come errore ('x') e registra il log_file, senza lasciarla 'l'
if ($errIddatadb !== 'unknown') {
try {
$up = $pdo->prepare("UPDATE datadb SET status = 'x', log_file = :log_file WHERE iddatadb = :iddatadb");
$up->execute(['log_file' => $errFileToSave, 'iddatadb' => $errIddatadb]);
} catch (Exception $ignore) { /* non bloccare la risposta d'errore */
}
}
// Indice di lotto: esito KO di questa commessa
if (isset($writeBatchIndex)) {
$writeBatchIndex("FAIL iddatadb={$errIddatadb} commessa=" . ($errCommessaId ?? '-') . " log={$errFileToSave} :: " . $e->getMessage());
}
error_log("LIMS Export Error: " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
echo json_encode([
"success" => false,
"message" => "Export failed: " . $e->getMessage(),
"errorLog" => $logDir . $errFileToSave,
"logFiles" => [
"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
]
]);
}
+400
View File
@@ -0,0 +1,400 @@
<?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 CAST(d.excelrow AS UNSIGNED) ASC, 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,90 @@
<?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) < 7200)) { // 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'] ?? [];
// Escludi le analisi con NominativoCliente compilato
$analisi = array_values(array_filter($analisi, function ($item) {
return empty(trim((string)($item['NominativoCliente'] ?? '')));
}));
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()]);
}
+89 -11
View File
@@ -1,24 +1,102 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Torna al livello di public per trovare vendor/
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json');
// Disabilita la visualizzazione degli errori PHP per evitare output HTML
// Disable PHP error display
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$data = $api->get("Cliente"); // Recupera i clienti
$api = VisualLimsApiClient::getInstance(); // also loads dotenv
// Salva la risposta in un file per debug
file_put_contents(__DIR__ . '/clienti_response.json', json_encode($data));
// 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;
}
echo json_encode($data);
// 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) < 7200)) {
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);
echo json_encode([
'error' => $e->getMessage()
]);
$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);
}
+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) < 7200)) {
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) < 7200)) {
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);
}
+39
View File
@@ -0,0 +1,39 @@
<?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();
// ID della CommessaWeb specifica (cambialo di volta in volta)
$id = 529435; // TODO: Cambia questo valore con l'ID desiderato
// Endpoint per recuperare la CommessaWeb specifica con espansione dello schema custom
$endpoint = "CommessaWeb({$id})";
// Opzioni per l'espansione: includi OrderCustomFields per ottenere i campi custom dello schema assegnato all'ordine
$options = ['$expand' => 'OrderCustomFields'];
// 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_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/commessaweb_schema_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()]);
}
+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) < 7200)) {
$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) < 7200)) { // 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()]);
}
+46
View File
@@ -0,0 +1,46 @@
<?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; }
header('Content-Type: application/json');
try {
// 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]));
}
// 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);
echo json_encode(['success' => true, 'value' => $macroMatrici]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+36
View File
@@ -0,0 +1,36 @@
<?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 Matrici
$endpoint = 'Matrice';
// (Opzionale) aggiungi parametri, ad esempio $top per limitare i risultati
$options = []; // oppure ad esempio: ['$top' => 100]
// 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_url.txt', $full_url . PHP_EOL, FILE_APPEND);
// Chiamata API
$data = $api->get($endpoint, $options);
// Salva il JSON in locale
file_put_contents(__DIR__ . '/matrici_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()]);
}
+51
View File
@@ -0,0 +1,51 @@
<?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) < 7200)) {
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()]);
}
+59
View File
@@ -0,0 +1,59 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; // Risale a root/vendor/
use Dotenv\Dotenv;
// 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);
}
// 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'];
// 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 le matrici, includendo MacroMatrice, escludendo quelle che iniziano con '*' e ordinandole
$query = "SELECT IdMatrice, NomeMatrice, MacroMatrice FROM {$dbPrefix}matrici WHERE NomeMatrice NOT LIKE '*%' ORDER BY NomeMatrice ASC";
$stmt = $pdo->prepare($query);
$stmt->execute();
$matrici = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Debug: Log del numero di matrici recuperate
file_put_contents(__DIR__ . '/debug_log.txt', date('Y-m-d H:i:s') . ' - Retrieved ' . count($matrici) . ' matrices from database' . PHP_EOL, FILE_APPEND);
// Restituisci risposta JSON
echo json_encode(['success' => true, 'value' => $matrici]);
} 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 matrici: ' . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'message' => 'Errore nel recupero delle matrici: ' . $e->getMessage()]);
exit(1);
}

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