소스 검색

first commit

Roberto Santini 1 개월 전
커밋
062e120f54
100개의 변경된 파일12962개의 추가작업 그리고 0개의 파일을 삭제
  1. 18
    0
      .editorconfig
  2. 98
    0
      .env.example
  3. 2
    0
      .eslintignore
  4. 24
    0
      .eslintrc.json
  5. 11
    0
      .gitattributes
  6. 47
    0
      .gitignore
  7. 2
    0
      .npmrc
  8. 127
    0
      .php-cs-fixer.php
  9. 3
    0
      .prettierignore
  10. 29
    0
      .prettierrc.json
  11. 4
    0
      .stylelintignore
  12. 133
    0
      .stylelintrc.json
  13. BIN
      Editor.zip
  14. 1079
    0
      Eventi.skipper
  15. 846
    0
      GestioneManutenzioni.skipper
  16. 66
    0
      README.md
  17. 1
    0
      Untitled
  18. 0
    0
      aimon_api
  19. 35
    0
      app/Actions/Fortify/CreateNewUser.php
  20. 18
    0
      app/Actions/Fortify/PasswordValidationRules.php
  21. 29
    0
      app/Actions/Fortify/ResetUserPassword.php
  22. 32
    0
      app/Actions/Fortify/UpdateUserPassword.php
  23. 56
    0
      app/Actions/Fortify/UpdateUserProfileInformation.php
  24. 19
    0
      app/Actions/Jetstream/DeleteUser.php
  25. 75
    0
      app/Console/Commands/GeneraSegnalazioniContratto.php
  26. 36
    0
      app/Console/Commands/NumeraTicket.php
  27. 85
    0
      app/DataTables/AziendaDataTable.php
  28. 144
    0
      app/DataTables/ConfiguraSegnalazioneDataTable.php
  29. 136
    0
      app/DataTables/ConfiguraSegnalazioneDataTableEditor.php
  30. 115
    0
      app/DataTables/ContrattoServizioDataTable.php
  31. 121
    0
      app/DataTables/ContrattoServizioDataTableEditor.php
  32. 89
    0
      app/DataTables/ContrattoServizioIndexDataTable.php
  33. 78
    0
      app/DataTables/ContrattoTemplateDataTable.php
  34. 244
    0
      app/DataTables/CostoSegnalazioneDataTable.php
  35. 88
    0
      app/DataTables/CostoSegnalazioneDataTableEditor.php
  36. 122
    0
      app/DataTables/GruppoDataTable.php
  37. 92
    0
      app/DataTables/GruppoDataTableEditor.php
  38. 83
    0
      app/DataTables/OffertaDataTable.php
  39. 84
    0
      app/DataTables/OrdineLavoroDataTable.php
  40. 76
    0
      app/DataTables/ProdottoDataTable.php
  41. 70
    0
      app/DataTables/ProdottoDataTableEditor.php
  42. 85
    0
      app/DataTables/PromoCodeDataTable.php
  43. 339
    0
      app/DataTables/SegnalazioneDataTable.php
  44. 119
    0
      app/DataTables/SegnalazioneDataTableEditor.php
  45. 215
    0
      app/DataTables/TempoImpiegatoDataTable.php
  46. 99
    0
      app/DataTables/TempoImpiegatoDataTableEditor.php
  47. 135
    0
      app/DataTables/UserDataTable.php
  48. 127
    0
      app/DataTables/UserDataTableEditor.php
  49. 35
    0
      app/Events/NuovaSegnalazione.php
  50. 311
    0
      app/Helpers/Helpers.php
  51. 137
    0
      app/Http/Controllers/AcquistoServizioFirmaController.php
  52. 577
    0
      app/Http/Controllers/AcquistoServizioOperatoreController.php
  53. 247
    0
      app/Http/Controllers/AcquistoServizioPublicController.php
  54. 101
    0
      app/Http/Controllers/AllegatoAnnotazioneController.php
  55. 112
    0
      app/Http/Controllers/AnnotazioniController.php
  56. 140
    0
      app/Http/Controllers/AziendaController.php
  57. 70
    0
      app/Http/Controllers/ComuneController.php
  58. 87
    0
      app/Http/Controllers/ConfigController.php
  59. 70
    0
      app/Http/Controllers/ConfigNotificheController.php
  60. 31
    0
      app/Http/Controllers/ConfiguraSegnalazioneController.php
  61. 339
    0
      app/Http/Controllers/ContrattoServizioController.php
  62. 38
    0
      app/Http/Controllers/ContrattoServizioTodoController.php
  63. 228
    0
      app/Http/Controllers/ContrattoTemplateController.php
  64. 8
    0
      app/Http/Controllers/Controller.php
  65. 47
    0
      app/Http/Controllers/CostoSegnalazioneController.php
  66. 40
    0
      app/Http/Controllers/GruppoController.php
  67. 282
    0
      app/Http/Controllers/HomePageController.php
  68. 70
    0
      app/Http/Controllers/NazioneController.php
  69. 426
    0
      app/Http/Controllers/OffertaController.php
  70. 222
    0
      app/Http/Controllers/OrdineLavoroController.php
  71. 215
    0
      app/Http/Controllers/ProdottoController.php
  72. 123
    0
      app/Http/Controllers/PromoCodeController.php
  73. 317
    0
      app/Http/Controllers/RolePermissionsController.php
  74. 947
    0
      app/Http/Controllers/SegnalazioneController.php
  75. 41
    0
      app/Http/Controllers/ShortUrlController.php
  76. 61
    0
      app/Http/Controllers/TempoImpiegatoController.php
  77. 453
    0
      app/Http/Controllers/UserController.php
  78. 25
    0
      app/Http/Middleware/LocaleMiddleware.php
  79. 43
    0
      app/Listeners/InviaNotificaSegnalazione.php
  80. 54
    0
      app/Livewire/Profile/LogoutOtherBrowserSessionsForm.php
  81. 108
    0
      app/Models/AbstractModels/AbstractAcquistoServizio.php
  82. 61
    0
      app/Models/AbstractModels/AbstractAllegatoAnnotazione.php
  83. 78
    0
      app/Models/AbstractModels/AbstractAnagrafica.php
  84. 95
    0
      app/Models/AbstractModels/AbstractAnnotazione.php
  85. 71
    0
      app/Models/AbstractModels/AbstractComune.php
  86. 72
    0
      app/Models/AbstractModels/AbstractConfig.php
  87. 61
    0
      app/Models/AbstractModels/AbstractConfigNotifiche.php
  88. 68
    0
      app/Models/AbstractModels/AbstractConfiguraSegnalazione.php
  89. 105
    0
      app/Models/AbstractModels/AbstractContrattoServizio.php
  90. 81
    0
      app/Models/AbstractModels/AbstractContrattoServizioRiga.php
  91. 105
    0
      app/Models/AbstractModels/AbstractContrattoServizioTodo.php
  92. 84
    0
      app/Models/AbstractModels/AbstractContrattoTemplate.php
  93. 81
    0
      app/Models/AbstractModels/AbstractContrattoTemplateRiga.php
  94. 94
    0
      app/Models/AbstractModels/AbstractContrattoTemplateTodo.php
  95. 62
    0
      app/Models/AbstractModels/AbstractGruppo.php
  96. 73
    0
      app/Models/AbstractModels/AbstractLuogo.php
  97. 62
    0
      app/Models/AbstractModels/AbstractNazione.php
  98. 83
    0
      app/Models/AbstractModels/AbstractOfferta.php
  99. 85
    0
      app/Models/AbstractModels/AbstractOffertaRiga.php
  100. 0
    0
      app/Models/AbstractModels/AbstractOrdineLavoro.php

+ 18
- 0
.editorconfig 파일 보기

@@ -0,0 +1,18 @@
1
+root = true
2
+
3
+[*]
4
+charset = utf-8
5
+end_of_line = lf
6
+indent_size = 2
7
+indent_style = space
8
+insert_final_newline = true
9
+trim_trailing_whitespace = true
10
+
11
+[*.md]
12
+trim_trailing_whitespace = false
13
+
14
+[*.{yml,yaml}]
15
+indent_size = 2
16
+
17
+[docker-compose.yml]
18
+indent_size = 4

+ 98
- 0
.env.example 파일 보기

@@ -0,0 +1,98 @@
1
+APP_NAME=Laravel
2
+APP_ENV=local
3
+APP_KEY=
4
+APP_DEBUG=true
5
+APP_URL=http://localhost
6
+
7
+APP_LOCALE=en
8
+APP_FALLBACK_LOCALE=en
9
+APP_FAKER_LOCALE=en_US
10
+PHONE_LOCALE=IT
11
+
12
+APP_MAINTENANCE_DRIVER=file
13
+# APP_MAINTENANCE_STORE=database
14
+
15
+PHP_CLI_SERVER_WORKERS=4
16
+
17
+BCRYPT_ROUNDS=12
18
+
19
+LOG_CHANNEL=stack
20
+LOG_STACK=single
21
+LOG_DEPRECATIONS_CHANNEL=null
22
+LOG_LEVEL=debug
23
+
24
+DB_CONNECTION=sqlite
25
+# DB_HOST=127.0.0.1
26
+# DB_PORT=3306
27
+# DB_DATABASE=laravel
28
+# DB_USERNAME=root
29
+# DB_PASSWORD=
30
+
31
+SESSION_DRIVER=database
32
+SESSION_LIFETIME=120
33
+SESSION_ENCRYPT=false
34
+SESSION_PATH=/
35
+SESSION_DOMAIN=null
36
+
37
+BROADCAST_CONNECTION=log
38
+FILESYSTEM_DISK=local
39
+QUEUE_CONNECTION=database
40
+
41
+CACHE_STORE=database
42
+# CACHE_PREFIX=
43
+
44
+MEMCACHED_HOST=127.0.0.1
45
+
46
+REDIS_CLIENT=phpredis
47
+REDIS_HOST=127.0.0.1
48
+REDIS_PASSWORD=null
49
+REDIS_PORT=6379
50
+
51
+MAIL_MAILER=log
52
+MAIL_SCHEME=null
53
+MAIL_HOST=127.0.0.1
54
+MAIL_PORT=2525
55
+MAIL_USERNAME=null
56
+MAIL_PASSWORD=null
57
+MAIL_FROM_ADDRESS="hello@example.com"
58
+MAIL_FROM_NAME="${APP_NAME}"
59
+
60
+AWS_ACCESS_KEY_ID=
61
+AWS_SECRET_ACCESS_KEY=
62
+AWS_DEFAULT_REGION=us-east-1
63
+AWS_BUCKET=
64
+AWS_USE_PATH_STYLE_ENDPOINT=false
65
+
66
+VITE_APP_NAME="${APP_NAME}"
67
+
68
+STRIPE_KEY=
69
+STRIPE_SECRET=
70
+STRIPE_WEBHOOK_SECRET=
71
+
72
+PAYPAL_CLIENT_ID=
73
+PAYPAL_SECRET=
74
+PAYPAL_MODE=sandbox
75
+
76
+ACQUISTO_DOMICILIO_EURO=15
77
+
78
+# Provider SMS per firma consegna: aimon | skebby
79
+SMS_PROVIDER=aimon
80
+
81
+# Aimon SMS
82
+AIMON_LOGIN=
83
+AIMON_PASSWORD=
84
+AIMON_ID_API=3916
85
+AIMON_SENDER=ElephanTech
86
+# AIMON_SEND_URL=https://secure.apisms.it/http/send_sms
87
+
88
+# Skebby REST API
89
+# Attenzione: SKEBBY_PASSWORD deve essere la "password API" (impostazioni account),
90
+# non la password di accesso al pannello. Skebby risponde 404 se le credenziali sono errate.
91
+SKEBBY_USERNAME=
92
+SKEBBY_PASSWORD=
93
+# Oppure token fissi dal pannello (salta login):
94
+# SKEBBY_USER_KEY=
95
+# SKEBBY_ACCESS_TOKEN=
96
+SKEBBY_SENDER=
97
+SKEBBY_MESSAGE_TYPE=GP
98
+# SKEBBY_BASE_URL=https://api.skebby.it/API/v1.0/REST

+ 2
- 0
.eslintignore 파일 보기

@@ -0,0 +1,2 @@
1
+**/*.*
2
+!resources/assets/vendor/js/*.js

+ 24
- 0
.eslintrc.json 파일 보기

@@ -0,0 +1,24 @@
1
+{
2
+  "env": {
3
+    "browser": true,
4
+    "commonjs": true,
5
+    "es6": true,
6
+    "jquery": true
7
+  },
8
+  "plugins": ["prettier"],
9
+  "extends": ["airbnb-base", "plugin:prettier/recommended"],
10
+  "rules": {
11
+    "prettier/prettier": "error",
12
+    "no-underscore-dangle": "off",
13
+    "semi": ["error", "never"],
14
+    "arrow-parens": ["error", "as-needed"],
15
+    "no-param-reassign": "off",
16
+    "no-plusplus": [
17
+      "error",
18
+      {
19
+        "allowForLoopAfterthoughts": true
20
+      }
21
+    ],
22
+    "no-cond-assign": ["error", "except-parens"]
23
+  }
24
+}

+ 11
- 0
.gitattributes 파일 보기

@@ -0,0 +1,11 @@
1
+* text=auto eol=lf
2
+
3
+*.blade.php diff=html
4
+*.css diff=css
5
+*.html diff=html
6
+*.md diff=markdown
7
+*.php diff=php
8
+
9
+/.github export-ignore
10
+CHANGELOG.md export-ignore
11
+.styleci.yml export-ignore

+ 47
- 0
.gitignore 파일 보기

@@ -0,0 +1,47 @@
1
+/.phpunit.cache
2
+/node_modules
3
+/public/build
4
+/public/hot
5
+/public/storage
6
+/storage/*.key
7
+/storage/pail
8
+/vendor
9
+
10
+.env
11
+.env.backup
12
+.env.production
13
+.phpactor.json
14
+.phpunit.result.cache
15
+
16
+docker-compose.override.yml
17
+
18
+Homestead.json
19
+Homestead.yaml
20
+npm-debug.log
21
+yarn-error.log
22
+/auth.json
23
+/.fleet
24
+/.idea
25
+/.nova
26
+/.vscode
27
+/.zed
28
+
29
+!/public/assets
30
+!/public/assets/*
31
+/resources/assets/vendor/fonts/flags/**
32
+/resources/assets/vendor/fonts/fontawesome/**
33
+/public/css
34
+/public/js
35
+/public/mix-manifest.json
36
+# /public/assets/*
37
+# But not these files...
38
+# !/public/assets/json
39
+# /public/assets/json/*
40
+# !/public/assets/json/search-vertical.json
41
+# !/public/assets/json/search-horizontal.json
42
+
43
+/resources/assets/vendor/fonts/flags/**
44
+/resources/assets/vendor/fonts/fontawesome/**
45
+
46
+# Except for 'template-customizer.js' inside 'resources/assets/vendor/js/'
47
+!/resources/assets/vendor/js/template-customizer.js

+ 2
- 0
.npmrc 파일 보기

@@ -0,0 +1,2 @@
1
+@datatables.net:registry=https://npm.datatables.net/
2
+//npm.datatables.net/:_authToken=45B0E017CC327B09B34FD9FFAA8400AA0251AE9B13A2ED66F1AE5FA27978438F

+ 127
- 0
.php-cs-fixer.php 파일 보기

@@ -0,0 +1,127 @@
1
+<?php
2
+return (new PhpCsFixer\Config())
3
+    ->setRules(
4
+    [
5
+    'array_syntax' => ['syntax' => 'short'],
6
+    'binary_operator_spaces' => [
7
+        'default' => 'single_space',
8
+        'operators' => ['=>' => null],
9
+    ],
10
+    'blank_line_after_namespace' => true,
11
+    'blank_line_after_opening_tag' => true,
12
+    'blank_line_before_statement' => [
13
+        'statements' => ['return'],
14
+    ],
15
+    'cast_spaces' => true,
16
+    'class_attributes_separation' => [
17
+        'elements' =>['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'none', 'case' => 'none']
18
+    ],
19
+    'class_definition' => true,
20
+    'concat_space' => [
21
+        'spacing' => 'one',
22
+    ],
23
+    'declare_equal_normalize' => true,
24
+    'elseif' => true,
25
+    'encoding' => true,
26
+    'full_opening_tag' => true,
27
+    'fully_qualified_strict_types' => true,
28
+    'function_declaration' => true,
29
+    'type_declaration_spaces' => true,
30
+    'heredoc_to_nowdoc' => true,
31
+    'include' => true,
32
+    'increment_style' => ['style' => 'post'],
33
+    'indentation_type' => true,
34
+    'linebreak_after_opening_tag' => true,
35
+    'line_ending' => true,
36
+    'lowercase_cast' => true,
37
+    'constant_case' => true,
38
+    'lowercase_keywords' => true,
39
+    'lowercase_static_reference' => true,
40
+    'magic_method_casing' => true,
41
+    'magic_constant_casing' => true,
42
+    'method_argument_space' => true,
43
+    'native_function_casing' => true,
44
+    'no_alias_functions' => true,
45
+    'no_blank_lines_after_class_opening' => true,
46
+    'no_blank_lines_after_phpdoc' => true,
47
+    'no_closing_tag' => true,
48
+    'no_empty_phpdoc' => true,
49
+    'no_empty_statement' => true,
50
+    'no_leading_import_slash' => true,
51
+    'no_leading_namespace_whitespace' => true,
52
+    'no_mixed_echo_print' => [
53
+        'use' => 'echo',
54
+    ],
55
+    'no_multiline_whitespace_around_double_arrow' => true,
56
+    'multiline_whitespace_before_semicolons' => [
57
+        'strategy' => 'no_multi_line',
58
+    ],
59
+    'no_short_bool_cast' => true,
60
+    'no_singleline_whitespace_before_semicolons' => true,
61
+    'no_spaces_after_function_name' => true,
62
+    'no_spaces_around_offset' => true,
63
+    'spaces_inside_parentheses' => true,
64
+    'no_trailing_comma_in_singleline' => true,
65
+    'no_trailing_whitespace' => true,
66
+    'no_trailing_whitespace_in_comment' => true,
67
+    'no_unneeded_control_parentheses' => true,
68
+    'no_unreachable_default_argument_value' => true,
69
+    'no_useless_return' => true,
70
+    'no_whitespace_before_comma_in_array' => true,
71
+    'no_whitespace_in_blank_line' => true,
72
+    'normalize_index_brace' => true,
73
+    'not_operator_with_successor_space' => false,
74
+    'object_operator_without_whitespace' => true,
75
+    'ordered_imports' => ['sort_algorithm' => 'alpha'],
76
+    'phpdoc_indent' => true,
77
+    'general_phpdoc_tag_rename' => true,
78
+    'phpdoc_inline_tag_normalizer' => true,
79
+    'phpdoc_tag_type' => true,
80
+    'phpdoc_no_access' => true,
81
+    'phpdoc_no_package' => true,
82
+    'phpdoc_no_useless_inheritdoc' => true,
83
+    'phpdoc_scalar' => true,
84
+    'phpdoc_single_line_var_spacing' => true,
85
+    'phpdoc_summary' => true,
86
+    'phpdoc_to_comment' => true,
87
+    'phpdoc_trim' => true,
88
+    'phpdoc_types' => true,
89
+    'phpdoc_var_without_name' => true,
90
+    'psr_autoloading' => true,
91
+    'self_accessor' => true,
92
+    'short_scalar_cast' => true,
93
+    'simplified_null_return' => false,
94
+    'single_blank_line_at_eof' => true,
95
+    'blank_lines_before_namespace' => true,
96
+    'single_class_element_per_statement' => true,
97
+    'single_import_per_statement' => true,
98
+    'single_line_after_imports' => true,
99
+    'single_line_comment_style' => [
100
+        'comment_types' => ['hash'],
101
+    ],
102
+    'space_after_semicolon' => true,
103
+    'standardize_not_equals' => true,
104
+    'switch_case_semicolon_to_colon' => true,
105
+    'switch_case_space' => true,
106
+    'ternary_operator_spaces' => true,
107
+    'trailing_comma_in_multiline' => true,
108
+    'trim_array_spaces' => true,
109
+    'unary_operator_spaces' => true,
110
+    'visibility_required' => [
111
+        'elements' => ['method', 'property'],
112
+    ],
113
+    'whitespace_after_comma_in_array' => true,
114
+    'no_unused_imports' => true,
115
+])
116
+    ->setFinder(
117
+        PhpCsFixer\Finder::create()
118
+            ->name('*.php')
119
+    ->notName('*.blade.php')
120
+    ->ignoreDotFiles(true)
121
+    ->ignoreVCS(true)
122
+            ->exclude('vendor')
123
+            ->exclude('node_modules')
124
+            ->exclude('storage')
125
+            ->exclude('bootstrap/cache')
126
+            ->exclude('.git')
127
+    );

+ 3
- 0
.prettierignore 파일 보기

@@ -0,0 +1,3 @@
1
+dist
2
+node_modules
3
+build

+ 29
- 0
.prettierrc.json 파일 보기

@@ -0,0 +1,29 @@
1
+{
2
+  "arrowParens": "avoid",
3
+  "bracketSpacing": true,
4
+  "bracketSameLine": true,
5
+  "htmlWhitespaceSensitivity": "css",
6
+  "insertPragma": false,
7
+  "jsxSingleQuote": true,
8
+  "printWidth": 120,
9
+  "proseWrap": "preserve",
10
+  "quoteProps": "as-needed",
11
+  "requirePragma": false,
12
+  "semi": true,
13
+  "singleQuote": true,
14
+  "tabWidth": 2,
15
+  "trailingComma": "none",
16
+  "useTabs": false,
17
+  "endOfLine": "lf",
18
+  "embeddedLanguageFormatting": "auto",
19
+  "overrides": [
20
+    {
21
+      "files": [
22
+        "resources/assets/vendor/js/*.js"
23
+      ],
24
+      "options": {
25
+        "semi": false
26
+      }
27
+    }
28
+  ]
29
+}

+ 4
- 0
.stylelintignore 파일 보기

@@ -0,0 +1,4 @@
1
+dist
2
+node_modules
3
+build
4
+_temp/

+ 133
- 0
.stylelintrc.json 파일 보기

@@ -0,0 +1,133 @@
1
+{
2
+  "extends": [
3
+    "stylelint-config-standard-scss",
4
+    "stylelint-config-idiomatic-order",
5
+    "@stylistic/stylelint-config"
6
+  ],
7
+  "plugins": [
8
+    "stylelint-use-logical-spec",
9
+    "@stylistic/stylelint-plugin"
10
+  ],
11
+  "rules": {
12
+    "alpha-value-notation": null,
13
+    "at-rule-empty-line-before": null,
14
+    "block-no-empty": null,
15
+    "color-function-notation": null,
16
+    "color-named": "never",
17
+    "custom-property-empty-line-before": null,
18
+    "custom-property-pattern": null,
19
+    "declaration-block-no-redundant-longhand-properties": null,
20
+    "declaration-empty-line-before": null,
21
+    "declaration-no-important": null,
22
+    "font-family-no-missing-generic-family-keyword": [
23
+      true,
24
+      {
25
+        "ignoreFontFamilies": [
26
+          "boxicons",
27
+          "tabler-icons",
28
+          "remix-icons"
29
+        ]
30
+      }
31
+    ],
32
+    "font-weight-notation": [
33
+      "numeric",
34
+      {
35
+        "ignore": [
36
+          "relative"
37
+        ]
38
+      }
39
+    ],
40
+    "function-url-no-scheme-relative": true,
41
+    "liberty/use-logical-spec": true,
42
+    "media-feature-range-notation": null,
43
+    "media-query-no-invalid": null,
44
+    "no-descending-specificity": null,
45
+    "no-invalid-double-slash-comments": true,
46
+    "no-invalid-position-at-import-rule": null,
47
+    "number-max-precision": null,
48
+    "rule-empty-line-before": null,
49
+    "selector-class-pattern": null,
50
+    "selector-id-pattern": null,
51
+    "selector-max-attribute": 2,
52
+    "selector-max-id": 1,
53
+    "selector-max-specificity": null,
54
+    "selector-not-notation": null,
55
+    "scss/at-extend-no-missing-placeholder": null,
56
+    "scss/at-function-named-arguments": "never",
57
+    "scss/at-if-closing-brace-newline-after": null,
58
+    "scss/at-if-closing-brace-space-after": null,
59
+    "scss/at-if-no-null": null,
60
+    "scss/at-mixin-pattern": null,
61
+    "scss/at-mixin-argumentless-call-parentheses": "always",
62
+    "scss/at-rule-conditional-no-parentheses": null,
63
+    "scss/comment-no-empty": null,
64
+    "scss/dimension-no-non-numeric-values": true,
65
+    "scss/dollar-variable-empty-line-before": null,
66
+    "scss/dollar-variable-pattern": null,
67
+    "scss/double-slash-comment-empty-line-before": null,
68
+    "scss/double-slash-comment-whitespace-inside": null,
69
+    "scss/function-quote-no-quoted-strings-inside": null,
70
+    "scss/media-feature-value-dollar-variable": null,
71
+    "scss/no-global-function-names": null,
72
+    "@stylistic/at-rule-name-space-after": "always",
73
+    "@stylistic/at-rule-semicolon-space-before": "never",
74
+    "@stylistic/block-closing-brace-empty-line-before": null,
75
+    "@stylistic/block-closing-brace-newline-after": [
76
+      "always",
77
+      {
78
+        "ignoreAtRules": [
79
+          "if",
80
+          "else"
81
+        ]
82
+      }
83
+    ],
84
+    "@stylistic/block-opening-brace-space-before": null,
85
+    "@stylistic/declaration-block-semicolon-newline-before": "never-multi-line",
86
+    "@stylistic/indentation": 2,
87
+    "@stylistic/max-empty-lines": 2,
88
+    "@stylistic/max-line-length": [
89
+      220,
90
+      {
91
+        "ignore": "comments"
92
+      }
93
+    ],
94
+    "@stylistic/no-eol-whitespace": true,
95
+    "@stylistic/number-leading-zero": "never",
96
+    "@stylistic/selector-list-comma-newline-before": "never-multi-line",
97
+    "@stylistic/selector-list-comma-space-after": "always-single-line",
98
+    "@stylistic/selector-list-comma-space-before": "never-single-line",
99
+    "@stylistic/unicode-bom": "never"
100
+  },
101
+  "overrides": [
102
+    {
103
+      "files": [
104
+        "**/_bootstrap-extended/**/*.scss"
105
+      ],
106
+      "rules": {
107
+        "declaration-property-value-disallowed-list": {
108
+          "border": "none",
109
+          "outline": "none"
110
+        },
111
+        "function-disallowed-list": [
112
+          "lighten",
113
+          "darken"
114
+        ],
115
+        "property-disallowed-list": [
116
+          "border-radius",
117
+          "border-top-left-radius",
118
+          "border-top-right-radius",
119
+          "border-bottom-right-radius",
120
+          "border-bottom-left-radius",
121
+          "transition"
122
+        ],
123
+        "scss/dollar-variable-default": [
124
+          true,
125
+          {
126
+            "ignore": "local"
127
+          }
128
+        ],
129
+        "scss/selector-no-union-class-name": true
130
+      }
131
+    }
132
+  ]
133
+}

BIN
Editor.zip 파일 보기


+ 1079
- 0
Eventi.skipper
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 846
- 0
GestioneManutenzioni.skipper 파일 보기

@@ -0,0 +1,846 @@
1
+<?xml version="1.0"?>
2
+<skipper version="3.2.25.1702" mvc="Laravel" orm="Laravel" name="MyAzienda" uuid="3072e02c-7554-4faa-a6bb-1bdf47958cd4">
3
+  <orm-attributes>
4
+    <attribute name="migrations-path">database/migrations</attribute>
5
+  </orm-attributes>
6
+  <module name="\Application module" local-name="Application module" namespace="\App\Models" local-namespace="App\Models" export-format="Laravel" export-path="." uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72">
7
+    <entity name="\App\Models\User" local-name="User" namespace="\App\Models" uuid="56185713-1439-4bd9-81df-6d5339d066ce">
8
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="0a6f5cb1-6847-45b2-9241-8ab815e1c380"/>
9
+      <field name="nome" type="string" uuid="7146ddab-7bf0-4c4a-9cb9-0a80eedba7fd"/>
10
+      <field name="cognome" type="string" uuid="5d3e9adc-9d0e-4b7c-abef-2b50ec57333c"/>
11
+      <field name="email" type="string" uuid="097ab2c7-9646-43ff-bc82-8cc0c277d909"/>
12
+      <field name="telefono" type="string" uuid="2cb16ee6-fd81-4981-95be-d88191274a0d"/>
13
+      <field name="password" type="string" uuid="ca5b3607-4149-4aa9-9fe9-ed3f9ea4b79b"/>
14
+      <field name="azienda_id" type="bigInteger" unsigned="true" uuid="a71fea94-0e25-4722-a9f4-a764b739a498"/>
15
+      <field name="email_verified_at" type="timestamp" uuid="e9cfc935-4b99-4bc8-be16-c582001b7cf4"/>
16
+      <field name="remember_token" type="string" uuid="43a000b7-9f98-4fbd-a88d-c6ea908cb2cc"/>
17
+      <field name="api_token" type="string" uuid="e1c61394-f57e-4941-aeb7-04fb368733cd"/>
18
+      <field name="two_factor_secret" type="text" uuid="9f263de8-d827-4458-abac-08dc31740853"/>
19
+      <field name="two_factor_recovery_codes" type="text" uuid="07ddc14d-0638-4998-9c0f-61e6b4c275ef"/>
20
+      <field name="two_factor_confirmed_at" type="timestamp" uuid="7e1530aa-7374-4465-af8d-0aed05196cf6"/>
21
+      <field name="is_gruppo" type="boolean" default="0" uuid="704eb3e5-b120-4303-b675-f3b7e051b24d"/>
22
+      <field name="is_azienda" type="boolean" default="0" uuid="2e3ab400-2d56-48bd-b868-74e5494168cd"/>
23
+      <field name="label_gruppo" type="string" uuid="b575155d-c744-4622-92bb-d28a36710267"/>
24
+      <field name="componenti_gruppo" type="json" uuid="c26ef028-e3f3-4dfe-b949-397da060e974"/>
25
+      <field name="ragione_sociale" type="string" uuid="ab3e9eee-6cc3-4b04-923d-c9eaeb067f3a"/>
26
+      <field name="piva" type="string" uuid="24b53519-f7ed-4605-97ed-0fe8007aec6d"/>
27
+      <field name="cod_fiscale" type="string" uuid="54f8f89b-8f1d-4350-b6be-f3d442bd097b"/>
28
+      <field name="indirizzo" type="string" uuid="a904fce5-68f9-49c5-9c4b-32cedf9bb6bb"/>
29
+      <field name="note" type="text" uuid="75bbae31-8850-4bd7-ad84-264a0a9ef52d"/>
30
+      <field name="comune_id" type="bigInteger" unsigned="true" uuid="519bbe2d-e5b8-49c0-b964-08644a0c119f"/>
31
+      <field name="created_at" type="timestamp" uuid="40919a29-47f6-4516-9c3d-917ed2c9afe6"/>
32
+      <field name="updated_at" type="timestamp" uuid="ef88ffd6-4374-4238-99c9-9cc9596a093e"/>
33
+      <orm-attributes>
34
+        <attribute name="table">users</attribute>
35
+        <attribute name="fillableAll">as-fillable-all</attribute>
36
+        <attribute name="extends">\Illuminate\Foundation\Auth\User</attribute>
37
+      </orm-attributes>
38
+    </entity>
39
+    <association from="\App\Models\User" to="\App\Models\Comune" caption="Comune Users" owner-alias="users" inverse-alias="comune" many-owner="true" many-inverse="false" uuid="8399970c-2d0b-403e-996b-e86248cb3bc5">
40
+      <association-field from="comune_id" to="id" uuid="f66c0c1c-a961-4b58-b5b9-46c97265ead5"/>
41
+      <orm-attributes>
42
+        <attribute name="onDelete">set null</attribute>
43
+      </orm-attributes>
44
+    </association>
45
+    <association from="\App\Models\User" to="\App\Models\User" caption="User Users" owner-alias="utentiAzienda" inverse-alias="azienda" many-owner="true" many-inverse="false" uuid="787d90ec-5676-4749-9b36-cea959c6793b">
46
+      <association-field from="azienda_id" to="id" uuid="867d2980-d54c-4b5c-8ab6-c07f0783c368"/>
47
+      <orm-attributes>
48
+        <attribute name="onDelete">set null</attribute>
49
+      </orm-attributes>
50
+    </association>
51
+    <entity name="\App\Models\Segnalazione" local-name="Segnalazione" namespace="\App\Models" uuid="b21d156e-828b-49fe-87a8-659eb8411896">
52
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="d15505d1-a284-4cf1-94b0-70d0f5788243"/>
53
+      <field name="user_id" description="chi ha fatto la segnalazione" type="bigInteger" unsigned="true" uuid="ecd5a597-63db-4120-8e3f-0c4b6fff198d"/>
54
+      <field name="assegnata_a_id" description="a chi &#xE8; assegnata la segnalazione" type="bigInteger" unsigned="true" uuid="9157dc7c-305b-4227-8b05-3992650c5cfc"/>
55
+      <field name="azienda_id" type="bigInteger" unsigned="true" uuid="76454f4a-a34f-43c3-9ae6-e6cbaedcf788"/>
56
+      <field name="contratto_servizio_id" type="bigInteger" unsigned="true" uuid="b214ffbc-1d19-4585-8ba6-40f9481d9267"/>
57
+      <field name="contratto_servizio_todo_id" type="bigInteger" unsigned="true" uuid="e483a339-f493-4b94-8f93-a2b661231ee5"/>
58
+      <field name="ordine_lavoro_id" type="bigInteger" unsigned="true" uuid="3cef7379-2ba5-4b50-9da8-6afcf2c66341"/>
59
+      <field name="acquisto_servizio_id" type="bigInteger" unsigned="true" uuid="22a1f5dd-b956-4ee7-bf73-062e66d3a5b0"/>
60
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="7158b094-0bec-4edb-914e-2457f0424463"/>
61
+      <field name="numero" type="integer" uuid="992bbdcc-c8a3-49a9-a95d-c28287148b38"/>
62
+      <field name="titolo" type="string" uuid="93e92649-f52f-4db5-baad-a4abf8e20f62"/>
63
+      <field name="stato" type="string" uuid="86938116-9b87-4051-bc45-3009fe9cbf83"/>
64
+      <field name="priorita" type="string" uuid="47ea6f64-0734-42ae-9374-bfe268ac2fca"/>
65
+      <field name="tipo" type="string" uuid="16f71b45-d45e-490a-9985-f19bed3c4d1c"/>
66
+      <field name="token" type="string" uuid="e5bd4274-35d3-4bff-9643-31c0b84ef421"/>
67
+      <field name="meta" type="json" uuid="feb6065e-dfd6-47d6-a5bb-1d54c5d99050"/>
68
+      <field name="origine" type="string" uuid="330da781-7ebb-4a50-b6fd-b626f954735f"/>
69
+      <field name="tempo_stimato" type="decimal" size="10" uuid="91880b18-1a17-4b31-85e5-61ae81e6aba2">
70
+        <orm-attributes>
71
+          <attribute name="decimal">2</attribute>
72
+        </orm-attributes>
73
+      </field>
74
+      <field name="created_at" type="timestamp" uuid="d0040cc6-db11-4587-beed-bf4313681d57"/>
75
+      <field name="updated_at" type="timestamp" uuid="3a3d74a0-b87a-40b2-aa1d-691d9ff66856"/>
76
+      <orm-attributes>
77
+        <attribute name="fillableAll">as-fillable-all</attribute>
78
+        <attribute name="table">segnalazione</attribute>
79
+      </orm-attributes>
80
+    </entity>
81
+    <association from="\App\Models\Segnalazione" to="\App\Models\User" caption="User Segnalaziones" owner-alias="segnalazioniAssegnate" inverse-alias="assegnataA" many-owner="true" many-inverse="false" uuid="2fa65d03-8ddb-413f-8064-798860928786">
82
+      <association-field from="assegnata_a_id" to="id" uuid="d432694a-89df-447e-801d-24b276e2ac8c"/>
83
+      <orm-attributes>
84
+        <attribute name="onDelete">set null</attribute>
85
+      </orm-attributes>
86
+    </association>
87
+    <association from="\App\Models\Segnalazione" to="\App\Models\User" caption="User Segnalaziones" owner-alias="segnalazioni" inverse-alias="user" many-owner="true" many-inverse="false" uuid="6faa9818-b5b6-4eab-96b6-6eaf56d1de79">
88
+      <association-field from="user_id" to="id" uuid="08699248-9392-4b75-b16b-ade34b487929"/>
89
+      <orm-attributes>
90
+        <attribute name="onDelete">set null</attribute>
91
+      </orm-attributes>
92
+    </association>
93
+    <association from="\App\Models\Segnalazione" to="\App\Models\User" caption="User Segnalaziones" owner-alias="segnalazioniAzienda" inverse-alias="azienda" many-owner="true" many-inverse="false" uuid="d586b98c-7daa-49d4-ada1-8c2c9223fb66">
94
+      <association-field from="azienda_id" to="id" uuid="ff304b0f-2ee3-4290-8f70-d512cc0cedbe"/>
95
+      <orm-attributes>
96
+        <attribute name="onDelete">set null</attribute>
97
+      </orm-attributes>
98
+    </association>
99
+    <association from="\App\Models\Segnalazione" to="\App\Models\ContrattoServizio" caption="ContrattoServizio Segnalaziones" owner-alias="segnalazioni" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="5f4ca39e-40ba-4157-bf4c-603b7469b044">
100
+      <association-field from="contratto_servizio_id" to="id" uuid="f88439b2-fde3-4839-896b-4fc572f767e0"/>
101
+      <orm-attributes>
102
+        <attribute name="onDelete">set null</attribute>
103
+      </orm-attributes>
104
+    </association>
105
+    <association from="\App\Models\Segnalazione" to="\App\Models\OrdineLavoro" caption="OrdineLavoro Segnalaziones" owner-alias="segnalazioni" inverse-alias="ordineLavoro" many-owner="true" many-inverse="false" uuid="86aded2a-2e4e-4fc2-8702-5191cd073056">
106
+      <association-field from="ordine_lavoro_id" to="id" uuid="b9b6e825-5ce8-4473-be84-81c0df564f89"/>
107
+      <orm-attributes>
108
+        <attribute name="onDelete">set null</attribute>
109
+      </orm-attributes>
110
+    </association>
111
+    <association from="\App\Models\Segnalazione" to="\App\Models\ContrattoServizioTodo" caption="ContrattoServizioTodo Segnalaziones" owner-alias="segnalazioni" inverse-alias="contrattoServizioTodo" many-owner="true" many-inverse="false" uuid="faaed052-f628-4ae7-8f52-b3614fad893e">
112
+      <association-field from="contratto_servizio_todo_id" to="id" uuid="14be7b69-4db0-40ea-bb4b-c32bec31da91"/>
113
+      <orm-attributes>
114
+        <attribute name="onDelete">set null</attribute>
115
+      </orm-attributes>
116
+    </association>
117
+    <association from="\App\Models\Segnalazione" to="\App\Models\AcquistoServizio" caption="AcquistoServizio Segnalaziones" owner-alias="segnalazioni" inverse-alias="acquistoServizio" many-owner="true" many-inverse="false" uuid="41971b51-8523-4c42-83a6-4ea29a618e4c">
118
+      <association-field from="acquisto_servizio_id" to="id" uuid="e298ce6f-7e2c-4b7e-80e3-41c8cd31be5e"/>
119
+      <orm-attributes>
120
+        <attribute name="onDelete">set null</attribute>
121
+      </orm-attributes>
122
+    </association>
123
+    <association from="\App\Models\Segnalazione" to="\App\Models\Prodotto" caption="Prodotto Segnalaziones" owner-alias="segnalazioni" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="3d14fd5b-7f78-49e4-831f-a98aa484a94f">
124
+      <association-field from="prodotto_id" to="id" uuid="dfe269b5-36b7-4a6a-ad73-5195877c88f3"/>
125
+      <orm-attributes>
126
+        <attribute name="onDelete">set null</attribute>
127
+      </orm-attributes>
128
+    </association>
129
+    <entity name="\App\Models\ConfiguraSegnalazione" local-name="ConfiguraSegnalazione" namespace="\App\Models" uuid="8819ca52-ae65-43e3-8aa4-a9d99d228284">
130
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="b82082e0-0b90-471d-9698-545b58e84b5a"/>
131
+      <field name="label" type="string" uuid="c0b1bbac-3561-4290-8d26-0f80e005ddf9"/>
132
+      <field name="descrizione" type="text" uuid="27bba23a-4ee0-47bf-bb70-b7029dbacd8a"/>
133
+      <field name="gruppo" type="string" uuid="59cd917a-97b3-4227-badc-ee7604c971f7"/>
134
+      <field name="key" type="string" uuid="436d6a3f-fc22-4fe5-a91f-e2c393ed7780"/>
135
+      <field name="value" type="string" uuid="6122590b-dc17-4bf2-8497-c5b17bacaacd"/>
136
+      <field name="type" type="string" uuid="b7ab3e5d-4482-483b-8cea-a26de1feda03"/>
137
+      <field name="order" type="integer" uuid="e4996d7f-4473-478d-8fea-5936deb136bd"/>
138
+      <field name="icona" type="string" uuid="b9635a0a-85b2-467a-b2dc-89cd63ee0c4d"/>
139
+      <field name="colore" type="string" uuid="c6811c53-5d36-454d-b43f-5df0157e39c2"/>
140
+      <field name="opzioni" type="json" uuid="1038885e-eca5-4f75-a412-81059913bfe5"/>
141
+      <field name="created_at" type="timestamp" uuid="00cd7459-b3a0-482c-83a4-7568de2c7124"/>
142
+      <field name="updated_at" type="timestamp" uuid="93b12b98-06a8-4563-9e4d-553b532d39db"/>
143
+      <orm-attributes>
144
+        <attribute name="fillableAll">as-fillable-all</attribute>
145
+        <attribute name="table">configura_segnalazione</attribute>
146
+      </orm-attributes>
147
+    </entity>
148
+    <entity name="\App\Models\Annotazione" local-name="Annotazione" namespace="\App\Models" uuid="43c9733c-5074-4c8d-9cb1-33867510c2fc">
149
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="0437cbe3-b2c4-4fa6-8c9c-7df74e17a5e2"/>
150
+      <field name="segnalazione_id" type="bigInteger" unsigned="true" uuid="a55daaa7-8ba7-4bb0-83dd-dcfdb8036968"/>
151
+      <field name="user_id" type="bigInteger" unsigned="true" uuid="734b6cb6-58f7-4a2b-b27d-ab8dc46cf714"/>
152
+      <field name="text" type="text" uuid="f748b6d3-a6af-40b0-a522-4447364a2948"/>
153
+      <field name="tempo_minuti" type="integer" default="0" uuid="b3f34db1-e2e5-4426-a960-51eb2d8ca0b1"/>
154
+      <field name="tempo_data" type="dateTime" uuid="f305a718-29d3-4bd5-8da9-619db3671251">
155
+        <orm-attributes>
156
+          <attribute name="cast">datetime</attribute>
157
+          <attribute name="castFormat">d/m/Y H:i</attribute>
158
+        </orm-attributes>
159
+      </field>
160
+      <field name="costo" type="decimal" size="10" uuid="330f67c6-9da7-4130-8907-9c1339a62723">
161
+        <orm-attributes>
162
+          <attribute name="decimal">2</attribute>
163
+        </orm-attributes>
164
+      </field>
165
+      <field name="costo_descrizione" type="text" uuid="2d89a9fb-9229-412e-b6df-9b35a19f14a5"/>
166
+      <field name="riservata" type="boolean" default="1" uuid="c48733ff-ec02-4aa0-be7c-25be727c3c1e"/>
167
+      <field name="riferimento" type="string" uuid="dca8d620-9b45-4a1f-96e4-0d76392ac410"/>
168
+      <field name="email" type="string" uuid="e2ca0f54-426d-4977-b729-fd007dee6f64"/>
169
+      <field name="telefono" type="string" uuid="7b367191-cdfd-465f-8efe-ba0922fa32ff"/>
170
+      <field name="created_at" type="timestamp" uuid="d0ae0044-ec8e-4aee-8cb5-84d5a663c57a"/>
171
+      <field name="updated_at" type="timestamp" uuid="80c6b3cc-d201-4972-8b42-e25cbee8b615"/>
172
+      <orm-attributes>
173
+        <attribute name="fillableAll">as-fillable-all</attribute>
174
+        <attribute name="table">annotazione</attribute>
175
+      </orm-attributes>
176
+    </entity>
177
+    <association from="\App\Models\Annotazione" to="\App\Models\Segnalazione" caption="Segnalazione Annotaziones" owner-alias="annotazioni" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="9d4fa7d0-23be-4bb2-99bd-67797c3244c0">
178
+      <association-field from="segnalazione_id" to="id" uuid="76a290ad-9338-4ae7-a168-dd924d4c0b9f"/>
179
+      <orm-attributes>
180
+        <attribute name="onDelete">cascade</attribute>
181
+      </orm-attributes>
182
+    </association>
183
+    <association from="\App\Models\Annotazione" to="\App\Models\User" caption="User Annotaziones" owner-alias="annotazioni" inverse-alias="user" many-owner="true" many-inverse="false" uuid="f5d31adf-5971-42f7-90bf-41571fe64218">
184
+      <association-field from="user_id" to="id" uuid="1fb9c039-f291-4e4e-ac2f-c31f14db295b"/>
185
+      <orm-attributes>
186
+        <attribute name="onDelete">set null</attribute>
187
+      </orm-attributes>
188
+    </association>
189
+    <entity name="\App\Models\AllegatoAnnotazione" local-name="AllegatoAnnotazione" namespace="\App\Models" uuid="be8b5d1d-91da-4d7b-a50e-415ef90e5669">
190
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="30f3fa1d-7834-42da-bbfc-3a60ddad4034"/>
191
+      <field name="annotazione_id" type="bigInteger" unsigned="true" uuid="d48cb9c0-3907-4472-88a5-44774d686d37"/>
192
+      <field name="nome" type="string" uuid="96220dc4-79f3-4e79-8024-cd4a976f443a"/>
193
+      <field name="descrizione" type="string" uuid="97adca03-8802-4042-8f00-ff79b3910b0d"/>
194
+      <field name="path_file" type="string" uuid="14d15c1e-3bfe-45b8-8d74-5ddd222ff3b5"/>
195
+      <field name="created_at" type="timestamp" uuid="aa099fda-2af1-4c9e-8e79-3020bff7acde"/>
196
+      <field name="updated_at" type="timestamp" uuid="65bce51b-82df-4a22-a870-38aa7dc6e304"/>
197
+      <orm-attributes>
198
+        <attribute name="fillableAll">as-fillable-all</attribute>
199
+        <attribute name="table">allegato_annotazione</attribute>
200
+      </orm-attributes>
201
+    </entity>
202
+    <association from="\App\Models\AllegatoAnnotazione" to="\App\Models\Annotazione" caption="Annotazione AllegatoAnnotaziones" owner-alias="allegatiAnnotazione" inverse-alias="annotazione" many-owner="true" many-inverse="false" uuid="719929c1-e0fe-4feb-950a-ba417c35a60c">
203
+      <association-field from="annotazione_id" to="id" uuid="c74b78f8-307e-4473-a96d-263d5f8908d2"/>
204
+      <orm-attributes>
205
+        <attribute name="onDelete">cascade</attribute>
206
+      </orm-attributes>
207
+    </association>
208
+    <entity name="\App\Models\Provincia" local-name="Provincia" namespace="\App\Models" uuid="f1331b94-8ccf-4328-bc32-bffdc732572e">
209
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="e489d50b-7a4e-4869-b62a-6e552e4a1140"/>
210
+      <field name="nazione_id" type="bigInteger" unsigned="true" uuid="a51b37ab-f251-4222-b5cd-09de8f6032ea"/>
211
+      <field name="nome" type="string" uuid="673adc64-b483-4df0-b282-74322004a246"/>
212
+      <field name="sigla" type="string" uuid="640d52fb-d124-4ee8-a184-52f855e4153b"/>
213
+      <field name="created_at" type="timestamp" uuid="7f7a24b6-229f-4acf-aef3-0ec512768d24"/>
214
+      <field name="updated_at" type="timestamp" uuid="62d0de6c-f896-491b-99db-1840ede2d1fa"/>
215
+      <orm-attributes>
216
+        <attribute name="table">provincia</attribute>
217
+        <attribute name="fillableAll">as-fillable-all</attribute>
218
+      </orm-attributes>
219
+    </entity>
220
+    <association from="\App\Models\Provincia" to="\App\Models\Nazione" caption="Nazione Provincias" owner-alias="provincie" inverse-alias="nazione" many-owner="true" many-inverse="false" uuid="345b5351-9f8a-4d02-9192-1bb27645761d">
221
+      <association-field from="nazione_id" to="id" uuid="8f51c513-0361-4147-b7ac-26b56c42011d"/>
222
+      <orm-attributes>
223
+        <attribute name="onDelete">set null</attribute>
224
+      </orm-attributes>
225
+    </association>
226
+    <entity name="\App\Models\Nazione" local-name="Nazione" namespace="\App\Models" uuid="6128d2f6-88d0-472b-bc71-0a9f49370847">
227
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="67af633f-42c4-416d-ab1f-6b1c8a36dd82"/>
228
+      <field name="nome" type="string" uuid="c98ab5d4-058e-46c3-ae49-5cf78478c8e0"/>
229
+      <field name="sigla" type="string" uuid="7ed5c802-64fb-4bba-aca9-0de4a1d1b099"/>
230
+      <field name="created_at" type="timestamp" uuid="b7cb1d70-b9a6-441a-aacd-7cd963cbc371"/>
231
+      <field name="updated_at" type="timestamp" uuid="b40e30e1-a742-4297-932c-e28291e0a8e4"/>
232
+      <orm-attributes>
233
+        <attribute name="fillableAll">as-fillable-all</attribute>
234
+        <attribute name="table">nazione</attribute>
235
+      </orm-attributes>
236
+    </entity>
237
+    <entity name="\App\Models\Comune" local-name="Comune" namespace="\App\Models" uuid="2ef4e619-ebea-457a-8613-05b3dfb6c7fa">
238
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="656af606-c65b-4b92-bac2-4e8a23d286e4"/>
239
+      <field name="provincia_id" type="bigInteger" unsigned="true" uuid="e52e1b52-9dd3-4f50-ade3-a01755961201"/>
240
+      <field name="nazione_id" type="bigInteger" unsigned="true" uuid="366b0dc8-c30b-4649-a1a2-3819e803595b"/>
241
+      <field name="nome" type="string" uuid="02c5a258-0c72-47fc-8a1f-0b0af6c37e38"/>
242
+      <field name="codice_catastale" type="string" uuid="efc61454-f98b-49aa-98f2-3e6014129b09"/>
243
+      <field name="created_at" type="timestamp" uuid="8f5ae8e3-acaf-40ea-8eba-3139d76454a7"/>
244
+      <field name="updated_at" type="timestamp" uuid="e603d21d-ddb5-46bd-929f-94a78dc0c5f5"/>
245
+      <orm-attributes>
246
+        <attribute name="table">comune</attribute>
247
+        <attribute name="fillableAll">as-fillable-all</attribute>
248
+      </orm-attributes>
249
+    </entity>
250
+    <association from="\App\Models\Comune" to="\App\Models\Provincia" caption="Provincia Comunes" owner-alias="comuni" inverse-alias="provincia" many-owner="true" many-inverse="false" uuid="4399e41c-477e-44d7-91db-b1cd4614f294">
251
+      <association-field from="provincia_id" to="id" uuid="bb143e14-84bd-4e4a-bc86-6045f340856b"/>
252
+    </association>
253
+    <association from="\App\Models\Comune" to="\App\Models\Nazione" caption="Nazione Comunes" owner-alias="comuni" inverse-alias="nazione" many-owner="true" many-inverse="false" uuid="299d534d-b5ef-411c-b66b-f4adaaa3b4f1">
254
+      <association-field from="nazione_id" to="id" uuid="3bf703ca-9546-4b79-85c0-8febb961d821"/>
255
+      <orm-attributes>
256
+        <attribute name="onDelete">set null</attribute>
257
+      </orm-attributes>
258
+    </association>
259
+    <entity name="\App\Models\Config" local-name="Config" namespace="\App\Models" uuid="dd24c9ce-a45f-4d90-b374-ede2efe2e25b">
260
+      <field name="key" type="string" required="true" unique="true" primary="true" uuid="badce0d4-64dd-43b2-be60-820ff13951fa"/>
261
+      <field name="group" type="string" uuid="8ab75bd9-b755-4637-a230-9e2d5de6ac55"/>
262
+      <field name="display_name" type="string" uuid="4210e393-7c3b-4982-8bee-30238073d5bc"/>
263
+      <field name="value" type="longText" uuid="e9019a5d-d4b6-4bf5-a014-7963da6341f8"/>
264
+      <field name="order" type="integer" uuid="9e54a33a-8637-4273-8027-e0aecc53eeb6"/>
265
+      <field name="type" type="string" uuid="e04ce407-d9d0-4c74-9a8d-721deeb63c3c"/>
266
+      <field name="created_at" type="timestamp" uuid="fb7f6e10-5782-4506-9258-4b6185860d71"/>
267
+      <field name="updated_at" type="timestamp" uuid="370dbe57-9a63-4af4-ad81-b7c7647c273c"/>
268
+      <orm-attributes>
269
+        <attribute name="table">config</attribute>
270
+        <attribute name="fillableAll">as-fillable-all</attribute>
271
+      </orm-attributes>
272
+    </entity>
273
+    <entity name="\App\Models\ConfigNotifiche" local-name="ConfigNotifiche" namespace="\App\Models" uuid="23c0457c-f9c0-4210-9b12-1c8fa92cd788">
274
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="ace37544-4c55-4ca9-8144-2da585fae3d6"/>
275
+      <field name="event" type="string" uuid="7ad720b4-ab7e-43a5-b29a-4f434a3d886b"/>
276
+      <field name="users" type="json" uuid="973a3230-ede5-4efd-9cfb-84e1c19876b9"/>
277
+      <field name="is_attivo" type="boolean" default="1" uuid="72f71f81-6a3e-4ad9-8950-fe2a8542d25c"/>
278
+      <field name="created_at" type="timestamp" uuid="2917737f-d22e-4bcb-b966-ebf834a6d20e"/>
279
+      <field name="updated_at" type="timestamp" uuid="573df29f-6ff9-4ffc-897e-c72069ab4200"/>
280
+      <orm-attributes>
281
+        <attribute name="fillableAll">as-fillable-all</attribute>
282
+        <attribute name="table">config_notifiche</attribute>
283
+      </orm-attributes>
284
+    </entity>
285
+    <entity name="\App\Models\Prodotto" local-name="Prodotto" namespace="\App\Models" uuid="ec22a629-0e0a-4949-9ca3-43c6afee8ed5">
286
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="cd5804ad-d5cc-4203-b38a-e5cfa3ebacac"/>
287
+      <field name="codice" type="string" uuid="1bae7c61-7d9b-4795-97ee-fbff819cb95a"/>
288
+      <field name="nome" type="string" uuid="5bd47d50-0113-40ed-b737-aa03f5f9401b"/>
289
+      <field name="descrizione" type="text" uuid="8aae2f86-f5c8-4281-83fd-b46c70228e53"/>
290
+      <field name="tipo" type="string" uuid="c7f8fdd4-0962-4932-bc9a-ca7b96b9b2d8"/>
291
+      <field name="unita_misura" type="string" uuid="30a50b48-7975-46d3-82e4-05e90afa3a6e"/>
292
+      <field name="prezzo" type="decimal" size="10" uuid="fcdc4a08-e2d3-4fb5-baf2-a90df6fbbb04">
293
+        <orm-attributes>
294
+          <attribute name="decimal">2</attribute>
295
+        </orm-attributes>
296
+      </field>
297
+      <field name="periodo" type="string" uuid="81b8c54f-f859-4ff3-9ce0-ffbade00aa28"/>
298
+      <field name="attivo" type="boolean" default="1" uuid="a40f0518-a638-4247-90a1-6aeac1701428"/>
299
+      <field name="prenotabile_online" type="boolean" default="0" uuid="1df3d6c4-020e-48e8-b944-aee2698e66a6"/>
300
+      <field name="meta" type="json" uuid="693b4419-0151-4d8b-89a9-ab7b2ce17f9d"/>
301
+      <field name="created_at" type="timestamp" uuid="5126112e-189a-4b54-9ff5-cc44da7d60fe"/>
302
+      <field name="updated_at" type="timestamp" uuid="e3870d5f-497d-41b4-8e78-09ba31fc34f8"/>
303
+      <orm-attributes>
304
+        <attribute name="fillableAll">as-fillable-all</attribute>
305
+        <attribute name="table">prodotto</attribute>
306
+      </orm-attributes>
307
+    </entity>
308
+    <entity name="\App\Models\ContrattoServizio" local-name="ContrattoServizio" namespace="\App\Models" uuid="58092d28-4a8e-4122-8204-5e009fc16ce6">
309
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="2fc7e24f-923a-49ea-acb2-82d9a15535a2"/>
310
+      <field name="user_id" type="bigInteger" unsigned="true" uuid="a430d266-f99f-4e48-be62-de6daddf22ab"/>
311
+      <field name="contratto_template_id" type="bigInteger" unsigned="true" uuid="a97d83fa-0893-4bcc-a1d4-3fba9849789a"/>
312
+      <field name="nome" type="string" uuid="94ee2223-9e46-43e2-989b-f75e6a6ce3ca"/>
313
+      <field name="costo" type="decimal" size="10" uuid="140deaa1-bdab-4a67-90ac-6b69d96e344e">
314
+        <orm-attributes>
315
+          <attribute name="decimal">2</attribute>
316
+        </orm-attributes>
317
+      </field>
318
+      <field name="periodo" type="string" uuid="9350c4b3-eab3-464a-9366-c0edf37869ce"/>
319
+      <field name="data_inizio" type="date" uuid="36c9fce4-d27b-4b3a-a6b2-58dc0e4dd8a2"/>
320
+      <field name="data_fine" type="date" uuid="d3ca8b85-9117-4b55-a455-66150bd31d3e"/>
321
+      <field name="collegabile_segnalazioni" type="boolean" default="0" uuid="da43c1da-6158-40bc-bd15-62dddad492a7"/>
322
+      <field name="quantita_residua" type="decimal" size="10" uuid="629ff801-824d-4ad7-bc7d-b72a2a3717cf">
323
+        <orm-attributes>
324
+          <attribute name="decimal">2</attribute>
325
+        </orm-attributes>
326
+      </field>
327
+      <field name="note" type="text" uuid="579649ed-46f6-4371-9692-58e724bf5ca1"/>
328
+      <field name="attivo" type="boolean" default="1" uuid="d811fff4-3f7a-488b-9be1-c623c26f43f8"/>
329
+      <field name="created_at" type="timestamp" uuid="b89c48dd-a31b-4173-98c3-56f688449a23"/>
330
+      <field name="updated_at" type="timestamp" uuid="8da87f11-9757-486e-beab-a0b2c1e5d0e9"/>
331
+      <orm-attributes>
332
+        <attribute name="fillableAll">as-fillable-all</attribute>
333
+        <attribute name="table">contratto_servizio</attribute>
334
+      </orm-attributes>
335
+    </entity>
336
+    <association from="\App\Models\ContrattoServizio" to="\App\Models\User" caption="User ContrattoServizios" owner-alias="contrattiServizio" inverse-alias="user" many-owner="true" many-inverse="false" uuid="3842d2b3-d137-47bc-9a10-14b7a860c153">
337
+      <association-field from="user_id" to="id" uuid="cf09dd81-f27c-4eb3-a6b8-60c56ff9c629"/>
338
+      <orm-attributes>
339
+        <attribute name="onDelete">set null</attribute>
340
+      </orm-attributes>
341
+    </association>
342
+    <association from="\App\Models\ContrattoServizio" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoServizios" owner-alias="contrattiServizio" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="4e4e7f6b-b25f-4a8a-8c8d-ab0e6ec93db7">
343
+      <association-field from="contratto_template_id" to="id" uuid="f5ecdcde-183a-44d3-9a40-6f73b49289cc"/>
344
+      <orm-attributes>
345
+        <attribute name="onDelete">set null</attribute>
346
+      </orm-attributes>
347
+    </association>
348
+    <entity name="\App\Models\Offerta" local-name="Offerta" namespace="\App\Models" uuid="f98de9f5-eb6f-467a-8f95-31b70d915946">
349
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="c3304ea3-9e77-4559-992c-be946ba3e47f"/>
350
+      <field name="user_id" type="bigInteger" unsigned="true" uuid="63813fc9-575d-4e6e-810d-b1047497c075"/>
351
+      <field name="numero" type="integer" uuid="f023a417-2c0b-4fff-9c80-21d2425be5ce"/>
352
+      <field name="titolo" type="string" uuid="5ba72745-a94a-4bf2-b90d-a61505e4a60a"/>
353
+      <field name="stato" type="string" uuid="6fbf26fd-3f6a-423d-b93d-48010787778d"/>
354
+      <field name="data" type="date" uuid="c1700acb-f03a-4d19-bcf4-aa54690109ec"/>
355
+      <field name="data_validita" type="date" uuid="d5f192a6-8011-4b45-9e54-4b8e635facb5"/>
356
+      <field name="totale" type="decimal" size="10" uuid="73cbf6f8-9a53-41f7-82f0-1e60cee174f3">
357
+        <orm-attributes>
358
+          <attribute name="decimal">2</attribute>
359
+        </orm-attributes>
360
+      </field>
361
+      <field name="note" type="text" uuid="74caf115-8e82-457f-b4ff-041634717a84"/>
362
+      <field name="path_file" type="string" uuid="a9a104ed-13f8-4798-a6ca-8935b0341cf1"/>
363
+      <field name="meta" type="json" uuid="515560f9-be1b-4a21-921e-98fd5ad103b0"/>
364
+      <field name="created_at" type="timestamp" uuid="bf09aff4-7fd0-4d51-af42-6aec217b7822"/>
365
+      <field name="updated_at" type="timestamp" uuid="42e19ba6-0c54-4ffb-9c8e-1026f077b6af"/>
366
+      <orm-attributes>
367
+        <attribute name="fillableAll">as-fillable-all</attribute>
368
+        <attribute name="table">offerta</attribute>
369
+      </orm-attributes>
370
+    </entity>
371
+    <association from="\App\Models\Offerta" to="\App\Models\User" caption="User Offertas" owner-alias="offerte" inverse-alias="user" many-owner="true" many-inverse="false" uuid="bf45bb05-0679-49af-a7e9-9ae9422675ab">
372
+      <association-field from="user_id" to="id" uuid="a54053de-2584-4984-8dbc-ba5a1137571c"/>
373
+      <orm-attributes>
374
+        <attribute name="onDelete">set null</attribute>
375
+      </orm-attributes>
376
+    </association>
377
+    <entity name="\App\Models\OffertaRiga" local-name="OffertaRiga" namespace="\App\Models" uuid="77be2d5b-7aa8-4a5f-86da-102064c025e6">
378
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="7fac024b-f96f-4012-9097-aea5a263d403"/>
379
+      <field name="offerta_id" type="bigInteger" unsigned="true" uuid="ef151a19-0644-41c0-a0d3-e4c09ea3f25c"/>
380
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="ff013f74-b127-47b5-91b4-0d0122ab7b40"/>
381
+      <field name="descrizione" type="string" uuid="ed5cf628-97fd-47c8-bfd7-fbe70d9fbde1"/>
382
+      <field name="quantita" type="decimal" size="10" uuid="a398d67d-eb98-4ed7-8ec2-7b3e6c97d889">
383
+        <orm-attributes>
384
+          <attribute name="decimal">2</attribute>
385
+        </orm-attributes>
386
+      </field>
387
+      <field name="unita_misura" type="string" uuid="bcfa8042-a56b-4c5e-b727-b95f17d683cf"/>
388
+      <field name="prezzo_unitario" type="decimal" size="10" uuid="372c3640-3751-4f24-b56e-bbcc3bf47852">
389
+        <orm-attributes>
390
+          <attribute name="decimal">2</attribute>
391
+        </orm-attributes>
392
+      </field>
393
+      <field name="sconto" type="decimal" size="10" uuid="12147109-0d9f-4540-9507-b94a0c4a2975">
394
+        <orm-attributes>
395
+          <attribute name="decimal">2</attribute>
396
+        </orm-attributes>
397
+      </field>
398
+      <field name="ordine" type="integer" default="0" uuid="ec952846-0eff-4955-a147-4033fcfd651e"/>
399
+      <field name="note_private" type="text" uuid="d7c52787-0c81-49f9-a3fb-57a94e0a92aa"/>
400
+      <field name="tempo_stimato" type="decimal" size="10" uuid="563af2f8-093c-4e5b-baf5-0f2249311f62">
401
+        <orm-attributes>
402
+          <attribute name="decimal">2</attribute>
403
+        </orm-attributes>
404
+      </field>
405
+      <field name="created_at" type="timestamp" uuid="7dee4720-093d-49e4-aa16-2a9f192f273b"/>
406
+      <field name="updated_at" type="timestamp" uuid="dd5d14dc-9b7a-43f6-bce2-8d7bbf3e4909"/>
407
+      <orm-attributes>
408
+        <attribute name="fillableAll">as-fillable-all</attribute>
409
+        <attribute name="table">offerta_riga</attribute>
410
+      </orm-attributes>
411
+    </entity>
412
+    <association from="\App\Models\OffertaRiga" to="\App\Models\Offerta" caption="Offerta OffertaRigas" owner-alias="offerteRiga" inverse-alias="offerta" many-owner="true" many-inverse="false" uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3">
413
+      <association-field from="offerta_id" to="id" uuid="68a5419f-adef-4d6e-9b28-873c2fbc3d50"/>
414
+      <orm-attributes>
415
+        <attribute name="onDelete">set null</attribute>
416
+      </orm-attributes>
417
+    </association>
418
+    <association from="\App\Models\OffertaRiga" to="\App\Models\Prodotto" caption="Prodotto OffertaRigas" owner-alias="offerteRiga" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="ad855143-f6b1-4d71-9d42-409e76e1efb7">
419
+      <association-field from="prodotto_id" to="id" uuid="0e98c32c-512b-47c3-bc4f-b3ec66dff09f"/>
420
+      <orm-attributes>
421
+        <attribute name="onDelete">set null</attribute>
422
+      </orm-attributes>
423
+    </association>
424
+    <entity name="\App\Models\OrdineLavoro" local-name="OrdineLavoro" namespace="\App\Models" uuid="97b95f3d-1074-4c5d-a526-c829f6bda8f0">
425
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="2edeaae9-ee24-486a-8915-68701af23fa3"/>
426
+      <field name="user_id" type="bigInteger" unsigned="true" uuid="4b1a6baf-3a48-4170-83a5-361faef16291"/>
427
+      <field name="numero" type="integer" uuid="f3a008ed-3125-4530-b9d4-0296c190226c"/>
428
+      <field name="titolo" type="string" uuid="0c4cb8f3-62b6-40b3-b9b5-ee228663551a"/>
429
+      <field name="stato" type="string" uuid="6e27b2cc-35d1-4694-a86a-1713e30100fe"/>
430
+      <field name="percentuale_avanzamento" type="integer" size="5" default="0" uuid="3c3ba72a-22ea-40b8-8395-c2e8a878d908">
431
+        <orm-attributes>
432
+          <attribute name="decimal">2</attribute>
433
+        </orm-attributes>
434
+      </field>
435
+      <field name="data_apertura" type="date" uuid="19096a86-de29-4ab5-8689-d9edd6f93644"/>
436
+      <field name="data_chiusura" type="date" uuid="ea74aee1-de7e-4feb-aeea-f59a2806f7c0"/>
437
+      <field name="note" type="text" uuid="dbfdfb46-0d9e-4921-8ee4-b702cb3988f4"/>
438
+      <field name="meta" type="json" uuid="bcce23aa-fa1e-4b27-8fb0-260ac25113ef"/>
439
+      <field name="created_at" type="timestamp" uuid="310c81ea-88a2-489a-9223-21b77da5cd21"/>
440
+      <field name="updated_at" type="timestamp" uuid="e690deea-d3d1-415c-a077-cfe5408341e6"/>
441
+      <orm-attributes>
442
+        <attribute name="fillableAll">as-fillable-all</attribute>
443
+        <attribute name="table">ordine_lavoro</attribute>
444
+      </orm-attributes>
445
+    </entity>
446
+    <association from="\App\Models\OrdineLavoro" to="\App\Models\User" caption="User OrdineLavoros" owner-alias="ordiniLavoro" inverse-alias="user" many-owner="true" many-inverse="false" uuid="a005da89-ed4b-4145-bca2-b27aaa8fce4b">
447
+      <association-field from="user_id" to="id" uuid="ea6d7186-c6ee-401b-97b4-3274e72831c5"/>
448
+      <orm-attributes>
449
+        <attribute name="onDelete">set null</attribute>
450
+      </orm-attributes>
451
+    </association>
452
+    <entity name="\App\Models\OrdineLavoroHasOfferta" local-name="OrdineLavoroHasOfferta" namespace="\App\Models" uuid="98806966-1270-452c-b1ab-cdae6e0016b6">
453
+      <field name="offerta_id" type="bigInteger" required="true" primary="true" unsigned="true" uuid="cba87bc3-ab11-4af2-bbb8-6830ad23c01a"/>
454
+      <field name="ordine_lavoro_id" type="bigInteger" required="true" primary="true" unsigned="true" uuid="d2f51969-071a-43c1-b95f-680166e51783"/>
455
+    </entity>
456
+    <many-to-many mn-entity="\App\Models\OrdineLavoroHasOfferta" caption="OrdineLavoro to Offerta" uuid="2f367518-de50-431b-a35b-845be0f0d6a1">
457
+      <many-to-many-entity name="\App\Models\Offerta" owning-side="true" alias="offerte" uuid="23689857-4279-44e9-9a77-f58deb0455e3">
458
+        <many-to-many-field from="offerta_id" to="id" uuid="565f4711-1df5-45ad-949d-8c08cc7d81ec"/>
459
+      </many-to-many-entity>
460
+      <many-to-many-entity name="\App\Models\OrdineLavoro" owning-side="false" alias="ordiniLavoro" uuid="f557820c-6dac-427e-ac7c-ffa88be2cce4">
461
+        <many-to-many-field from="ordine_lavoro_id" to="id" uuid="bf9afc7e-7bb7-43dd-a081-3eac94839d30"/>
462
+      </many-to-many-entity>
463
+    </many-to-many>
464
+    <entity name="\App\Models\ContrattoTemplate" local-name="ContrattoTemplate" namespace="\App\Models" uuid="735de4a0-df7b-48d1-a686-8b7b88229151">
465
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="a67ea0e6-cf8e-4afb-a469-be87c6fb17cf"/>
466
+      <field name="codice" type="string" uuid="dc5fcea9-5c72-4086-a311-b72bfcfec4b1"/>
467
+      <field name="nome" type="string" uuid="d8272d1d-d423-4e6f-815d-3fb649c3fc1e"/>
468
+      <field name="descrizione" type="text" uuid="a553726d-4bbf-47b7-8061-aca7e126b5eb"/>
469
+      <field name="periodo" type="string" uuid="46172701-6a39-4a9f-a334-f93507b0a960"/>
470
+      <field name="attivo" type="boolean" default="1" uuid="930b1171-9a12-44df-abaa-54c95cd9a5a5"/>
471
+      <field name="costo_base" type="decimal" size="10" uuid="b6196b2c-5609-4d95-8765-9a3fef75f1f3">
472
+        <orm-attributes>
473
+          <attribute name="decimal">2</attribute>
474
+        </orm-attributes>
475
+      </field>
476
+      <field name="meta" type="json" uuid="ad26f4dd-95bb-4278-a555-9672ee9bbb99"/>
477
+      <field name="created_at" type="timestamp" uuid="8f53b06a-b7e5-4490-b02d-9a777ae9b33d"/>
478
+      <field name="updated_at" type="timestamp" uuid="4a34b942-0d52-4c5b-9ab4-42cf1990284a"/>
479
+      <orm-attributes>
480
+        <attribute name="fillableAll">as-fillable-all</attribute>
481
+        <attribute name="table">contratto_template</attribute>
482
+      </orm-attributes>
483
+    </entity>
484
+    <entity name="\App\Models\ContrattoTemplateTodo" local-name="ContrattoTemplateTodo" namespace="\App\Models" uuid="cdb29815-3972-4ad8-8d0a-f3265469539f">
485
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="663ad965-eeea-4054-85e0-0ee4f376c809"/>
486
+      <field name="contratto_template_id" type="bigInteger" unsigned="true" uuid="6648a1e1-dcaf-447e-be6b-91b1e3291dd7"/>
487
+      <field name="titolo" type="string" uuid="f7a50105-eaa2-44f0-bbb3-8e85521f9993"/>
488
+      <field name="descrizione" type="text" uuid="eb2c2957-2e28-44d2-a863-fae44f0c57ac"/>
489
+      <field name="tipo_schedulazione" type="string" uuid="dbb7da2b-d5e3-4bd9-8914-42e837e14452"/>
490
+      <field name="cron" type="string" uuid="bb7c1237-cf7d-4c74-9ada-7dfcd5556c4d"/>
491
+      <field name="mese" type="integer" uuid="8565fdbc-6300-47d1-95c3-9f551b83e3f7"/>
492
+      <field name="giorno" type="integer" uuid="d3840541-257a-480f-b435-f90387c73a6e"/>
493
+      <field name="offset_giorni" type="integer" uuid="5542385e-4e90-4e00-a1df-67b922783054"/>
494
+      <field name="priorita" type="string" uuid="6fea7b43-9ac5-46e6-abc2-cc536b3ad9a1"/>
495
+      <field name="tipo_segnalazione" type="string" uuid="ce5542d0-0ec5-4060-b96b-ca3c4d24d622"/>
496
+      <field name="ordine" type="integer" default="0" uuid="fc2f2ea2-f5c4-4869-8b7a-d5506a9decd7"/>
497
+      <field name="attivo" type="boolean" default="1" uuid="1a0ad012-c968-4da2-81df-8c3cf2f028ab"/>
498
+      <field name="meta" type="json" uuid="8cbf484b-40ba-4b06-b460-ee96af0abf77"/>
499
+      <field name="created_at" type="timestamp" uuid="f74ad5f7-3cfc-4f36-8d04-d34e9450761e"/>
500
+      <field name="updated_at" type="timestamp" uuid="0dd4eddd-2ba5-4ecd-a172-ed2aed451dde"/>
501
+      <orm-attributes>
502
+        <attribute name="fillableAll">as-fillable-all</attribute>
503
+        <attribute name="table">contratto_template_todo</attribute>
504
+      </orm-attributes>
505
+    </entity>
506
+    <association from="\App\Models\ContrattoTemplateTodo" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateTodos" owner-alias="contrattoTemplateTodos" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87">
507
+      <association-field from="contratto_template_id" to="id" uuid="585c20ce-9da8-4fe2-9926-0681cd0c32f1"/>
508
+      <orm-attributes>
509
+        <attribute name="onDelete">cascade</attribute>
510
+      </orm-attributes>
511
+    </association>
512
+    <entity name="\App\Models\ContrattoServizioTodo" local-name="ContrattoServizioTodo" namespace="\App\Models" uuid="394c3968-e086-46ca-a3ac-0c99d75eddc6">
513
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="95b74961-e2b4-4640-a235-4938034902c5"/>
514
+      <field name="contratto_servizio_id" type="bigInteger" unsigned="true" uuid="f516123c-2034-4aca-9213-397460593262"/>
515
+      <field name="contratto_template_todo_id" type="bigInteger" unsigned="true" uuid="0d340f14-2c61-4b71-b1ac-d01d0ff45adc"/>
516
+      <field name="titolo" type="string" uuid="cc0106de-1acb-4da9-84f3-53c8941c0619"/>
517
+      <field name="descrizione" type="text" uuid="020ff5ed-d5a6-43a2-a278-ac4a2b764959"/>
518
+      <field name="tipo_schedulazione" type="string" uuid="f8841146-18f3-4906-9553-23f29e60ae3d"/>
519
+      <field name="cron" type="string" uuid="83e3ed9d-ec16-42c3-8e29-adb77a03bb79"/>
520
+      <field name="mese" type="integer" uuid="89138272-52f9-4889-a719-456357a0a27f"/>
521
+      <field name="giorno" type="integer" uuid="e00540bf-6f4a-47ae-9820-da5a5f1d638f"/>
522
+      <field name="offset_giorni" type="integer" uuid="7746bf3a-6d93-4109-ad6f-0147316b410a"/>
523
+      <field name="data_prossima" type="date" uuid="396211be-d355-4cba-8b01-3fc9433b79a5"/>
524
+      <field name="data_ultima" type="date" uuid="6e791de8-605c-47ad-9cee-a91fe1201068"/>
525
+      <field name="priorita" type="string" uuid="1bb1e02d-a910-4d25-807f-c2c980d4e796"/>
526
+      <field name="tipo_segnalazione" type="string" uuid="a21a2141-f099-452a-99ca-9c58d337b73f"/>
527
+      <field name="ordine" type="integer" default="0" uuid="7a97d379-d5bd-4f9d-946c-6ae1c90ba703"/>
528
+      <field name="attivo" type="boolean" default="1" uuid="186a4b94-28db-416d-85b6-c29eeeb39485"/>
529
+      <field name="meta" type="json" uuid="a331b115-cc85-4785-961a-a2d38cab807c"/>
530
+      <field name="created_at" type="timestamp" uuid="31e43e01-782b-4d66-a47a-3c582ac65491"/>
531
+      <field name="updated_at" type="timestamp" uuid="c1aacd09-e455-4e64-b398-8bd590da01ad"/>
532
+      <orm-attributes>
533
+        <attribute name="fillableAll">as-fillable-all</attribute>
534
+        <attribute name="table">contratto_servizio_todo</attribute>
535
+      </orm-attributes>
536
+    </entity>
537
+    <association from="\App\Models\ContrattoServizioTodo" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioTodos" owner-alias="contrattoServizioTodos" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="3e2b345d-f7f3-47e9-b6da-39b2c597e870">
538
+      <association-field from="contratto_servizio_id" to="id" uuid="599210f0-f87a-4280-badd-0717a4fbcd40"/>
539
+      <orm-attributes>
540
+        <attribute name="onDelete">cascade</attribute>
541
+      </orm-attributes>
542
+    </association>
543
+    <association from="\App\Models\ContrattoServizioTodo" to="\App\Models\ContrattoTemplateTodo" caption="ContrattoTemplateTodo ContrattoServizioTodos" owner-alias="contrattoServizioTodos" inverse-alias="contrattoTemplateTodo" many-owner="true" many-inverse="false" uuid="e29244ac-f1e5-4629-bf06-2d29d4a7664c">
544
+      <association-field from="contratto_template_todo_id" to="id" uuid="5b213914-00e8-4c89-9150-7022b294766a"/>
545
+      <orm-attributes>
546
+        <attribute name="onDelete">set null</attribute>
547
+      </orm-attributes>
548
+    </association>
549
+    <entity name="\App\Models\ContrattoTemplateRiga" local-name="ContrattoTemplateRiga" namespace="\App\Models" uuid="5c90d7b2-3bfb-4531-906b-ffb50049910d">
550
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="c594c8bc-e6be-4b44-b28a-8ddfe4893c97"/>
551
+      <field name="contratto_template_id" type="bigInteger" unsigned="true" uuid="655f5603-7405-4da7-b945-492a74b82232"/>
552
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="891eb65f-5e50-47a5-8d6a-48dd6f5a76ff"/>
553
+      <field name="quantita" type="decimal" size="10" uuid="674f04ea-e6bd-4015-b80d-531ef3f4092d">
554
+        <orm-attributes>
555
+          <attribute name="decimal">2</attribute>
556
+        </orm-attributes>
557
+      </field>
558
+      <field name="unita_misura" type="string" uuid="8a770361-3ac1-4f27-a2b5-0f7375fea6c4"/>
559
+      <field name="prezzo_unitario" type="decimal" size="10" uuid="a0418b73-b1f5-444c-b546-e7fe1216b4a9">
560
+        <orm-attributes>
561
+          <attribute name="decimal">2</attribute>
562
+        </orm-attributes>
563
+      </field>
564
+      <field name="periodo" type="string" uuid="cb3ffa97-9aa1-4e33-9de6-f2dc5b69347e"/>
565
+      <field name="note" type="text" uuid="afecd4d2-26d4-4639-a390-be8b02023ba1"/>
566
+      <field name="ordine" type="integer" default="0" uuid="509ec89f-2c7c-49b4-9083-0d1304760ef1"/>
567
+      <field name="created_at" type="timestamp" uuid="0618fd13-368f-46d4-9c9f-cd649579384b"/>
568
+      <field name="updated_at" type="timestamp" uuid="9d5c1e64-5a11-4dd5-bbda-7287d5c91307"/>
569
+      <orm-attributes>
570
+        <attribute name="fillableAll">as-fillable-all</attribute>
571
+        <attribute name="table">contratto_template_riga</attribute>
572
+      </orm-attributes>
573
+    </entity>
574
+    <association from="\App\Models\ContrattoTemplateRiga" to="\App\Models\Prodotto" caption="Prodotto ContrattoTemplateRigas" owner-alias="contrattiTemplateRiga" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="64df4dd6-cb23-408a-a4f1-e1a723103aa0">
575
+      <association-field from="prodotto_id" to="id" uuid="ddbd2bf2-b433-47b1-9c3b-486b86f25032"/>
576
+      <orm-attributes>
577
+        <attribute name="onDelete">set null</attribute>
578
+      </orm-attributes>
579
+    </association>
580
+    <association from="\App\Models\ContrattoTemplateRiga" to="\App\Models\ContrattoTemplate" caption="ContrattoTemplate ContrattoTemplateRigas" owner-alias="contrattoTemplateRigas" inverse-alias="contrattoTemplate" many-owner="true" many-inverse="false" uuid="79ac3c47-aa91-4b9f-a46d-b5aa7c818f6f">
581
+      <association-field from="contratto_template_id" to="id" uuid="868068aa-7b93-4c95-ae09-588db5fe51e2"/>
582
+      <orm-attributes>
583
+        <attribute name="onDelete">cascade</attribute>
584
+      </orm-attributes>
585
+    </association>
586
+    <entity name="\App\Models\ContrattoServizioRiga" local-name="ContrattoServizioRiga" namespace="\App\Models" uuid="acba2571-6f1b-46a8-85b8-001d04f4aaca">
587
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="e39c970f-e64a-44c5-ad58-1701397cd924"/>
588
+      <field name="contratto_servizio_id" type="bigInteger" unsigned="true" uuid="0a5b2558-c482-448c-bbd4-438a5ce8a905"/>
589
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="1365f409-2ba4-46f1-8781-bff463c01a2c"/>
590
+      <field name="quantita" type="decimal" size="10" uuid="1b2edca7-2ad1-47de-971e-5c1b82e7a49d">
591
+        <orm-attributes>
592
+          <attribute name="decimal">2</attribute>
593
+        </orm-attributes>
594
+      </field>
595
+      <field name="unita_misura" type="string" uuid="d92bbf06-a497-4525-afd5-a550a99f8ef3"/>
596
+      <field name="prezzo_unitario" type="decimal" size="10" uuid="8db2eb16-6b67-402e-bd36-c9e662a0f5a7">
597
+        <orm-attributes>
598
+          <attribute name="decimal">2</attribute>
599
+        </orm-attributes>
600
+      </field>
601
+      <field name="periodo" type="string" uuid="ff878a32-4891-4622-8bd1-0c053da212ff"/>
602
+      <field name="note" type="text" uuid="73662125-5f5e-4918-bade-f5428ab2def4"/>
603
+      <field name="ordine" type="integer" default="0" uuid="953a6161-5754-4c09-8248-bd4f63383948"/>
604
+      <field name="created_at" type="timestamp" uuid="d01c008b-3dd8-4f7d-87a5-5b74dc328f5c"/>
605
+      <field name="updated_at" type="timestamp" uuid="23bc72d4-6532-4422-b813-f4689c8b34ad"/>
606
+      <orm-attributes>
607
+        <attribute name="fillableAll">as-fillable-all</attribute>
608
+        <attribute name="table">contratto_servizio_riga</attribute>
609
+      </orm-attributes>
610
+    </entity>
611
+    <association from="\App\Models\ContrattoServizioRiga" to="\App\Models\Prodotto" caption="Prodotto ContrattoTemplateRigas" owner-alias="contrattiServizioRiga" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="5c8d1dc1-15ed-4379-addb-6b0addcd047e">
612
+      <association-field from="prodotto_id" to="id" uuid="e1b63a6d-9fc1-48b8-8a2a-54a34489ff3a"/>
613
+      <orm-attributes>
614
+        <attribute name="onDelete">set null</attribute>
615
+      </orm-attributes>
616
+    </association>
617
+    <association from="\App\Models\ContrattoServizioRiga" to="\App\Models\ContrattoServizio" caption="ContrattoServizio ContrattoServizioRigas" owner-alias="contrattoServizioRigas" inverse-alias="contrattoServizio" many-owner="true" many-inverse="false" uuid="b2867c66-0dcf-4a4c-99ce-cbdda8b11d6f">
618
+      <association-field from="contratto_servizio_id" to="id" uuid="79825cde-3073-435d-9dec-fec172cbf19d"/>
619
+      <orm-attributes>
620
+        <attribute name="onDelete">cascade</attribute>
621
+      </orm-attributes>
622
+    </association>
623
+    <entity name="\App\Models\PromoCode" local-name="PromoCode" namespace="\App\Models" uuid="c0890191-d438-495f-819a-1a045d21a357">
624
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="82e57f04-7b3a-4ec9-9cb1-8ab821e49024"/>
625
+      <field name="codice" type="string" uuid="ee767607-dea9-4828-bf9f-780b10ab3127"/>
626
+      <field name="sconto_percentuale" type="decimal" size="10" uuid="fe6bedd5-c44a-405d-af80-868f871e09fe">
627
+        <orm-attributes>
628
+          <attribute name="decimal">2</attribute>
629
+        </orm-attributes>
630
+      </field>
631
+      <field name="attivo" type="boolean" default="1" uuid="5ffde063-aea1-49de-a6b4-c8b710e11c5d"/>
632
+      <field name="valido_dal" type="date" uuid="fba28412-e36c-4bb9-bbbc-a536cf12ae20"/>
633
+      <field name="valido_al" type="date" uuid="6b62a9cc-8f2b-45cb-9396-b3a73e3201be"/>
634
+      <field name="usi_max" type="integer" uuid="b6e44e7c-fe55-4914-9772-691b14832075"/>
635
+      <field name="usi_count" type="integer" uuid="866015d2-09d0-4395-891c-a6c66cd27230"/>
636
+      <field name="note" type="text" uuid="0945421e-d222-4ba8-aa95-762a58678b7e"/>
637
+      <field name="created_at" type="timestamp" uuid="108d2bec-f727-41af-9a47-f1d29b0be843"/>
638
+      <field name="updated_at" type="timestamp" uuid="b433b376-1050-49b0-9c1d-86f0b4aef896"/>
639
+      <orm-attributes>
640
+        <attribute name="fillableAll">as-fillable-all</attribute>
641
+        <attribute name="table">promo_code</attribute>
642
+      </orm-attributes>
643
+    </entity>
644
+    <entity name="\App\Models\PromoCodeHasProdotto" local-name="PromoCodeHasProdotto" namespace="\App\Models" uuid="273deed0-9f6e-4aca-b64b-22a657db2a21">
645
+      <field name="prodotto_id" type="bigInteger" required="true" primary="true" unsigned="true" uuid="2ded0962-e47f-4bee-94d0-364923a9aace"/>
646
+      <field name="promo_code_id" type="bigInteger" required="true" primary="true" unsigned="true" uuid="cbfc3db8-20fd-4fce-8dff-402604dd3d20"/>
647
+    </entity>
648
+    <many-to-many mn-entity="\App\Models\PromoCodeHasProdotto" caption="PromoCode to Prodotto" uuid="b628247f-6792-430c-970b-aa651c7f7870">
649
+      <many-to-many-entity name="\App\Models\Prodotto" owning-side="true" alias="prodotti" uuid="b51dbffa-75cf-4b1a-92eb-83a8ac62d775">
650
+        <many-to-many-field from="prodotto_id" to="id" uuid="727ea3ce-db87-4aba-8a83-db60bda0e7e4"/>
651
+      </many-to-many-entity>
652
+      <many-to-many-entity name="\App\Models\PromoCode" owning-side="false" alias="promoCodes" uuid="90017630-36c3-4e45-b433-7eec25a364e8">
653
+        <many-to-many-field from="promo_code_id" to="id" uuid="3155e17d-c9ee-4911-a98d-ca14e7351d0b"/>
654
+      </many-to-many-entity>
655
+    </many-to-many>
656
+    <entity name="\App\Models\AcquistoServizio" local-name="AcquistoServizio" namespace="\App\Models" uuid="c9dad18d-0272-4878-8404-fd0db896b447">
657
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="f948a486-f887-46c0-96be-87ce5f4c6945"/>
658
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="bc680727-da9a-43e4-8291-067fcdb491db"/>
659
+      <field name="promo_code_id" type="bigInteger" unsigned="true" uuid="c4101336-5ef2-4fe8-a222-ba16450e4317"/>
660
+      <field name="segnalazione_id" type="bigInteger" unsigned="true" uuid="43d5b052-1494-4bd6-97ac-391c515d37f5"/>
661
+      <field name="nome" type="string" uuid="27d91a86-9d2f-4311-a923-bd97abde2241"/>
662
+      <field name="email" type="string" uuid="4ad66228-0077-4bed-a371-b6784f4e28eb"/>
663
+      <field name="telefono" type="string" uuid="0cbd8803-1513-4000-ac24-428a9b217ea9"/>
664
+      <field name="modalita" type="string" uuid="adcbaf5e-7e09-422b-a6ce-4016c7bab0de"/>
665
+      <field name="prezzo_servizio" type="decimal" size="10" uuid="6d3fd49c-d838-47c5-b212-381b9a2a920e">
666
+        <orm-attributes>
667
+          <attribute name="decimal">2</attribute>
668
+        </orm-attributes>
669
+      </field>
670
+      <field name="costo_domicilio" type="decimal" size="10" uuid="b993cd59-1578-4198-bca0-d48c3fa882a1">
671
+        <orm-attributes>
672
+          <attribute name="decimal">2</attribute>
673
+        </orm-attributes>
674
+      </field>
675
+      <field name="sconto" type="decimal" size="10" uuid="6874d156-0200-4215-80c3-2d6a7719eda3">
676
+        <orm-attributes>
677
+          <attribute name="decimal">2</attribute>
678
+        </orm-attributes>
679
+      </field>
680
+      <field name="totale" type="decimal" size="10" uuid="88cf3d8e-2f2c-4c58-9c13-da2d462b4d16">
681
+        <orm-attributes>
682
+          <attribute name="decimal">2</attribute>
683
+        </orm-attributes>
684
+      </field>
685
+      <field name="provider" type="string" uuid="e66150ce-7775-4426-959a-00e799f75568"/>
686
+      <field name="payment_status" type="string" uuid="463c8589-2ec6-433c-b78e-f173cf9787b7"/>
687
+      <field name="condizioni_accettate_at" type="dateTime" uuid="4a75c4b5-5391-4088-a88d-14beadfb0914"/>
688
+      <field name="token" type="string" uuid="3c187398-c6bd-42b8-a22c-0674e1579405"/>
689
+      <field name="meta" type="json" uuid="9b9bd179-d249-4d73-a35d-1811619012df"/>
690
+      <field name="codice_fiscale" type="string" uuid="ce0d688a-8a3b-4b93-a5b1-34c449881314"/>
691
+      <field name="indirizzo" type="string" uuid="43f589c6-38d2-4b3d-a009-daf87264a3f9"/>
692
+      <field name="note_cliente" type="text" uuid="88543171-e695-4be3-acfe-8955f90ef825"/>
693
+      <field name="note_operatore" type="string" uuid="d992c256-dd60-40f7-8032-01c55e6b759b"/>
694
+      <field name="created_at" type="timestamp" uuid="af995e1a-0223-486e-98d0-85d429fa3d9b"/>
695
+      <field name="updated_at" type="timestamp" uuid="fd6b9919-0139-45f2-b4e2-f3cc84321316"/>
696
+      <orm-attributes>
697
+        <attribute name="fillableAll">as-fillable-all</attribute>
698
+        <attribute name="table">acquisto_servizio</attribute>
699
+      </orm-attributes>
700
+    </entity>
701
+    <association from="\App\Models\AcquistoServizio" to="\App\Models\Prodotto" caption="Prodotto AcquistoServizios" owner-alias="acquistiServizio" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="d49573ad-aa71-43d3-a053-0858f655cd29">
702
+      <association-field from="prodotto_id" to="id" uuid="cbfb0c26-6096-4ff5-95ec-766af130bf9c"/>
703
+      <orm-attributes>
704
+        <attribute name="onDelete">set null</attribute>
705
+      </orm-attributes>
706
+    </association>
707
+    <association from="\App\Models\AcquistoServizio" to="\App\Models\PromoCode" caption="PromoCode AcquistoServizios" owner-alias="acquistiServizio" inverse-alias="promoCode" many-owner="true" many-inverse="false" uuid="c227ab02-8c3d-4a91-88a1-51e115f830a0">
708
+      <association-field from="promo_code_id" to="id" uuid="35849a96-4291-4068-a130-a6570917a0cc"/>
709
+      <orm-attributes>
710
+        <attribute name="onDelete">set null</attribute>
711
+      </orm-attributes>
712
+    </association>
713
+    <association from="\App\Models\AcquistoServizio" to="\App\Models\Segnalazione" caption="Segnalazione AcquistoServizios" owner-alias="acquistoServizios" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="b4bf7317-84eb-4d04-889e-5a8fcf84b12f">
714
+      <association-field from="segnalazione_id" to="id" uuid="54f240d3-c752-4594-9da1-2f1f569d36c8"/>
715
+      <orm-attributes>
716
+        <attribute name="onDelete">set null</attribute>
717
+      </orm-attributes>
718
+    </association>
719
+    <entity name="\App\Models\ProdottoTodo" local-name="ProdottoTodo" namespace="\App\Models" uuid="c493452e-6e93-4e0e-a1b2-0791f82188b1">
720
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="87bde1ea-7640-400e-a7d1-0462489ae11d"/>
721
+      <field name="prodotto_id" type="bigInteger" unsigned="true" uuid="25811152-45de-471d-8564-cc81ef9786e8"/>
722
+      <field name="titolo" type="string" uuid="a603f5de-b1a1-4f62-bb8c-f4f1cd7c3f50"/>
723
+      <field name="descrizione" type="text" uuid="985444a2-483b-4a8c-a899-1479af35314b"/>
724
+      <field name="ordine" type="integer" default="0" uuid="95190e25-ec45-4882-b288-e38a0db8ed74"/>
725
+      <field name="attivo" type="boolean" default="1" uuid="d64d14fd-6ac7-40bc-9f92-3139b413f803"/>
726
+      <field name="obbligatorio" type="boolean" default="0" uuid="07db0aa3-81fe-4d89-a496-04842f0d087b"/>
727
+      <field name="meta" type="json" uuid="af4eba12-2a21-4023-bd8e-da0d4b46e265"/>
728
+      <field name="public" type="boolean" default="1" uuid="f6354bf0-0cf6-43e5-9df8-bcf6c63ed4bd"/>
729
+      <field name="created_at" type="timestamp" uuid="c0946141-13ab-487a-b958-6faee53e5b08"/>
730
+      <field name="updated_at" type="timestamp" uuid="27d28691-d311-4d90-9e47-6a56544091dc"/>
731
+      <orm-attributes>
732
+        <attribute name="fillableAll">as-fillable-all</attribute>
733
+        <attribute name="table">prodotto_todo</attribute>
734
+      </orm-attributes>
735
+    </entity>
736
+    <association from="\App\Models\ProdottoTodo" to="\App\Models\Prodotto" caption="Prodotto ProdottoTodos" owner-alias="prodottoTodos" inverse-alias="prodotto" many-owner="true" many-inverse="false" uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6">
737
+      <association-field from="prodotto_id" to="id" uuid="8739cbb8-97d0-4b24-bdf2-7218bba13cf2"/>
738
+      <orm-attributes>
739
+        <attribute name="onDelete">cascade</attribute>
740
+      </orm-attributes>
741
+    </association>
742
+    <entity name="\App\Models\SegnalazioneTodo" local-name="SegnalazioneTodo" namespace="\App\Models" uuid="05f7d8b0-095f-456c-96ec-3980b0435855">
743
+      <field name="id" type="bigInteger" required="true" unique="true" primary="true" unsigned="true" auto-increment="true" uuid="1df0218e-6128-4831-9b3b-2eac58ffa182"/>
744
+      <field name="segnalazione_id" type="bigInteger" unsigned="true" uuid="d14a0c59-6b66-4328-bd55-de2d7a0a27a6"/>
745
+      <field name="prodotto_todo_id" type="bigInteger" unsigned="true" uuid="a48df42a-6791-41b7-92a8-0b02e7f25ca5"/>
746
+      <field name="completato_da_id" type="bigInteger" unsigned="true" uuid="fdee23c2-9175-4495-99b4-631761149856"/>
747
+      <field name="titolo" type="string" uuid="cb9d1238-a216-4d6d-b75b-890b532e6073"/>
748
+      <field name="descrizione" type="text" uuid="192661a9-3d18-4eda-840c-e03991eeb59f"/>
749
+      <field name="ordine" type="integer" default="0" uuid="50402be7-5614-49a1-9da9-ab614274f510"/>
750
+      <field name="obbligatorio" type="boolean" default="0" uuid="4cafcf9d-7b00-4834-aa70-cdec0fd04dea"/>
751
+      <field name="completato_at" type="dateTime" uuid="49c1f168-39e6-4c8a-974f-e8897ce76e15"/>
752
+      <field name="meta" type="json" uuid="2c3de855-3f52-411b-8490-0c4367d21605"/>
753
+      <field name="public" type="boolean" default="1" uuid="67bb376a-567c-486e-a285-d1a52b515c00"/>
754
+      <field name="created_at" type="timestamp" uuid="eeb4f415-6d04-4886-8c4a-b24732fd87ba"/>
755
+      <field name="updated_at" type="timestamp" uuid="b8abd05e-4682-4f4e-ad50-cc3980955c82"/>
756
+      <orm-attributes>
757
+        <attribute name="fillableAll">as-fillable-all</attribute>
758
+        <attribute name="table">segnalazione_todo</attribute>
759
+      </orm-attributes>
760
+    </entity>
761
+    <association from="\App\Models\SegnalazioneTodo" to="\App\Models\Segnalazione" caption="Segnalazione SegnalazioneTodos" owner-alias="segnalazioneTodos" inverse-alias="segnalazione" many-owner="true" many-inverse="false" uuid="ae43bea5-a4e1-4e9c-881c-ab2a91c7b743">
762
+      <association-field from="segnalazione_id" to="id" uuid="7c11bde8-f541-4a43-aa6b-a8583a591dbe"/>
763
+      <orm-attributes>
764
+        <attribute name="onDelete">cascade</attribute>
765
+      </orm-attributes>
766
+    </association>
767
+    <association from="\App\Models\SegnalazioneTodo" to="\App\Models\ProdottoTodo" caption="ProdottoTodo SegnalazioneTodos" owner-alias="segnalazioneTodos" inverse-alias="prodottoTodo" many-owner="true" many-inverse="false" uuid="05c3f975-b61c-490e-aa04-080772b499ad">
768
+      <association-field from="prodotto_todo_id" to="id" uuid="86840487-55f9-4adc-b5fe-98379305f899"/>
769
+      <orm-attributes>
770
+        <attribute name="onDelete">set null</attribute>
771
+      </orm-attributes>
772
+    </association>
773
+    <association from="\App\Models\SegnalazioneTodo" to="\App\Models\User" caption="User SegnalazioneTodos" owner-alias="segnalazioneTodos" inverse-alias="completatoDa" many-owner="true" many-inverse="false" uuid="eb76c46e-4fae-41a5-8cca-e38e69788e6d">
774
+      <association-field from="completato_da_id" to="id" uuid="1422f882-923a-4fb9-ba53-b782043d1aad"/>
775
+    </association>
776
+    <migrations version="1.0"/>
777
+  </module>
778
+  <visual-data>
779
+    <association uuid="05c3f975-b61c-490e-aa04-080772b499ad" color="#969696"/>
780
+    <association uuid="299d534d-b5ef-411c-b66b-f4adaaa3b4f1" color="#969696"/>
781
+    <association uuid="2fa65d03-8ddb-413f-8064-798860928786" color="#969696"/>
782
+    <association uuid="345b5351-9f8a-4d02-9192-1bb27645761d" color="#969696"/>
783
+    <association uuid="35b0ed5e-e7f9-4a4f-ad24-2547a9392e87" color="#969696"/>
784
+    <association uuid="3842d2b3-d137-47bc-9a10-14b7a860c153" color="#969696"/>
785
+    <association uuid="3d14fd5b-7f78-49e4-831f-a98aa484a94f" color="#969696"/>
786
+    <association uuid="3e2b345d-f7f3-47e9-b6da-39b2c597e870" color="#969696"/>
787
+    <association uuid="41971b51-8523-4c42-83a6-4ea29a618e4c" color="#969696"/>
788
+    <association uuid="4399e41c-477e-44d7-91db-b1cd4614f294" caption1-position-x="0" caption1-position-y="0" center-position-x="0" center-position-y="0" color="#969696"/>
789
+    <association uuid="4e4e7f6b-b25f-4a8a-8c8d-ab0e6ec93db7" color="#969696"/>
790
+    <association uuid="5c8d1dc1-15ed-4379-addb-6b0addcd047e" color="#969696"/>
791
+    <association uuid="5f4ca39e-40ba-4157-bf4c-603b7469b044" color="#969696"/>
792
+    <association uuid="64df4dd6-cb23-408a-a4f1-e1a723103aa0" color="#969696"/>
793
+    <association uuid="6debb5a0-4bc9-4e03-bafa-569e49949ad6" color="#969696"/>
794
+    <association uuid="6faa9818-b5b6-4eab-96b6-6eaf56d1de79" color="#969696"/>
795
+    <association uuid="719929c1-e0fe-4feb-950a-ba417c35a60c" color="#969696"/>
796
+    <association uuid="787d90ec-5676-4749-9b36-cea959c6793b" caption1-position-x="0" caption1-position-y="0" center-position-x="0" center-position-y="-20" color="#969696"/>
797
+    <association uuid="79ac3c47-aa91-4b9f-a46d-b5aa7c818f6f" color="#969696"/>
798
+    <association uuid="8399970c-2d0b-403e-996b-e86248cb3bc5" color="#969696"/>
799
+    <association uuid="86aded2a-2e4e-4fc2-8702-5191cd073056" color="#969696"/>
800
+    <association uuid="9d4fa7d0-23be-4bb2-99bd-67797c3244c0" color="#969696"/>
801
+    <association uuid="a005da89-ed4b-4145-bca2-b27aaa8fce4b" color="#969696"/>
802
+    <association uuid="ad855143-f6b1-4d71-9d42-409e76e1efb7" color="#969696"/>
803
+    <association uuid="ae43bea5-a4e1-4e9c-881c-ab2a91c7b743" color="#969696"/>
804
+    <association uuid="b2867c66-0dcf-4a4c-99ce-cbdda8b11d6f" color="#969696"/>
805
+    <association uuid="b4bf7317-84eb-4d04-889e-5a8fcf84b12f" color="#969696"/>
806
+    <association uuid="bf45bb05-0679-49af-a7e9-9ae9422675ab" color="#969696"/>
807
+    <association uuid="c227ab02-8c3d-4a91-88a1-51e115f830a0" color="#969696"/>
808
+    <association uuid="d49573ad-aa71-43d3-a053-0858f655cd29" color="#969696"/>
809
+    <association uuid="d586b98c-7daa-49d4-ada1-8c2c9223fb66" color="#969696"/>
810
+    <association uuid="e29244ac-f1e5-4629-bf06-2d29d4a7664c" color="#969696"/>
811
+    <association uuid="eb76c46e-4fae-41a5-8cca-e38e69788e6d" color="#969696"/>
812
+    <association uuid="f5d31adf-5971-42f7-90bf-41571fe64218" color="#969696"/>
813
+    <association uuid="faaed052-f628-4ae7-8f52-b3614fad893e" color="#969696"/>
814
+    <association uuid="fdd9ee5a-9ae1-4736-8b9a-7e7fb84061b3" color="#969696"/>
815
+    <entity uuid="05f7d8b0-095f-456c-96ec-3980b0435855" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="377" position-y="460" size-x="0" size-x2="137" size-y="0" size-y2="73"/>
816
+    <entity uuid="23c0457c-f9c0-4210-9b12-1c8fa92cd788" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1311" position-y="581" size-x="0" size-x2="119" size-y="0" size-y2="101"/>
817
+    <entity uuid="273deed0-9f6e-4aca-b64b-22a657db2a21" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="877" position-y="547" size-x="0" size-x2="134" size-y="0" size-y2="60"/>
818
+    <entity uuid="2ef4e619-ebea-457a-8613-05b3dfb6c7fa" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1300" position-y="299" size-x="0" size-x2="121" size-y="0" size-y2="115"/>
819
+    <entity uuid="394c3968-e086-46ca-a3ac-0c99d75eddc6" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1028" position-y="306" size-x="0" size-x2="181" size-y="0" size-y2="283"/>
820
+    <entity uuid="43c9733c-5074-4c8d-9cb1-33867510c2fc" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="40" position-y="440" size-x="0" size-x2="137" size-y="0" size-y2="213"/>
821
+    <entity uuid="56185713-1439-4bd9-81df-6d5339d066ce" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="35" position-y="36" size-x="0" size-x2="142" size-y="0" size-y2="157"/>
822
+    <entity uuid="58092d28-4a8e-4122-8204-5e009fc16ce6" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="643" position-y="308" size-x="0" size-x2="162" size-y="0" size-y2="255"/>
823
+    <entity uuid="5c90d7b2-3bfb-4531-906b-ffb50049910d" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="809" position-y="25" size-x="0" size-x2="141" size-y="0" size-y2="157"/>
824
+    <entity uuid="6128d2f6-88d0-472b-bc71-0a9f49370847" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1301" position-y="144" size-x="0" size-x2="119" size-y="0" size-y2="87"/>
825
+    <entity uuid="735de4a0-df7b-48d1-a686-8b7b88229151" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="647" position-y="26" size-x="0" size-x2="119" size-y="0" size-y2="143"/>
826
+    <entity uuid="77be2d5b-7aa8-4a5f-86da-102064c025e6" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="807" position-y="634" size-x="0" size-x2="141" size-y="0" size-y2="199"/>
827
+    <entity uuid="8819ca52-ae65-43e3-8aa4-a9d99d228284" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1301" position-y="697" size-x="0" size-x2="119" size-y="0" size-y2="199"/>
828
+    <entity uuid="97b95f3d-1074-4c5d-a526-c829f6bda8f0" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="216" position-y="690" size-x="0" size-x2="177" size-y="0" size-y2="185"/>
829
+    <entity uuid="98806966-1270-452c-b1ab-cdae6e0016b6" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="560" position-y="569" size-x="0" size-x2="139" size-y="0" size-y2="60"/>
830
+    <entity uuid="acba2571-6f1b-46a8-85b8-001d04f4aaca" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="829" position-y="310" size-x="0" size-x2="141" size-y="0" size-y2="157"/>
831
+    <entity uuid="b21d156e-828b-49fe-87a8-659eb8411896" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="273" position-y="23" size-x="0" size-x2="177" size-y="0" size-y2="297"/>
832
+    <entity uuid="be8b5d1d-91da-4d7b-a50e-415ef90e5669" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="221" position-y="422" size-x="0" size-x2="133" size-y="0" size-y2="115"/>
833
+    <entity uuid="c0890191-d438-495f-819a-1a045d21a357" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="967" position-y="709" size-x="0" size-x2="158" size-y="0" size-y2="171"/>
834
+    <entity uuid="c493452e-6e93-4e0e-a1b2-0791f82188b1" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="513" position-y="274" size-x="0" size-x2="119" size-y="0" size-y2="157"/>
835
+    <entity uuid="c9dad18d-0272-4878-8404-fd0db896b447" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1132" position-y="623" size-x="0" size-x2="164" size-y="0" size-y2="269"/>
836
+    <entity uuid="cdb29815-3972-4ad8-8d0a-f3265469539f" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="1030" position-y="25" size-x="0" size-x2="159" size-y="0" size-y2="241"/>
837
+    <entity uuid="dd24c9ce-a45f-4d90-b374-ede2efe2e25b" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1300" position-y="433" size-x="0" size-x2="119" size-y="0" size-y2="129"/>
838
+    <entity uuid="ec22a629-0e0a-4949-9ca3-43c6afee8ed5" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="481" position-y="25" size-x="0" size-x2="119" size-y="0" size-y2="185"/>
839
+    <entity uuid="f1331b94-8ccf-4328-bc32-bffdc732572e" bg-color="#FFFFFF" hdr-color="#FFE762" position-x="1301" position-y="-11" size-x="0" size-x2="119" size-y="0" size-y2="101"/>
840
+    <entity uuid="f98de9f5-eb6f-467a-8f95-31b70d915946" bg-color="#FFFFFF" hdr-color="#D2D2D2" position-x="675" position-y="633" size-x="0" size-x2="119" size-y="0" size-y2="199"/>
841
+    <many-to-many-association uuid="2f367518-de50-431b-a35b-845be0f0d6a1" color="#969696"/>
842
+    <many-to-many-association uuid="b628247f-6792-430c-970b-aa651c7f7870" color="#969696"/>
843
+    <module uuid="09bf51bf-3812-47d1-ab60-2ea025d5ee72" bg-color="#EAE4F1" position-x="34" position-y="55" size-x="11" size-x2="1625" size-y="22" size-y2="968"/>
844
+    <project uuid="3072e02c-7554-4faa-a6bb-1bdf47958cd4" size-x="27" size-x2="1709" size-y="17" size-y2="1073"/>
845
+  </visual-data>
846
+</skipper>

+ 66
- 0
README.md 파일 보기

@@ -0,0 +1,66 @@
1
+<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
2
+
3
+<p align="center">
4
+<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
5
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
6
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
7
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
8
+</p>
9
+
10
+## About Laravel
11
+
12
+Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
13
+
14
+- [Simple, fast routing engine](https://laravel.com/docs/routing).
15
+- [Powerful dependency injection container](https://laravel.com/docs/container).
16
+- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
17
+- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
18
+- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
19
+- [Robust background job processing](https://laravel.com/docs/queues).
20
+- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
21
+
22
+Laravel is accessible, powerful, and provides tools required for large, robust applications.
23
+
24
+## Learning Laravel
25
+
26
+Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
27
+
28
+You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
29
+
30
+If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
31
+
32
+## Laravel Sponsors
33
+
34
+We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
35
+
36
+### Premium Partners
37
+
38
+- **[Vehikl](https://vehikl.com/)**
39
+- **[Tighten Co.](https://tighten.co)**
40
+- **[WebReinvent](https://webreinvent.com/)**
41
+- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
42
+- **[64 Robots](https://64robots.com)**
43
+- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
44
+- **[Cyber-Duck](https://cyber-duck.co.uk)**
45
+- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
46
+- **[Jump24](https://jump24.co.uk)**
47
+- **[Redberry](https://redberry.international/laravel/)**
48
+- **[Active Logic](https://activelogic.com)**
49
+- **[byte5](https://byte5.de)**
50
+- **[OP.GG](https://op.gg)**
51
+
52
+## Contributing
53
+
54
+Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
55
+
56
+## Code of Conduct
57
+
58
+In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
59
+
60
+## Security Vulnerabilities
61
+
62
+If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
63
+
64
+## License
65
+
66
+The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

+ 1
- 0
Untitled 파일 보기

@@ -0,0 +1 @@
1
+REDIS_CLIENT

+ 0
- 0
aimon_api 파일 보기


+ 35
- 0
app/Actions/Fortify/CreateNewUser.php 파일 보기

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Contracts\CreatesNewUsers;
9
+use Laravel\Jetstream\Jetstream;
10
+
11
+class CreateNewUser implements CreatesNewUsers
12
+{
13
+    use PasswordValidationRules;
14
+
15
+    /**
16
+     * Validate and create a newly registered user.
17
+     *
18
+     * @param  array<string, string>  $input
19
+     */
20
+    public function create(array $input): User
21
+    {
22
+        Validator::make($input, [
23
+            'name' => ['required', 'string', 'max:255'],
24
+            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
25
+            'password' => $this->passwordRules(),
26
+            'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
27
+        ])->validate();
28
+
29
+        return User::create([
30
+            'name' => $input['name'],
31
+            'email' => $input['email'],
32
+            'password' => Hash::make($input['password']),
33
+        ]);
34
+    }
35
+}

+ 18
- 0
app/Actions/Fortify/PasswordValidationRules.php 파일 보기

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use Illuminate\Validation\Rules\Password;
6
+
7
+trait PasswordValidationRules
8
+{
9
+    /**
10
+     * Get the validation rules used to validate passwords.
11
+     *
12
+     * @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
13
+     */
14
+    protected function passwordRules(): array
15
+    {
16
+        return ['required', 'string', Password::default(), 'confirmed'];
17
+    }
18
+}

+ 29
- 0
app/Actions/Fortify/ResetUserPassword.php 파일 보기

@@ -0,0 +1,29 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Contracts\ResetsUserPasswords;
9
+
10
+class ResetUserPassword implements ResetsUserPasswords
11
+{
12
+    use PasswordValidationRules;
13
+
14
+    /**
15
+     * Validate and reset the user's forgotten password.
16
+     *
17
+     * @param  array<string, string>  $input
18
+     */
19
+    public function reset(User $user, array $input): void
20
+    {
21
+        Validator::make($input, [
22
+            'password' => $this->passwordRules(),
23
+        ])->validate();
24
+
25
+        $user->forceFill([
26
+            'password' => Hash::make($input['password']),
27
+        ])->save();
28
+    }
29
+}

+ 32
- 0
app/Actions/Fortify/UpdateUserPassword.php 파일 보기

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Contracts\UpdatesUserPasswords;
9
+
10
+class UpdateUserPassword implements UpdatesUserPasswords
11
+{
12
+    use PasswordValidationRules;
13
+
14
+    /**
15
+     * Validate and update the user's password.
16
+     *
17
+     * @param  array<string, string>  $input
18
+     */
19
+    public function update(User $user, array $input): void
20
+    {
21
+        Validator::make($input, [
22
+            'current_password' => ['required', 'string', 'current_password:web'],
23
+            'password' => $this->passwordRules(),
24
+        ], [
25
+            'current_password.current_password' => __('The provided password does not match your current password.'),
26
+        ])->validateWithBag('updatePassword');
27
+
28
+        $user->forceFill([
29
+            'password' => Hash::make($input['password']),
30
+        ])->save();
31
+    }
32
+}

+ 56
- 0
app/Actions/Fortify/UpdateUserProfileInformation.php 파일 보기

@@ -0,0 +1,56 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Contracts\Auth\MustVerifyEmail;
7
+use Illuminate\Support\Facades\Validator;
8
+use Illuminate\Validation\Rule;
9
+use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
10
+
11
+class UpdateUserProfileInformation implements UpdatesUserProfileInformation
12
+{
13
+    /**
14
+     * Validate and update the given user's profile information.
15
+     *
16
+     * @param  array<string, mixed>  $input
17
+     */
18
+    public function update(User $user, array $input): void
19
+    {
20
+        Validator::make($input, [
21
+            'name' => ['required', 'string', 'max:255'],
22
+            'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
23
+            'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
24
+        ])->validateWithBag('updateProfileInformation');
25
+
26
+        if (isset($input['photo'])) {
27
+            $user->updateProfilePhoto($input['photo']);
28
+        }
29
+
30
+        if ($input['email'] !== $user->email &&
31
+            $user instanceof MustVerifyEmail) {
32
+            $this->updateVerifiedUser($user, $input);
33
+        } else {
34
+            $user->forceFill([
35
+                'name' => $input['name'],
36
+                'email' => $input['email'],
37
+            ])->save();
38
+        }
39
+    }
40
+
41
+    /**
42
+     * Update the given verified user's profile information.
43
+     *
44
+     * @param  array<string, string>  $input
45
+     */
46
+    protected function updateVerifiedUser(User $user, array $input): void
47
+    {
48
+        $user->forceFill([
49
+            'name' => $input['name'],
50
+            'email' => $input['email'],
51
+            'email_verified_at' => null,
52
+        ])->save();
53
+
54
+        $user->sendEmailVerificationNotification();
55
+    }
56
+}

+ 19
- 0
app/Actions/Jetstream/DeleteUser.php 파일 보기

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Actions\Jetstream;
4
+
5
+use App\Models\User;
6
+use Laravel\Jetstream\Contracts\DeletesUsers;
7
+
8
+class DeleteUser implements DeletesUsers
9
+{
10
+    /**
11
+     * Delete the given user.
12
+     */
13
+    public function delete(User $user): void
14
+    {
15
+        $user->deleteProfilePhoto();
16
+        $user->tokens->each->delete();
17
+        $user->delete();
18
+    }
19
+}

+ 75
- 0
app/Console/Commands/GeneraSegnalazioniContratto.php 파일 보기

@@ -0,0 +1,75 @@
1
+<?php
2
+
3
+namespace App\Console\Commands;
4
+
5
+use App\Models\Annotazione;
6
+use App\Models\ContrattoServizioTodo;
7
+use App\Models\Segnalazione;
8
+use Carbon\Carbon;
9
+use Illuminate\Console\Command;
10
+use Illuminate\Support\Facades\DB;
11
+
12
+class GeneraSegnalazioniContratto extends Command
13
+{
14
+    protected $signature = 'app:genera-segnalazioni-contratto {--dry-run : Mostra senza creare}';
15
+
16
+    protected $description = 'Crea segnalazioni automatiche dai todo scaduti dei contratti servizio';
17
+
18
+    public function handle(): int
19
+    {
20
+        $oggi = Carbon::today();
21
+        $todos = ContrattoServizioTodo::query()
22
+            ->with(['contrattoServizio.user'])
23
+            ->where('attivo', true)
24
+            ->whereNotNull('data_prossima')
25
+            ->whereDate('data_prossima', '<=', $oggi->toDateString())
26
+            ->whereHas('contrattoServizio', fn ($q) => $q->attivi())
27
+            ->get();
28
+
29
+        $this->info('Todo da elaborare: '.$todos->count());
30
+        $created = 0;
31
+
32
+        foreach ($todos as $todo) {
33
+            $contratto = $todo->contrattoServizio;
34
+            if (! $contratto) {
35
+                continue;
36
+            }
37
+
38
+            $titolo = $todo->titolo ?: 'Attività contratto';
39
+            $this->line(" - [{$todo->id}] {$titolo} (cliente #{$contratto->user_id})");
40
+
41
+            if ($this->option('dry-run')) {
42
+                continue;
43
+            }
44
+
45
+            DB::transaction(function () use ($todo, $contratto, $titolo, &$created) {
46
+                $segnalazione = new Segnalazione();
47
+                $segnalazione->titolo = $titolo;
48
+                $segnalazione->stato = 'nuova';
49
+                $segnalazione->priorita = $todo->priorita;
50
+                $segnalazione->tipo = $todo->tipo_segnalazione;
51
+                $segnalazione->origine = 'contratto';
52
+                $segnalazione->contratto_servizio_id = $contratto->id;
53
+                $segnalazione->contratto_servizio_todo_id = $todo->id;
54
+                $segnalazione->azienda_id = $contratto->user?->is_azienda ? $contratto->user_id : ($contratto->user?->azienda_id);
55
+                $segnalazione->user_id = $contratto->user_id;
56
+                $segnalazione->notificaApertaDa = false;
57
+                $segnalazione->save();
58
+
59
+                $annotazione = new Annotazione();
60
+                $annotazione->segnalazione_id = $segnalazione->id;
61
+                $annotazione->user_id = $contratto->user_id;
62
+                $annotazione->text = $todo->descrizione ?: ('Attività automatica da contratto: '.$contratto->label);
63
+                $annotazione->riservata = false;
64
+                $annotazione->save();
65
+
66
+                $todo->avanzaDopoGenerazione();
67
+                $created++;
68
+            });
69
+        }
70
+
71
+        $this->info($this->option('dry-run') ? 'Dry-run completato.' : "Create {$created} segnalazioni.");
72
+
73
+        return self::SUCCESS;
74
+    }
75
+}

+ 36
- 0
app/Console/Commands/NumeraTicket.php 파일 보기

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace App\Console\Commands;
4
+
5
+use Illuminate\Console\Command;
6
+use App\Models\Segnalazione;
7
+use Carbon\Carbon;
8
+
9
+class NumeraTicket extends Command
10
+{
11
+    /**
12
+     * The name and signature of the console command.
13
+     *
14
+     * @var string
15
+     */
16
+    protected $signature = 'app:numera-ticket';
17
+
18
+    /**
19
+     * The console command description.
20
+     *
21
+     * @var string
22
+     */
23
+    protected $description = 'Command description';
24
+
25
+    /**
26
+     * Execute the console command.
27
+     */
28
+    public function handle()
29
+    {
30
+        foreach(Segnalazione::all() as $segnalazione){
31
+          $maxNumero = Segnalazione::where('created_at', 'LIKE', Carbon::now()->format('Y').'-%')->max('numero');
32
+          $segnalazione->numero = $maxNumero ? $maxNumero + 1 : 1;
33
+          $segnalazione->save();
34
+        }
35
+    }
36
+}

+ 85
- 0
app/DataTables/AziendaDataTable.php 파일 보기

@@ -0,0 +1,85 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Azienda;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Services\DataTable;
13
+
14
+class AziendaDataTable extends DataTable
15
+{
16
+    public function __construct()
17
+    {
18
+        $this->dataTableVariable = 'dataTable_azienda';
19
+    }
20
+
21
+    /**
22
+     * @param  QueryBuilder<Azienda>  $query
23
+     */
24
+    public function dataTable(QueryBuilder $query): EloquentDataTable
25
+    {
26
+        return (new EloquentDataTable($query))
27
+            ->addColumn('action', function ($entity) {
28
+                return view('azienda.menu', ['entity' => $entity]);
29
+            })
30
+            ->addColumn('ragione_display', function ($entity) {
31
+                return $entity->full_name ?: '—';
32
+            })
33
+            ->addColumn('utenti_conta', function ($entity) {
34
+                return $entity->utenti()->soloUtenti()->count();
35
+            })
36
+            ->setRowId('id');
37
+    }
38
+
39
+    /**
40
+     * @return QueryBuilder<Azienda>
41
+     */
42
+    public function query(Azienda $model): QueryBuilder
43
+    {
44
+        return $model->newQuery();
45
+    }
46
+
47
+    public function html(): HtmlBuilder
48
+    {
49
+        $buttons = [];
50
+        if (Auth::user()->can('create-azienda')) {
51
+            $buttons[] = Button::make('create')
52
+                ->className('btn btn-sm btn-primary mb-4')
53
+                ->action('openAziendaModal()')
54
+                ->text('<i class="fas fa-plus"></i> Nuova azienda');
55
+        }
56
+
57
+        return $this->builder()
58
+            ->setTableId($this->dataTableVariable)
59
+            ->columns($this->getColumns())
60
+            ->buttons($buttons)
61
+            ->language(asset('assets/Italian.json'))
62
+            ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
63
+            ->responsive()
64
+            ->minifiedAjax()
65
+            ->orderBy(0, 'asc')
66
+            ->initComplete('function(){ initComplete_azienda(); }');
67
+    }
68
+
69
+    public function getColumns(): array
70
+    {
71
+        return [
72
+            Column::make('ragione_sociale')->title('Azienda')->data('ragione_display')->name('ragione_sociale')->responsivePriority(1),
73
+            Column::make('piva')->title('P. IVA')->responsivePriority(3),
74
+            Column::make('email')->title('Email')->responsivePriority(2),
75
+            Column::make('telefono')->title('Telefono'),
76
+            Column::make('utenti_conta')->title('Utenti')->attributes(['class' => 'text-center'])->searchable(false)->orderable(false),
77
+            Column::computed('action')
78
+                ->exportable(false)
79
+                ->printable(false)
80
+                ->width(60)
81
+                ->title(' ')
82
+                ->addClass('text-center'),
83
+        ];
84
+    }
85
+}

+ 144
- 0
app/DataTables/ConfiguraSegnalazioneDataTable.php 파일 보기

@@ -0,0 +1,144 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ConfiguraSegnalazione;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Html\Editor\Editor;
13
+use Yajra\DataTables\Html\Editor\Fields;
14
+use Yajra\DataTables\Services\DataTable;
15
+
16
+class ConfiguraSegnalazioneDataTable extends DataTable
17
+{
18
+  public function __construct()
19
+  {
20
+    $this->dataTableVariable = 'dataTable_configurasegnalazione';
21
+  }
22
+
23
+  /**
24
+  * Build the DataTable class.
25
+  *
26
+  * @param  QueryBuilder<ConfiguraSegnalazione>  $query  Results from query() method.
27
+  */
28
+  public function dataTable(QueryBuilder $query): EloquentDataTable
29
+  {
30
+    return (new EloquentDataTable($query))
31
+    ->addColumn('action', function ($entity) {
32
+      return view('configura_segnalazione.menu', ['entity' => $entity]);
33
+    })
34
+    ->addColumn('icona_display', function ($entity) {
35
+      $html = "<div class='rounded-5 p-1 justify-content-center align-items-center badge bg-label-".$entity->colore."'>";
36
+      $html .= '<i class="bx icon-base '.$entity->icona.'"></i>';
37
+      $html .= '</div>';
38
+
39
+      return $html;
40
+    })
41
+    ->addColumn('colore_display', function ($entity) {
42
+      return '<span class="badge bg-label-'.$entity->colore.'">'.$entity->label.'</span>';
43
+    })
44
+    ->rawColumns(['icona_display', 'colore_display'])
45
+    ->setRowId('id');
46
+  }
47
+
48
+  /**
49
+  * Get the query source of dataTable.
50
+  *
51
+  * @return QueryBuilder<ConfiguraSegnalazione>
52
+  */
53
+  public function query(ConfiguraSegnalazione $model): QueryBuilder
54
+  {
55
+    return $model->newQuery();
56
+  }
57
+
58
+  /**
59
+  * Optional method if you want to use the html builder.
60
+  */
61
+  public function html(): HtmlBuilder
62
+  {
63
+    $buttons = [];
64
+    // if(Auth::user()->can('create-configura-segnalazione')){
65
+    array_push($buttons, Button::make('create')
66
+    ->editor('editor')
67
+    ->className('btn btn-sm btn-primary mb-4')
68
+    ->formTitle('Crea nuova configurazione')
69
+    ->formButtons(
70
+      [Button::raw('Annulla')
71
+      ->className('btn btn-secondary mx-2')
72
+      ->actionClose(),
73
+      Button::raw('Salva')
74
+      ->className('btn btn-success ml-2')
75
+      ->actionHandler('create')]
76
+      )
77
+      ->text('<i class="fas fa-plus"></i> Nuova configurazione'));
78
+      // }
79
+
80
+      return $this->builder()
81
+      ->setTableId($this->dataTableVariable)
82
+      ->columns($this->getColumns())
83
+      ->buttons($buttons)
84
+      ->language(asset('assets/Italian.json'))
85
+      ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
86
+      ->responsive()
87
+      ->minifiedAjax()
88
+      ->pageLength(100)
89
+      ->rowGroupDataSrc(['gruppo'])
90
+      ->orderBy(2, 'asc')
91
+      ->orderBy(8, 'asc')
92
+      ->initComplete('function(settings, json){
93
+        initComplete_configurasegnalazione();
94
+                    }')
95
+      ->editor(
96
+        Editor::make()
97
+        ->ajax(route('admin.configura-segnalazione.store'))
98
+        ->fields([
99
+          Fields\Text::make('label', 'Label')->label('Nome configurazione'),
100
+          Fields\Text::make('descrizione', 'Descrizione'),
101
+          Fields\Text::make('gruppo', 'Gruppo'),
102
+          Fields\Text::make('key', 'Key'),
103
+          Fields\Text::make('value', 'Value'),
104
+          Fields\TextArea::make('opzioni', 'Opzioni')->label('Opzioni (JSON o elenco)'),
105
+          Fields\Text::make('type', 'Type'),
106
+          Fields\Text::make('order', 'Order'),
107
+          Fields\Text::make('icona', 'Icona'),
108
+          Fields\Text::make('colore', 'Colore'),
109
+          ])
110
+        );
111
+      }
112
+
113
+      /**
114
+      * Get the dataTable columns definition.
115
+      */
116
+      public function getColumns(): array
117
+      {
118
+        return [
119
+          Column::make('label')->title('Nome'),
120
+          Column::make('descrizione')->title('Descrizione'),
121
+          Column::make('gruppo')->title('Gruppo')->visible(false),
122
+          Column::make('icona')->title('Icona')->data('icona_display'),
123
+          Column::make('colore')->title('Colore')->data('colore_display'),
124
+          Column::make('key')->title('Key'),
125
+          Column::make('value')->title('Value'),
126
+          Column::make('type')->title('Type'),
127
+          Column::make('order')->title('Order'),
128
+          Column::computed('action')
129
+          ->exportable(false)
130
+          ->printable(false)
131
+          ->width('10%')
132
+          ->title(' ')
133
+          ->addClass('text-end'),
134
+        ];
135
+      }
136
+
137
+      /**
138
+      * Get the filename for export.
139
+      */
140
+      protected function filename(): string
141
+      {
142
+        return 'ConfiguraSegnalazione_'.date('YmdHis');
143
+      }
144
+    }

+ 136
- 0
app/DataTables/ConfiguraSegnalazioneDataTableEditor.php 파일 보기

@@ -0,0 +1,136 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ConfiguraSegnalazione;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Validation\Rule;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Validation\ValidationException;
11
+use Storage;
12
+
13
+class ConfiguraSegnalazioneDataTableEditor extends DataTablesEditor
14
+{
15
+  protected $model = ConfiguraSegnalazione::class;
16
+  // protected $uploadDir = '';
17
+  // protected $disk = 'modelliRicevute';
18
+
19
+  protected $messages = [
20
+    'label.required' => 'Il label è richiesto',
21
+    'descrizione.required' => 'La descrizione è richiesta',
22
+    'gruppo.required' => 'Il gruppo è richiesto',
23
+    'key.required' => 'La chiave è richiesta',
24
+    'value.required' => 'Il valore è richiesto',
25
+    'type.required' => 'Il tipo è richiesto',
26
+    'order.required' => 'L\'ordine è richiesto',
27
+  ];
28
+
29
+  /**
30
+  * Get create action validation rules.
31
+  *
32
+  * @return array
33
+  */
34
+  public function createRules(): array
35
+  {
36
+    return [
37
+      'label'  => 'required',
38
+      'descrizione' => 'required',
39
+      'gruppo' => 'required',
40
+      'key' => 'required',
41
+      'value' => 'required',
42
+      'opzioni' => 'nullable',
43
+      'type' => 'required',
44
+      'order' => 'required',
45
+    ];
46
+  }
47
+
48
+  public function createMessages(): array{
49
+    return $this->messages;
50
+  }
51
+
52
+  /**
53
+  * Get edit action validation rules.
54
+  *
55
+  * @param Model $model
56
+  * @return array
57
+  */
58
+  public function editRules(Model $model): array
59
+  {
60
+    return [
61
+      'label'  => 'required',
62
+      'descrizione' => 'required',
63
+      'gruppo' => 'required',
64
+      'key' => 'required',
65
+      'value' => 'required',
66
+      'opzioni' => 'nullable',
67
+      'type' => 'required',
68
+      'order' => 'required',
69
+    ];
70
+  }
71
+
72
+  public function editMessages(): array{
73
+    return $this->messages;
74
+  }
75
+
76
+  /**
77
+  * Get remove action validation rules.
78
+  *
79
+  * @param Model $model
80
+  * @return array
81
+  */
82
+  public function removeRules(Model $model): array
83
+  {
84
+    return [];
85
+  }
86
+
87
+  public function creating(Model $model, array $data): array
88
+  {
89
+    return $this->normalizeOpzioni($data);
90
+  }
91
+
92
+  public function updating(Model $model, array $data): array
93
+  {
94
+    return $this->normalizeOpzioni($data);
95
+  }
96
+
97
+  private function normalizeOpzioni(array $data): array
98
+  {
99
+    if (!array_key_exists('opzioni', $data)) {
100
+      return $data;
101
+    }
102
+
103
+    $value = $data['opzioni'];
104
+    if ($value === null) {
105
+      return $data;
106
+    }
107
+
108
+    if (is_string($value)) {
109
+      $trimmed = trim($value);
110
+      if ($trimmed === '') {
111
+        $data['opzioni'] = null;
112
+        return $data;
113
+      }
114
+
115
+      $decoded = json_decode($trimmed, true);
116
+      if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
117
+        $data['opzioni'] = $decoded;
118
+        return $data;
119
+      }
120
+
121
+      $parts = preg_split('/[\r\n,]+/', $trimmed);
122
+      $parts = array_filter(array_map('trim', $parts), function ($item) {
123
+        return $item !== '';
124
+      });
125
+      $data['opzioni'] = array_values($parts);
126
+      return $data;
127
+    }
128
+
129
+    if (is_array($value)) {
130
+      $data['opzioni'] = $value;
131
+    }
132
+
133
+    return $data;
134
+  }
135
+
136
+}

+ 115
- 0
app/DataTables/ContrattoServizioDataTable.php 파일 보기

@@ -0,0 +1,115 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ContrattoServizio;
6
+use App\Models\ContrattoTemplate;
7
+use App\Models\Prodotto;
8
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
9
+use Illuminate\Support\Facades\Auth;
10
+use Yajra\DataTables\EloquentDataTable;
11
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
12
+use Yajra\DataTables\Html\Button;
13
+use Yajra\DataTables\Html\Column;
14
+use Yajra\DataTables\Html\Editor\Editor;
15
+use Yajra\DataTables\Html\Editor\Fields;
16
+use Yajra\DataTables\Services\DataTable;
17
+
18
+class ContrattoServizioDataTable extends DataTable
19
+{
20
+  public function __construct()
21
+  {
22
+    $this->dataTableVariable = 'dataTable_contratto_servizio';
23
+  }
24
+
25
+  public function dataTable(QueryBuilder $query): EloquentDataTable
26
+  {
27
+    return (new EloquentDataTable($query))
28
+      ->addColumn('action', fn ($e) => view('contratto_servizio.menu', ['entity' => $e, 'mode' => 'embed']))
29
+      ->addColumn('template_label', fn ($e) => $e->contrattoTemplate ? $e->contrattoTemplate->nome : '—')
30
+      ->addColumn('costo_label', fn ($e) => $e->costo !== null
31
+        ? number_format((float) $e->costo, 2, ',', '.').' €'
32
+        : '—')
33
+      ->addColumn('righe_count', fn ($e) => $e->contratto_servizio_rigas_count ?? $e->contrattoServizioRigas()->count())
34
+      ->addColumn('periodo_label', fn ($e) => Prodotto::PERIODI[$e->periodo]
35
+        ?? (\App\Models\ContrattoTemplate::PERIODI[$e->periodo] ?? ($e->periodo ?: '—')))
36
+      ->addColumn('date_label', function ($e) {
37
+        $i = $e->data_inizio ? \Carbon\Carbon::parse($e->data_inizio)->format('d/m/Y') : '—';
38
+        $f = $e->data_fine ? \Carbon\Carbon::parse($e->data_fine)->format('d/m/Y') : 'aperto';
39
+
40
+        return $i.' → '.$f;
41
+      })
42
+      ->addColumn('flags_label', function ($e) {
43
+        return $e->attivo
44
+          ? '<span class="badge bg-label-success">Attivo</span>'
45
+          : '<span class="badge bg-label-secondary">No</span>';
46
+      })
47
+      ->editColumn('attivo', fn ($e) => $e->attivo ? 1 : 0)
48
+      ->rawColumns(['flags_label', 'action'])
49
+      ->setRowId('id');
50
+  }
51
+
52
+  public function query(ContrattoServizio $model): QueryBuilder
53
+  {
54
+    $q = $model->newQuery()->with('contrattoTemplate')->withCount('contrattoServizioRigas');
55
+    if ($this->user) {
56
+      $q->where('user_id', $this->user->id);
57
+    }
58
+
59
+    return $q;
60
+  }
61
+
62
+  public function html(): HtmlBuilder
63
+  {
64
+    $buttons = [];
65
+    if (Auth::user()->can('create-contratto-servizio')) {
66
+      $buttons[] = Button::make('create')
67
+        ->className('btn btn-sm btn-primary mb-4')
68
+        ->text('<i class="fas fa-plus"></i> Aggiungi')
69
+        ->action('window.location.href="'.route('admin.contratto_servizio.create', ['user_id' => $this->user->id]).'"');
70
+    }
71
+
72
+    $templates = ContrattoTemplate::where('attivo', true)->orderBy('nome')->pluck('id', 'nome');
73
+    $periodi = array_merge(Prodotto::PERIODI, ContrattoTemplate::PERIODI);
74
+
75
+    return $this->builder()
76
+      ->setTableId($this->dataTableVariable)
77
+      ->columns($this->getColumns())
78
+      ->buttons($buttons)
79
+      ->language(asset('assets/Italian.json'))
80
+      ->dom('<"py-2"<"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
81
+      ->responsive()
82
+      ->minifiedAjax(route('admin.contratto_servizio.index', ['user_id' => $this->user->id]))
83
+      ->orderBy(0, 'desc')
84
+      ->initComplete('function(){ $("div.dt-buttons button").removeClass("btn-secondary"); }')
85
+      ->editor(
86
+        Editor::make()
87
+          ->ajax(route('admin.contratto_servizio.store'))
88
+          ->fields([
89
+            Fields\Hidden::make('user_id')->default($this->user->id),
90
+            Fields\Select::make('contratto_template_id')->label('Template contratto')->options($templates)->placeholder('— nessuno —'),
91
+            Fields\Text::make('nome')->label('Nome contratto'),
92
+            Fields\Text::make('costo')->label('Costo (€)'),
93
+            Fields\Select::make('periodo')->label('Periodo')->options(array_flip($periodi))->placeholder('—'),
94
+            Fields\Date::make('data_inizio')->label('Data inizio')->format('YYYY-MM-DD'),
95
+            Fields\Date::make('data_fine')->label('Data fine')->format('YYYY-MM-DD'),
96
+            Fields\Radio::make('attivo')->label('Attivo')->options(['1' => 'Sì', '0' => 'No'])->default(1),
97
+            Fields\TextArea::make('note')->label('Note'),
98
+          ])
99
+      );
100
+  }
101
+
102
+  public function getColumns(): array
103
+  {
104
+    return [
105
+      Column::make('nome')->title('Contratto'),
106
+      Column::make('contratto_template_id')->title('Template')->data('template_label'),
107
+      Column::make('costo')->title('Costo')->data('costo_label')->searchable(false),
108
+      Column::computed('righe_count')->title('Servizi')->searchable(false),
109
+      Column::make('periodo')->title('Periodo')->data('periodo_label'),
110
+      Column::make('data_inizio')->title('Validità')->data('date_label')->searchable(false),
111
+      Column::make('attivo')->title('Stato')->data('flags_label')->searchable(false)->orderable(false),
112
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
113
+    ];
114
+  }
115
+}

+ 121
- 0
app/DataTables/ContrattoServizioDataTableEditor.php 파일 보기

@@ -0,0 +1,121 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ContrattoServizio;
6
+use App\Models\ContrattoTemplate;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Yajra\DataTables\DataTablesEditor;
9
+
10
+class ContrattoServizioDataTableEditor extends DataTablesEditor
11
+{
12
+  protected $model = ContrattoServizio::class;
13
+
14
+  protected $prevTemplateId = null;
15
+
16
+  protected $messages = [
17
+    'user_id.required' => 'Utente richiesto',
18
+  ];
19
+
20
+  public function createRules(): array
21
+  {
22
+    return [
23
+      'user_id' => 'required|exists:users,id',
24
+      'nome' => 'nullable|string|max:255',
25
+      'contratto_template_id' => 'nullable|exists:contratto_template,id',
26
+      'costo' => 'nullable|numeric|min:0',
27
+      'data_inizio' => 'nullable|date',
28
+      'data_fine' => 'nullable|date',
29
+    ];
30
+  }
31
+
32
+  public function createMessages(): array
33
+  {
34
+    return $this->messages;
35
+  }
36
+
37
+  public function editRules(Model $model): array
38
+  {
39
+    return $this->createRules();
40
+  }
41
+
42
+  public function editMessages(): array
43
+  {
44
+    return $this->messages;
45
+  }
46
+
47
+  public function removeRules(Model $model): array
48
+  {
49
+    return [];
50
+  }
51
+
52
+  public function creating(Model $model, array $data): array
53
+  {
54
+    return $this->normalize($data);
55
+  }
56
+
57
+  public function created(Model $model, array $data)
58
+  {
59
+    if (! empty($model->contratto_template_id)) {
60
+      $model->importaDaTemplate();
61
+    }
62
+  }
63
+
64
+  public function updating(Model $model, array $data): array
65
+  {
66
+    $this->prevTemplateId = $model->contratto_template_id;
67
+
68
+    return $this->normalize($data);
69
+  }
70
+
71
+  public function updated(Model $model, array $data)
72
+  {
73
+    if ($model->contratto_template_id && (int) $model->contratto_template_id !== (int) $this->prevTemplateId) {
74
+      $model->importaDaTemplate();
75
+    }
76
+  }
77
+
78
+  private function normalize(array $data): array
79
+  {
80
+    foreach (['contratto_template_id', 'periodo', 'data_fine', 'nome', 'note', 'costo'] as $key) {
81
+      if (array_key_exists($key, $data) && $data[$key] === '') {
82
+        $data[$key] = null;
83
+      }
84
+    }
85
+    foreach (['attivo'] as $flag) {
86
+      if (array_key_exists($flag, $data)) {
87
+        $data[$flag] = filter_var($data[$flag], FILTER_VALIDATE_BOOLEAN) || $data[$flag] === '1' || $data[$flag] === 1;
88
+      }
89
+    }
90
+
91
+    if (! empty($data['contratto_template_id'])) {
92
+      $tpl = ContrattoTemplate::find($data['contratto_template_id']);
93
+      if ($tpl) {
94
+        if (empty($data['nome'])) {
95
+          $data['nome'] = $tpl->nome;
96
+        }
97
+        if (($data['costo'] ?? null) === null && $tpl->costo_base !== null) {
98
+          $data['costo'] = $tpl->costo_base;
99
+        }
100
+        if (empty($data['periodo']) && $tpl->periodo) {
101
+          $data['periodo'] = $tpl->periodo;
102
+        }
103
+      }
104
+    }
105
+
106
+    // Campi legacy / gestiti sulle righe: non esporre in editor
107
+    unset(
108
+      $data['prodotto_id'],
109
+      $data['quantita'],
110
+      $data['unita_misura'],
111
+      $data['prezzo_unitario'],
112
+      $data['collegabile_segnalazioni'],
113
+      $data['quantita_residua']
114
+    );
115
+
116
+    $data['collegabile_segnalazioni'] = false;
117
+    $data['quantita_residua'] = null;
118
+
119
+    return $data;
120
+  }
121
+}

+ 89
- 0
app/DataTables/ContrattoServizioIndexDataTable.php 파일 보기

@@ -0,0 +1,89 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ContrattoServizio;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Services\DataTable;
13
+
14
+class ContrattoServizioIndexDataTable extends DataTable
15
+{
16
+  public function __construct()
17
+  {
18
+    $this->dataTableVariable = 'dataTable_contratto_servizio_index';
19
+  }
20
+
21
+  public function dataTable(QueryBuilder $query): EloquentDataTable
22
+  {
23
+    return (new EloquentDataTable($query))
24
+      ->addColumn('action', fn ($e) => view('contratto_servizio.menu', ['entity' => $e, 'mode' => 'index']))
25
+      ->addColumn('cliente_label', fn ($e) => $e->user
26
+        ? $e->user->full_name.($e->user->is_azienda ? ' (azienda)' : '')
27
+        : '—')
28
+      ->addColumn('template_label', fn ($e) => $e->contrattoTemplate ? $e->contrattoTemplate->nome : '—')
29
+      ->addColumn('costo_label', fn ($e) => $e->costo !== null
30
+        ? number_format((float) $e->costo, 2, ',', '.').' €'
31
+        : '—')
32
+      ->addColumn('periodo_label', fn ($e) => $e->periodo_label)
33
+      ->addColumn('date_label', function ($e) {
34
+        $i = $e->data_inizio ? \Carbon\Carbon::parse($e->data_inizio)->format('d/m/Y') : '—';
35
+        $f = $e->data_fine ? \Carbon\Carbon::parse($e->data_fine)->format('d/m/Y') : 'aperto';
36
+
37
+        return $i.' → '.$f;
38
+      })
39
+      ->addColumn('flags_label', function ($e) {
40
+        return $e->attivo
41
+          ? '<span class="badge bg-label-success">Attivo</span>'
42
+          : '<span class="badge bg-label-secondary">No</span>';
43
+      })
44
+      ->rawColumns(['flags_label', 'action'])
45
+      ->setRowId('id');
46
+  }
47
+
48
+  public function query(ContrattoServizio $model): QueryBuilder
49
+  {
50
+    return $model->newQuery()->with(['user', 'contrattoTemplate']);
51
+  }
52
+
53
+  public function html(): HtmlBuilder
54
+  {
55
+    $buttons = [];
56
+    if (Auth::user()->can('create-contratto-servizio')) {
57
+      $buttons[] = Button::make('create')
58
+        ->className('btn btn-sm btn-primary mb-4')
59
+        ->text('<i class="fas fa-plus"></i> Nuovo contratto')
60
+        ->action('window.location.href="'.route('admin.contratto_servizio.create').'"');
61
+    }
62
+
63
+    return $this->builder()
64
+      ->setTableId($this->dataTableVariable)
65
+      ->columns($this->getColumns())
66
+      ->buttons($buttons)
67
+      ->language(asset('assets/Italian.json'))
68
+      ->dom('<"py-2"<"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
69
+      ->responsive()
70
+      ->minifiedAjax()
71
+      ->orderBy(0, 'desc')
72
+      ->initComplete('function(){ $("div.dt-buttons button").removeClass("btn-secondary"); }');
73
+  }
74
+
75
+  public function getColumns(): array
76
+  {
77
+    return [
78
+      Column::make('id')->title('ID')->width(50),
79
+      Column::make('nome')->title('Contratto'),
80
+      Column::make('user_id')->title('Cliente / azienda')->data('cliente_label')->name('user_id'),
81
+      Column::make('contratto_template_id')->title('Template')->data('template_label'),
82
+      Column::make('costo')->title('Costo')->data('costo_label')->searchable(false),
83
+      Column::make('periodo')->title('Periodo')->data('periodo_label'),
84
+      Column::make('data_inizio')->title('Validità')->data('date_label')->searchable(false),
85
+      Column::make('attivo')->title('Stato')->data('flags_label')->searchable(false)->orderable(false),
86
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
87
+    ];
88
+  }
89
+}

+ 78
- 0
app/DataTables/ContrattoTemplateDataTable.php 파일 보기

@@ -0,0 +1,78 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\ContrattoTemplate;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Services\DataTable;
13
+
14
+class ContrattoTemplateDataTable extends DataTable
15
+{
16
+  public function __construct()
17
+  {
18
+    $this->dataTableVariable = 'dataTable_contratto_template';
19
+  }
20
+
21
+  public function dataTable(QueryBuilder $query): EloquentDataTable
22
+  {
23
+    return (new EloquentDataTable($query))
24
+      ->addColumn('action', fn ($e) => view('contratto_template.menu', ['entity' => $e]))
25
+      ->addColumn('periodo_label', fn ($e) => $e->periodo_label)
26
+      ->addColumn('attivo_label', fn ($e) => $e->attivo
27
+        ? '<span class="badge bg-label-success">Attivo</span>'
28
+        : '<span class="badge bg-label-secondary">No</span>')
29
+      ->addColumn('todos_count', fn ($e) => $e->contratto_template_todos_count ?? 0)
30
+      ->addColumn('righe_count', fn ($e) => $e->contratto_template_rigas_count ?? 0)
31
+      ->addColumn('costo_label', fn ($e) => $e->costo_base !== null
32
+        ? number_format((float) $e->costo_base, 2, ',', '.').' €'
33
+        : '—')
34
+      ->rawColumns(['attivo_label', 'action'])
35
+      ->setRowId('id');
36
+  }
37
+
38
+  public function query(ContrattoTemplate $model): QueryBuilder
39
+  {
40
+    return $model->newQuery()->withCount(['contrattoTemplateTodos', 'contrattoTemplateRigas']);
41
+  }
42
+
43
+  public function html(): HtmlBuilder
44
+  {
45
+    $buttons = [];
46
+    if (Auth::user()->can('create-contratto-template')) {
47
+      $buttons[] = Button::make('create')
48
+        ->className('btn btn-sm btn-primary mb-4')
49
+        ->text('<i class="fas fa-plus"></i> Nuovo template')
50
+        ->action('window.location.href="'.route('admin.contratto_template.create').'"');
51
+    }
52
+
53
+    return $this->builder()
54
+      ->setTableId($this->dataTableVariable)
55
+      ->columns($this->getColumns())
56
+      ->buttons($buttons)
57
+      ->language(asset('assets/Italian.json'))
58
+      ->dom('<"py-2"<"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
59
+      ->responsive()
60
+      ->minifiedAjax()
61
+      ->orderBy(1)
62
+      ->initComplete('function(){ $("div.dt-buttons button").removeClass("btn-secondary"); }');
63
+  }
64
+
65
+  public function getColumns(): array
66
+  {
67
+    return [
68
+      Column::make('codice')->title('Codice'),
69
+      Column::make('nome')->title('Nome'),
70
+      Column::make('periodo')->title('Periodo')->data('periodo_label'),
71
+      Column::make('costo_base')->title('Costo')->data('costo_label')->searchable(false),
72
+      Column::computed('righe_count')->title('Servizi')->searchable(false),
73
+      Column::computed('todos_count')->title('Todo')->searchable(false),
74
+      Column::make('attivo')->title('Stato')->data('attivo_label'),
75
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
76
+    ];
77
+  }
78
+}

+ 244
- 0
app/DataTables/CostoSegnalazioneDataTable.php 파일 보기

@@ -0,0 +1,244 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Annotazione;
6
+use App\Models\Config;
7
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
8
+use Illuminate\Support\Facades\Auth;
9
+use Yajra\DataTables\EloquentDataTable;
10
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
11
+use Yajra\DataTables\Html\Button;
12
+use Yajra\DataTables\Html\Column;
13
+use Yajra\DataTables\Html\Editor\Editor;
14
+use Yajra\DataTables\Html\Editor\Fields;
15
+use Yajra\DataTables\Services\DataTable;
16
+
17
+class CostoSegnalazioneDataTable extends DataTable
18
+{
19
+  public function __construct()
20
+  {
21
+    $this->dataTableVariable = 'dataTable_costo_segnalazione';
22
+  }
23
+
24
+  /**
25
+  * Build the DataTable class.
26
+  *
27
+  * @param  QueryBuilder<Annotazione>  $query  Results from query() method.
28
+  */
29
+  public function dataTable(QueryBuilder $query): EloquentDataTable
30
+  {
31
+    return (new EloquentDataTable($query))
32
+    ->addColumn('action', function ($entity) {
33
+      return view('costo_segnalazione.menu', ['entity' => $entity]);
34
+    })
35
+    ->addColumn('segnalazione_label', function ($entity) {
36
+      return $entity->segnalazione ? $entity->segnalazione->getLink() : 'N/A';
37
+    })
38
+    ->addColumn('user_label', function ($entity) {
39
+      return $entity->user ? $entity->user->full_name : 'N/A';
40
+    })
41
+    ->addColumn('costo_label', function ($entity) {
42
+      return Config::formatCosto($entity->costo);
43
+    })
44
+    ->addColumn('created_at_label', function ($entity) {
45
+      return $entity->created_at ? $entity->created_at->format('d/m/Y H:i') : 'N/A';
46
+    })
47
+    ->filterColumn('created_at', function ($query, $keyword) {
48
+      self::applyDateTimeColumnFilter($query, $keyword, 'annotazione.created_at');
49
+    })
50
+    ->filterColumn('user_id', function ($query, $keyword) {
51
+      $keyword = trim((string) $keyword);
52
+      if ($keyword === '') {
53
+        return;
54
+      }
55
+      $query->whereHas('user', function ($q) use ($keyword) {
56
+        $q->where(function ($sub) use ($keyword) {
57
+          $sub->where('nome', 'like', '%' . $keyword . '%')
58
+            ->orWhere('cognome', 'like', '%' . $keyword . '%')
59
+            ->orWhere('email', 'like', '%' . $keyword . '%');
60
+        });
61
+      });
62
+    })
63
+    ->filterColumn('segnalazione_id', function ($query, $keyword) {
64
+      $keyword = trim((string) $keyword);
65
+      if ($keyword === '') {
66
+        return;
67
+      }
68
+      $query->whereHas('segnalazione', function ($q) use ($keyword) {
69
+        $q->where(function ($sub) use ($keyword) {
70
+          $sub->where('titolo', 'like', '%' . $keyword . '%');
71
+          if (ctype_digit($keyword)) {
72
+            $sub->orWhere('segnalazione.id', 'like', $keyword . '%');
73
+          }
74
+        });
75
+      });
76
+    })
77
+    ->filterColumn('costo', function ($query, $keyword) {
78
+      $keyword = str_replace(',', '.', trim((string) $keyword));
79
+      if ($keyword === '') {
80
+        return;
81
+      }
82
+      $query->where('annotazione.costo', 'like', '%' . $keyword . '%');
83
+    })
84
+    ->setRowId('id')
85
+    ->rawColumns(['segnalazione_label']);
86
+  }
87
+
88
+  /**
89
+  * Get the query source of dataTable.
90
+  *
91
+  * @return QueryBuilder<Annotazione>
92
+  */
93
+  public function query(Annotazione $model): QueryBuilder
94
+  {
95
+    $builder = $model->newQuery()->where('costo', '!=', 0);
96
+    if ($this->segnalazione) {
97
+      $builder->where('segnalazione_id', $this->segnalazione->id);
98
+    }
99
+
100
+    return $builder->with(['user', 'segnalazione']);
101
+  }
102
+
103
+  public static function applyDateTimeColumnFilter($query, $keyword, string $column): void
104
+  {
105
+    $keyword = (string) $keyword;
106
+    if ($keyword === '') {
107
+      return;
108
+    }
109
+    $isYmd = static fn (string $d): bool => (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
110
+    if (str_contains($keyword, '|')) {
111
+      [$from, $to] = array_pad(explode('|', $keyword, 2), 2, '');
112
+      $from = trim($from);
113
+      $to = trim($to);
114
+      if ($from !== '' && ! $isYmd($from)) {
115
+        return;
116
+      }
117
+      if ($to !== '' && ! $isYmd($to)) {
118
+        return;
119
+      }
120
+      if ($from !== '' && $to !== '') {
121
+        if ($from > $to) {
122
+          [$from, $to] = [$to, $from];
123
+        }
124
+        $query->whereBetween($column, [$from . ' 00:00:00', $to . ' 23:59:59']);
125
+      } elseif ($from !== '') {
126
+        $query->where($column, '>=', $from . ' 00:00:00');
127
+      } elseif ($to !== '') {
128
+        $query->where($column, '<=', $to . ' 23:59:59');
129
+      }
130
+
131
+      return;
132
+    }
133
+    if (! $isYmd($keyword)) {
134
+      return;
135
+    }
136
+    $query->whereDate($column, $keyword);
137
+  }
138
+
139
+  /**
140
+  * Optional method if you want to use the html builder.
141
+  */
142
+  public function html(): HtmlBuilder
143
+  {
144
+    $buttons = [];
145
+    if (Auth::user()->can('create-costo-segnalazione')) {
146
+      array_push($buttons, Button::make('create')
147
+      ->editor('editor')
148
+      ->className('btn btn-sm btn-primary mb-4')
149
+      ->formTitle('Registra nuovo costo segnalazione')
150
+      ->formButtons(
151
+        [Button::raw('Annulla')
152
+        ->className('btn btn-secondary mx-2')
153
+        ->actionClose(),
154
+        Button::raw('Salva')
155
+        ->className('btn btn-success ml-2')
156
+        ->actionHandler('create')]
157
+        )
158
+        ->text('<i class="fas fa-plus"></i> Registra costo'));
159
+      }
160
+
161
+      return $this->builder()
162
+      ->setTableId($this->dataTableVariable)
163
+      ->columns($this->getColumns())
164
+      ->buttons($buttons)
165
+      ->initComplete('function(settings, json){
166
+        initComplete_costo_segnalazione();
167
+                    }')
168
+      ->language(asset('assets/Italian.json'))
169
+      ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
170
+      ->responsive()
171
+      ->minifiedAjax()
172
+      ->orderBy(0)
173
+      ->editor(
174
+        Editor::make()
175
+        ->ajax(route('admin.costo_segnalazione.store'))
176
+        ->fields([
177
+          Fields\Select2::make('user_id', 'Utente')
178
+          ->placeholder('Seleziona l\'utente')
179
+          ->default(Auth::id())
180
+          ->ajax(['url' => route('admin.user.list'), 'dataType' => 'json', 'data' => 'function(params) {
181
+                             return {
182
+                               term: params.term || "",
183
+                               page: params.page || 1
184
+                             }
185
+                           }']),
186
+          Fields\Number::make('costo', 'Costo')->attr('step', '0.01'),
187
+          Fields\Text::make('costo_descrizione', 'Descrizione'),
188
+          Fields\Select2::make('segnalazione_id', 'Segnalazione')
189
+          ->default($this->segnalazione ? $this->segnalazione->id : 0)
190
+          ->placeholder('Seleziona la segnalazione')
191
+          ->ajax(['url' => route('admin.segnalazione.list'), 'dataType' => 'json', 'data' => 'function(params) {
192
+                             return {
193
+                               term: params.term || "",
194
+                               page: params.page || 1
195
+                             };
196
+                           }']),
197
+          ])
198
+          ->on('initEdit', 'function( e, node, data, items, type ){
199
+            if('.(($this->segnalazione != null) ? '1' : '0')."){
200
+              window.LaravelDataTables['dataTable_costo_segnalazione-editor'].hide(['segnalazione_id']);
201
+            }else{
202
+              window.LaravelDataTables['dataTable_costo_segnalazione-editor'].show(['segnalazione_id']);
203
+            }
204
+          }")
205
+          ->on('initCreate', 'function( e, node, data, items, type ){
206
+            var editor = window.LaravelDataTables["dataTable_costo_segnalazione-editor"];
207
+            var userField = editor.field("user_id");
208
+            var $select = userField.input();
209
+            var authId = "'.(int) Auth::id().'";
210
+            var authLabel = '.json_encode(Auth::user()->full_name).';
211
+            if ($select.find("option[value=\'" + authId + "\']").length === 0) {
212
+              $select.append(new Option(authLabel, authId, true, true));
213
+            }
214
+            userField.val(authId);
215
+            if('.(($this->segnalazione != null) ? '1' : '0')."){
216
+              editor.hide(['segnalazione_id']);
217
+            }else{
218
+              editor.show(['segnalazione_id']);
219
+            }
220
+          }")
221
+        );
222
+      }
223
+
224
+      /**
225
+      * Get the dataTable columns definition.
226
+      */
227
+      public function getColumns(): array
228
+      {
229
+        return [
230
+          Column::make('created_at')->title('Data/ora')->responsivePriority(1)->data('created_at_label'),
231
+          Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
232
+          Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
233
+          Column::make('costo')->title('Costo')->responsivePriority(3)->data('costo_label'),
234
+          Column::make('costo_descrizione')->title('Descrizione')->responsivePriority(3),
235
+          Column::computed('action')
236
+          ->title(' ')
237
+          ->exportable(false)
238
+          ->printable(false)
239
+          ->width('10%')
240
+          ->addClass('text-end')
241
+          ->responsivePriority(2),
242
+        ];
243
+      }
244
+    }

+ 88
- 0
app/DataTables/CostoSegnalazioneDataTableEditor.php 파일 보기

@@ -0,0 +1,88 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Annotazione;
6
+use App\Models\Config;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Validation\Rule;
9
+use Yajra\DataTables\DataTablesEditor;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Validation\ValidationException;
12
+use Storage;
13
+
14
+class CostoSegnalazioneDataTableEditor extends DataTablesEditor
15
+{
16
+  protected $model = Annotazione::class;
17
+  // protected $uploadDir = '';
18
+  // protected $disk = 'modelliRicevute';
19
+
20
+  protected $messages = [
21
+    'segnalazione_id.required' => 'La segnalazione è richiesta',
22
+    'user_id.required' => 'L\'utente è richiesto',
23
+    'costo.required' => 'Il costo è richiesto',
24
+    'costo.numeric' => 'Il costo deve essere un numero',
25
+    'costo_descrizione.required' => 'La descrizione è richiesta',
26
+  ];
27
+
28
+  /**
29
+  * Get create action validation rules.
30
+  *
31
+  * @return array
32
+  */
33
+  public function createRules(): array
34
+  {
35
+    return [
36
+      'segnalazione_id'  => 'required',
37
+      'user_id' => 'required',
38
+      'costo' => 'required|numeric',
39
+      'costo_descrizione' => 'required',
40
+    ];
41
+  }
42
+
43
+  public function createMessages(): array{
44
+    return $this->messages;
45
+  }
46
+
47
+  /**
48
+  * Get edit action validation rules.
49
+  *
50
+  * @param Model $model
51
+  * @return array
52
+  */
53
+  public function editRules(Model $model): array
54
+  {
55
+    return [
56
+      'segnalazione_id'  => 'required',
57
+      'user_id' => 'required',
58
+      'costo' => 'required|numeric|min:0',
59
+      'costo_descrizione' => 'required',
60
+    ];
61
+  }
62
+
63
+  public function editMessages(): array{
64
+    return $this->messages;
65
+  }
66
+
67
+  /**
68
+  * Get remove action validation rules.
69
+  *
70
+  * @param Model $model
71
+  * @return array
72
+  */
73
+  public function removeRules(Model $model): array
74
+  {
75
+    return [];
76
+  }
77
+
78
+  public function creating(Model $model, array $data): array
79
+  {
80
+    return $data;
81
+  }
82
+
83
+  public function updating(Model $model, array $data): array
84
+  {
85
+    return $data;
86
+  }
87
+
88
+}

+ 122
- 0
app/DataTables/GruppoDataTable.php 파일 보기

@@ -0,0 +1,122 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Azienda;
6
+use App\Models\User;
7
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Html\Editor\Editor;
13
+use Yajra\DataTables\Html\Editor\Fields;
14
+use Yajra\DataTables\Services\DataTable;
15
+use Illuminate\Support\Facades\Auth;
16
+
17
+class GruppoDataTable extends DataTable
18
+{
19
+  public function __construct(){
20
+    $this->dataTableVariable = 'dataTable_gruppo';
21
+  }
22
+  /**
23
+  * Build the DataTable class.
24
+  *
25
+  * @param QueryBuilder<User> $query Results from query() method.
26
+  */
27
+  public function dataTable(QueryBuilder $query): EloquentDataTable
28
+  {
29
+    return (new EloquentDataTable($query))
30
+    ->addColumn('action', function($entity){
31
+      return view('gruppo.menu', ['entity' => $entity]);
32
+    })
33
+    ->addColumn('componenti_gruppo_label', function($entity){
34
+      $ids = is_array($entity->componenti_gruppo)
35
+        ? $entity->componenti_gruppo
36
+        : explode(',', (string) $entity->componenti_gruppo);
37
+      $user = User::whereIn('id', array_filter(array_map('intval', $ids)))->soloUtenti()->get();
38
+      return $user->pluck('full_name')->implode(', ');
39
+    })
40
+    ->addColumn('azienda_display', function($entity){
41
+      return $entity->azienda ? $entity->azienda->nome : '';
42
+    })
43
+    ->editColumn('azienda_id', function ($entity) {
44
+      return $entity->azienda_id ? (string) $entity->azienda_id : '';
45
+    })
46
+    ->setRowId('id');
47
+  }
48
+
49
+  /**
50
+  * Get the query source of dataTable.
51
+  *
52
+  * @return QueryBuilder<User>
53
+  */
54
+  public function query(User $model): QueryBuilder
55
+  {
56
+    return $model->newQuery()
57
+    ->where('is_gruppo', 1)
58
+    ->with('azienda');
59
+  }
60
+
61
+  /**
62
+  * Optional method if you want to use the html builder.
63
+  */
64
+  public function html(): HtmlBuilder
65
+  {
66
+    $buttons = [];
67
+    if(Auth::user()->can('create-user')){
68
+      array_push($buttons, Button::make('create')
69
+      ->editor('editor')
70
+      ->formTitle('Crea nuovo gruppo')
71
+      ->className('btn btn-sm btn-primary mb-4')
72
+      ->text('<i class="fas fa-plus"></i> Nuovo gruppo'));
73
+    }
74
+
75
+    $aziende = Azienda::orderBy('nome', 'asc')->get()->pluck('id', 'nome');
76
+
77
+    $fields = [
78
+      Fields\Text::make('label_gruppo')->label('Nome gruppo'),
79
+      Fields\Select::make('azienda_id')->label('Azienda')->options($aziende)->placeholder('Nessuna'),
80
+      Fields\Checkbox::make('componenti_gruppo', 'Utenti')->options(User::soloUtenti()->orderBy('nome', 'asc')->get()->pluck('id', 'full_name') )->separator(","),
81
+      Fields\Hidden::make('is_gruppo')->default(1),
82
+    ];
83
+
84
+    return $this->builder()
85
+    ->setTableId($this->dataTableVariable)
86
+    ->initComplete("function(settings, json){
87
+        initComplete_gruppo();
88
+                    }")
89
+    ->language(asset('assets/Italian.json'))
90
+    ->columns($this->getColumns())
91
+    ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
92
+    ->responsive()
93
+    ->minifiedAjax()
94
+    ->editor(
95
+      Editor::make()
96
+      ->ajax(route('admin.gruppo.store'))
97
+      ->fields($fields)
98
+      )
99
+      ->buttons($buttons)
100
+      ->orderBy(0, 'asc');
101
+    }
102
+
103
+    /**
104
+    * Get the dataTable columns definition.
105
+    */
106
+    public function getColumns(): array
107
+    {
108
+      $columns = [
109
+        Column::make('label_gruppo')->title('Nome gruppo'),
110
+        Column::make('azienda_id')->title('Azienda')->data('azienda_display')->name('azienda_id')->searchable(false),
111
+        Column::make('componenti_gruppo_label')->title('Utenti'),
112
+        Column::computed('action')
113
+        ->exportable(false)
114
+        ->printable(false)
115
+        ->width('10%')
116
+        ->title(' ')
117
+        ->addClass('text-end')
118
+      ];
119
+
120
+      return $columns;
121
+    }
122
+  }

+ 92
- 0
app/DataTables/GruppoDataTableEditor.php 파일 보기

@@ -0,0 +1,92 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\User;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Validation\Rule;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Validation\ValidationException;
11
+use Storage;
12
+use Illuminate\Support\Str;
13
+
14
+class GruppoDataTableEditor extends DataTablesEditor
15
+{
16
+  protected $model = User::class;
17
+  // protected $uploadDir = '';
18
+  // protected $disk = 'modelliRicevute';
19
+
20
+  protected $messages = [
21
+    'label_gruppo.required' => 'Il nome gruppo è richiesto',
22
+  ];
23
+
24
+  /**
25
+  * Get create action validation rules.
26
+  *
27
+  * @return array
28
+  */
29
+  public function createRules(): array
30
+  {
31
+    return [
32
+      'label_gruppo'  => 'required',
33
+    ];
34
+  }
35
+
36
+  public function createMessages(): array{
37
+    return $this->messages;
38
+  }
39
+
40
+  /**
41
+  * Get edit action validation rules.
42
+  *
43
+  * @param Model $model
44
+  * @return array
45
+  */
46
+  public function editRules(Model $model): array
47
+  {
48
+    return [
49
+      'label_gruppo'  => 'required',
50
+    ];
51
+  }
52
+
53
+  public function editMessages(): array{
54
+    return $this->messages;
55
+  }
56
+
57
+  /**
58
+  * Get remove action validation rules.
59
+  *
60
+  * @param Model $model
61
+  * @return array
62
+  */
63
+  public function removeRules(Model $model): array
64
+  {
65
+    return [];
66
+  }
67
+
68
+  public function creating(Model $model, array $data): array
69
+  {
70
+    if (array_key_exists('azienda_id', $data) && ($data['azienda_id'] === '' || $data['azienda_id'] === null)) {
71
+      $data['azienda_id'] = null;
72
+    }
73
+
74
+    // $model->roles()->sync([$data['ruolo']]);
75
+    return $data;
76
+  }
77
+
78
+  public function updating(Model $model, array $data): array
79
+  {
80
+    if (array_key_exists('azienda_id', $data) && ($data['azienda_id'] === '' || $data['azienda_id'] === null)) {
81
+      $data['azienda_id'] = null;
82
+    }
83
+
84
+    return $data;
85
+  }
86
+
87
+  public function messages(): array
88
+  {
89
+    return $this->messages;
90
+  }
91
+
92
+}

+ 83
- 0
app/DataTables/OffertaDataTable.php 파일 보기

@@ -0,0 +1,83 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Config;
6
+use App\Models\Offerta;
7
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
8
+use Illuminate\Support\Facades\Auth;
9
+use Yajra\DataTables\EloquentDataTable;
10
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
11
+use Yajra\DataTables\Html\Button;
12
+use Yajra\DataTables\Html\Column;
13
+use Yajra\DataTables\Services\DataTable;
14
+
15
+class OffertaDataTable extends DataTable
16
+{
17
+  public function __construct()
18
+  {
19
+    $this->dataTableVariable = 'dataTable_offerta';
20
+  }
21
+
22
+  public function dataTable(QueryBuilder $query): EloquentDataTable
23
+  {
24
+    return (new EloquentDataTable($query))
25
+      ->addColumn('action', fn ($e) => view('offerta.menu', ['entity' => $e]))
26
+      ->addColumn('cliente_label', fn ($e) => $e->user ? $e->user->full_name : '—')
27
+      ->addColumn('stato_label', function ($e) {
28
+        $map = [
29
+          'bozza' => 'secondary',
30
+          'inviata' => 'info',
31
+          'accettata' => 'success',
32
+          'rifiutata' => 'danger',
33
+        ];
34
+        $color = $map[$e->stato] ?? 'secondary';
35
+
36
+        return '<span class="badge bg-label-'.$color.'">'.e($e->stato_label).'</span>';
37
+      })
38
+      ->addColumn('totale_label', fn ($e) => Config::formatCosto($e->totale))
39
+      ->addColumn('data_label', fn ($e) => $e->data ? \Carbon\Carbon::parse($e->data)->format('d/m/Y') : '—')
40
+      ->rawColumns(['stato_label', 'action'])
41
+      ->setRowId('id');
42
+  }
43
+
44
+  public function query(Offerta $model): QueryBuilder
45
+  {
46
+    return $model->newQuery()->with('user');
47
+  }
48
+
49
+  public function html(): HtmlBuilder
50
+  {
51
+    $buttons = [];
52
+    if (Auth::user()->can('create-offerta')) {
53
+      $buttons[] = Button::make('create')
54
+        ->className('btn btn-sm btn-primary mb-4')
55
+        ->text('<i class="fas fa-plus"></i> Nuova offerta')
56
+        ->action('window.location.href="'.route('admin.offerta.create').'"');
57
+    }
58
+
59
+    return $this->builder()
60
+      ->setTableId($this->dataTableVariable)
61
+      ->columns($this->getColumns())
62
+      ->buttons($buttons)
63
+      ->language(asset('assets/Italian.json'))
64
+      ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
65
+      ->responsive()
66
+      ->minifiedAjax()
67
+      ->orderBy(0, 'desc')
68
+      ->initComplete('function(){ $("div.dt-buttons button").removeClass("btn-secondary"); }');
69
+  }
70
+
71
+  public function getColumns(): array
72
+  {
73
+    return [
74
+      Column::make('numero')->title('N°'),
75
+      Column::make('titolo')->title('Titolo'),
76
+      Column::make('user_id')->title('Cliente')->data('cliente_label')->name('user_id'),
77
+      Column::make('stato')->title('Stato')->data('stato_label'),
78
+      Column::make('data')->title('Data')->data('data_label'),
79
+      Column::make('totale')->title('Totale')->data('totale_label')->searchable(false),
80
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
81
+    ];
82
+  }
83
+}

+ 84
- 0
app/DataTables/OrdineLavoroDataTable.php 파일 보기

@@ -0,0 +1,84 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\OrdineLavoro;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Services\DataTable;
13
+
14
+class OrdineLavoroDataTable extends DataTable
15
+{
16
+  public function __construct()
17
+  {
18
+    $this->dataTableVariable = 'dataTable_ordine_lavoro';
19
+  }
20
+
21
+  public function dataTable(QueryBuilder $query): EloquentDataTable
22
+  {
23
+    return (new EloquentDataTable($query))
24
+      ->addColumn('action', fn ($e) => view('ordine_lavoro.menu', ['entity' => $e]))
25
+      ->addColumn('cliente_label', fn ($e) => $e->user ? $e->user->full_name : '—')
26
+      ->addColumn('stato_label', function ($e) {
27
+        $map = [
28
+          'aperto' => 'info',
29
+          'in_lavorazione' => 'primary',
30
+          'in_attesa_materiale' => 'warning',
31
+          'evaso' => 'success',
32
+          'completato' => 'success',
33
+          'annullato' => 'secondary',
34
+        ];
35
+        $color = $map[$e->stato] ?? 'secondary';
36
+
37
+        return '<span class="badge bg-label-'.$color.'">'.e($e->stato_label).'</span>';
38
+      })
39
+      ->addColumn('avanzamento_label', fn ($e) => ((int) $e->percentuale_avanzamento).'%')
40
+      ->addColumn('data_label', fn ($e) => $e->data_apertura ? \Carbon\Carbon::parse($e->data_apertura)->format('d/m/Y') : '—')
41
+      ->rawColumns(['stato_label', 'action'])
42
+      ->setRowId('id');
43
+  }
44
+
45
+  public function query(OrdineLavoro $model): QueryBuilder
46
+  {
47
+    return $model->newQuery()->with('user');
48
+  }
49
+
50
+  public function html(): HtmlBuilder
51
+  {
52
+    $buttons = [];
53
+    if (Auth::user()->can('create-ordine-lavoro')) {
54
+      $buttons[] = Button::make('create')
55
+        ->className('btn btn-sm btn-primary mb-4')
56
+        ->text('<i class="fas fa-plus"></i> Nuovo ordine')
57
+        ->action('window.location.href="'.route('admin.ordine_lavoro.create').'"');
58
+    }
59
+
60
+    return $this->builder()
61
+      ->setTableId($this->dataTableVariable)
62
+      ->columns($this->getColumns())
63
+      ->buttons($buttons)
64
+      ->language(asset('assets/Italian.json'))
65
+      ->dom('<"py-2"<"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
66
+      ->responsive()
67
+      ->minifiedAjax()
68
+      ->orderBy(0, 'desc')
69
+      ->initComplete('function(){ $("div.dt-buttons button").removeClass("btn-secondary"); }');
70
+  }
71
+
72
+  public function getColumns(): array
73
+  {
74
+    return [
75
+      Column::make('numero')->title('N°'),
76
+      Column::make('titolo')->title('Titolo'),
77
+      Column::make('user_id')->title('Cliente')->data('cliente_label')->name('user_id'),
78
+      Column::make('stato')->title('Stato')->data('stato_label'),
79
+      Column::make('percentuale_avanzamento')->title('Avanz.')->data('avanzamento_label'),
80
+      Column::make('data_apertura')->title('Apertura')->data('data_label'),
81
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
82
+    ];
83
+  }
84
+}

+ 76
- 0
app/DataTables/ProdottoDataTable.php 파일 보기

@@ -0,0 +1,76 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Prodotto;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Yajra\DataTables\EloquentDataTable;
8
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
9
+use Yajra\DataTables\Html\Button;
10
+use Yajra\DataTables\Html\Column;
11
+use Yajra\DataTables\Services\DataTable;
12
+use Illuminate\Support\Facades\Auth;
13
+
14
+class ProdottoDataTable extends DataTable
15
+{
16
+  public function __construct()
17
+  {
18
+    $this->dataTableVariable = 'dataTable_prodotto';
19
+  }
20
+
21
+  public function dataTable(QueryBuilder $query): EloquentDataTable
22
+  {
23
+    return (new EloquentDataTable($query))
24
+      ->addColumn('action', fn ($entity) => view('prodotto.menu', ['entity' => $entity]))
25
+      ->addColumn('tipo_label', fn ($entity) => $entity->tipo_label)
26
+      ->addColumn('unita_misura_label', fn ($entity) => $entity->unita_misura_label)
27
+      ->addColumn('prezzo_label', fn ($entity) => $entity->prezzo !== null ? number_format((float) $entity->prezzo, 2, ',', '.').' €' : '—')
28
+      ->addColumn('attivo_label', fn ($entity) => $entity->attivo
29
+        ? '<span class="badge bg-label-success">Attivo</span>'
30
+        : '<span class="badge bg-label-secondary">No</span>')
31
+      ->editColumn('attivo', fn ($entity) => $entity->attivo ? 1 : 0)
32
+      ->rawColumns(['attivo_label', 'action'])
33
+      ->setRowId('id');
34
+  }
35
+
36
+  public function query(Prodotto $model): QueryBuilder
37
+  {
38
+    return $model->newQuery();
39
+  }
40
+
41
+  public function html(): HtmlBuilder
42
+  {
43
+    $buttons = [];
44
+    if (Auth::user()->can('create-prodotto')) {
45
+      $buttons[] = Button::make('create')
46
+        ->className('btn btn-sm btn-primary mb-4')
47
+        ->action('openProdottoModal()')
48
+        ->text('<i class="fas fa-plus"></i> Nuovo prodotto');
49
+    }
50
+
51
+    return $this->builder()
52
+      ->setTableId($this->dataTableVariable)
53
+      ->columns($this->getColumns())
54
+      ->buttons($buttons)
55
+      ->language(asset('assets/Italian.json'))
56
+      ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
57
+      ->responsive()
58
+      ->minifiedAjax()
59
+      ->orderBy(1, 'asc')
60
+      ->initComplete('function(){ initComplete_prodotto(); }');
61
+  }
62
+
63
+  public function getColumns(): array
64
+  {
65
+    return [
66
+      Column::make('codice')->title('Codice'),
67
+      Column::make('nome')->title('Nome'),
68
+      Column::make('tipo')->title('Tipo')->data('tipo_label'),
69
+      Column::make('unita_misura')->title('U.M.')->data('unita_misura_label'),
70
+      Column::make('prezzo')->title('Prezzo')->data('prezzo_label')->searchable(false),
71
+      Column::make('periodo')->title('Periodo'),
72
+      Column::make('attivo')->title('Stato')->data('attivo_label')->searchable(false),
73
+      Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
74
+    ];
75
+  }
76
+}

+ 70
- 0
app/DataTables/ProdottoDataTableEditor.php 파일 보기

@@ -0,0 +1,70 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Prodotto;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Yajra\DataTables\DataTablesEditor;
8
+
9
+class ProdottoDataTableEditor extends DataTablesEditor
10
+{
11
+  protected $model = Prodotto::class;
12
+
13
+  protected $messages = [
14
+    'nome.required' => 'Il nome è richiesto',
15
+  ];
16
+
17
+  public function createRules(): array
18
+  {
19
+    return [
20
+      'nome' => 'required|string|max:255',
21
+      'tipo' => 'nullable|string|max:50',
22
+      'prezzo' => 'nullable|numeric|min:0',
23
+    ];
24
+  }
25
+
26
+  public function createMessages(): array
27
+  {
28
+    return $this->messages;
29
+  }
30
+
31
+  public function editRules(Model $model): array
32
+  {
33
+    return $this->createRules();
34
+  }
35
+
36
+  public function editMessages(): array
37
+  {
38
+    return $this->messages;
39
+  }
40
+
41
+  public function removeRules(Model $model): array
42
+  {
43
+    return [];
44
+  }
45
+
46
+  public function creating(Model $model, array $data): array
47
+  {
48
+    return $this->normalize($data);
49
+  }
50
+
51
+  public function updating(Model $model, array $data): array
52
+  {
53
+    return $this->normalize($data);
54
+  }
55
+
56
+  private function normalize(array $data): array
57
+  {
58
+    if (array_key_exists('attivo', $data)) {
59
+      $data['attivo'] = filter_var($data['attivo'], FILTER_VALIDATE_BOOLEAN) || $data['attivo'] === '1' || $data['attivo'] === 1;
60
+    }
61
+    if (array_key_exists('prezzo', $data) && $data['prezzo'] === '') {
62
+      $data['prezzo'] = null;
63
+    }
64
+    if (array_key_exists('periodo', $data) && $data['periodo'] === '') {
65
+      $data['periodo'] = null;
66
+    }
67
+
68
+    return $data;
69
+  }
70
+}

+ 85
- 0
app/DataTables/PromoCodeDataTable.php 파일 보기

@@ -0,0 +1,85 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\PromoCode;
6
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
7
+use Illuminate\Support\Facades\Auth;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Services\DataTable;
13
+
14
+class PromoCodeDataTable extends DataTable
15
+{
16
+    public function __construct()
17
+    {
18
+        $this->dataTableVariable = 'dataTable_promo_code';
19
+    }
20
+
21
+    public function dataTable(QueryBuilder $query): EloquentDataTable
22
+    {
23
+        return (new EloquentDataTable($query))
24
+            ->addColumn('action', fn ($entity) => view('promo_code.menu', ['entity' => $entity]))
25
+            ->addColumn('sconto_label', fn ($entity) => number_format((float) $entity->sconto_percentuale, 2, ',', '.').' %')
26
+            ->addColumn('prodotti_label', function ($entity) {
27
+                if ($entity->prodotti->isEmpty()) {
28
+                    return '<span class="text-muted">Tutti</span>';
29
+                }
30
+
31
+                return e($entity->prodotti->pluck('nome')->join(', '));
32
+            })
33
+            ->addColumn('attivo_label', fn ($entity) => $entity->attivo
34
+                ? '<span class="badge bg-label-success">Attivo</span>'
35
+                : '<span class="badge bg-label-secondary">No</span>')
36
+            ->addColumn('validita', function ($entity) {
37
+                $dal = $entity->valido_dal ? \Carbon\Carbon::parse($entity->valido_dal)->format('d/m/Y') : '—';
38
+                $al = $entity->valido_al ? \Carbon\Carbon::parse($entity->valido_al)->format('d/m/Y') : '—';
39
+
40
+                return $dal.' → '.$al;
41
+            })
42
+            ->addColumn('usi', fn ($entity) => (int) $entity->usi_count.($entity->usi_max !== null ? ' / '.$entity->usi_max : ''))
43
+            ->rawColumns(['attivo_label', 'prodotti_label', 'action'])
44
+            ->setRowId('id');
45
+    }
46
+
47
+    public function query(PromoCode $model): QueryBuilder
48
+    {
49
+        return $model->newQuery()->with('prodotti');
50
+    }
51
+
52
+    public function html(): HtmlBuilder
53
+    {
54
+        $buttons = [];
55
+        if (Auth::user()->can('create-promo-code')) {
56
+            $buttons[] = Button::make()
57
+                ->className('btn btn-sm btn-primary mb-4')
58
+                ->text('<i class="fas fa-plus"></i> Nuovo codice')
59
+                ->attr(['onclick' => "window.location='".route('admin.promo_code.create')."'"]);
60
+        }
61
+
62
+        return $this->builder()
63
+            ->setTableId($this->dataTableVariable)
64
+            ->columns($this->getColumns())
65
+            ->buttons($buttons)
66
+            ->language(asset('assets/Italian.json'))
67
+            ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
68
+            ->responsive()
69
+            ->minifiedAjax()
70
+            ->orderBy(0, 'asc');
71
+    }
72
+
73
+    public function getColumns(): array
74
+    {
75
+        return [
76
+            Column::make('codice')->title('Codice'),
77
+            Column::make('sconto_percentuale')->title('Sconto')->data('sconto_label')->searchable(false),
78
+            Column::computed('prodotti_label')->title('Prodotti')->orderable(false)->searchable(false),
79
+            Column::computed('validita')->title('Validità')->orderable(false)->searchable(false),
80
+            Column::computed('usi')->title('Usi')->orderable(false)->searchable(false),
81
+            Column::make('attivo')->title('Stato')->data('attivo_label')->searchable(false),
82
+            Column::computed('action')->exportable(false)->printable(false)->width(60)->title(' ')->addClass('text-center'),
83
+        ];
84
+    }
85
+}

+ 339
- 0
app/DataTables/SegnalazioneDataTable.php 파일 보기

@@ -0,0 +1,339 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Config;
6
+use App\Models\Segnalazione;
7
+use Carbon\Carbon;
8
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
9
+use Illuminate\Support\Facades\Auth;
10
+use Yajra\DataTables\EloquentDataTable;
11
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
12
+use Yajra\DataTables\Html\Button;
13
+use Yajra\DataTables\Html\Column;
14
+use Yajra\DataTables\Html\Editor\Editor;
15
+use Yajra\DataTables\Services\DataTable;
16
+
17
+class SegnalazioneDataTable extends DataTable
18
+{
19
+    public $showAll = false;
20
+
21
+    public function __construct()
22
+    {
23
+        $this->dataTableVariable = 'dataTable_segnalazione';
24
+    }
25
+
26
+    /**
27
+     * Build the DataTable class.
28
+     *
29
+     * @param  QueryBuilder<Segnalazione>  $query  Results from query() method.
30
+     */
31
+    public function dataTable(QueryBuilder $query): EloquentDataTable
32
+    {
33
+        return (new EloquentDataTable($query))
34
+            ->addColumn('action', function ($entity) {
35
+                return view('segnalazione.admin.menu', ['entity' => $entity]);
36
+            })
37
+            ->addColumn('azienda_label', function ($entity) {
38
+                return $entity->azienda ? $entity->azienda->full_name : 'N/A';
39
+            })
40
+            ->addColumn('creato_il', function ($entity) {
41
+                return $entity->created_at->format('d/m/Y H:i');
42
+            })
43
+            ->addColumn('aggiornato_il', function ($entity) {
44
+                return Carbon::parse($entity->updated_at)->diffForHumans();
45
+            })
46
+            ->addColumn('numero_label', function ($entity) {
47
+                return $entity->getNumeroLabel();
48
+            })
49
+            ->addColumn('user_label', function ($entity) {
50
+                return $entity->user ? $entity->user->full_name : '--';
51
+            })
52
+            ->addColumn('stato_display', function ($entity) {
53
+                $config = $entity->getConfiguraStato();
54
+                if (! $config) {
55
+                    return '<span class="badge bg-label-secondary">'.($entity->stato ?? 'N/A').'</span>';
56
+                }
57
+                $icona = $config->icona ? '<i class="bx icon-base '.$config->icona.'"></i>' : '';
58
+                $colore = $config->colore ? 'bg-label-'.$config->colore : 'bg-label-secondary';
59
+
60
+                return '<div class="rounded-pill justify-content-center align-items-center badge badge-hover-label '.$colore.'">'.$icona.'<span class="badge-label-text">'.$config->label.'</span></div>';
61
+            })
62
+            ->addColumn('priorita_display', function ($entity) {
63
+                $config = $entity->getConfiguraPriorita();
64
+                if (! $config) {
65
+                    return '<span class="badge bg-label-secondary">'.($entity->priorita ?? 'N/A').'</span>';
66
+                }
67
+                $icona = $config->icona ? '<i class="bx icon-base '.$config->icona.'"></i>' : '';
68
+                $colore = $config->colore ? 'bg-label-'.$config->colore : 'bg-label-secondary';
69
+
70
+                return '<div class="rounded-5 justify-content-center align-items-center badge badge-hover-label '.$colore.'">'.$icona.'<span class="badge-label-text">'.$config->label.'</span></div>';
71
+            })
72
+            ->addColumn('tipo_display', function ($entity) {
73
+                $config = $entity->getConfiguraTipo();
74
+                if (! $config) {
75
+                    return '<div class="rounded-pill justify-content-center align-items-center badge bg-label-secondary">'.($entity->tipo ?? 'N/A').'</div>';
76
+                }
77
+                $icona = $config->icona ? '<i class="bx icon-base '.$config->icona.'"></i>' : '';
78
+                $colore = $config->colore ? 'bg-label-'.$config->colore : 'bg-label-secondary';
79
+
80
+                return '<div class="rounded-pill justify-content-center align-items-center badge badge-hover-label '.$colore.'">'.$icona.'<span class="badge-label-text">'.$config->label.'</span></div>';
81
+            })
82
+            ->addColumn('assegnato_a', function ($entity) {
83
+                return $entity->assegnataA ? $entity->assegnataA->full_name : '--';
84
+            })
85
+            ->filterColumn('numero_label', function ($query, $keyword) {
86
+                $keyword = trim((string) $keyword);
87
+                if ($keyword === '') {
88
+                    return;
89
+                }
90
+                $query->where(function ($q) use ($keyword) {
91
+                    $q->where('segnalazione.numero', 'like', '%' . $keyword . '%');
92
+                    if (ctype_digit($keyword)) {
93
+                        $q->orWhere('segnalazione.id', (int) $keyword);
94
+                    }
95
+                });
96
+            })
97
+            ->filterColumn('azienda_id', function ($query, $keyword) {
98
+                $keyword = trim((string) $keyword);
99
+                if ($keyword === '') {
100
+                    return;
101
+                }
102
+                $query->where(function ($q) use ($keyword) {
103
+                    $q->where('azienda_user.nome', 'like', '%' . $keyword . '%')
104
+                        ->orWhere('azienda_user.cognome', 'like', '%' . $keyword . '%');
105
+                });
106
+            })
107
+            ->filterColumn('stato', function ($query, $keyword) {
108
+                $this->filterSegnalazioneConfigColumn($query, $keyword, 'stato', 'stato');
109
+            })
110
+            ->filterColumn('priorita', function ($query, $keyword) {
111
+                $this->filterSegnalazioneConfigColumn($query, $keyword, 'Priorità', 'priorita');
112
+            })
113
+            ->filterColumn('tipo', function ($query, $keyword) {
114
+                $this->filterSegnalazioneConfigColumn($query, $keyword, 'Tipo', 'tipo');
115
+            })
116
+            ->filterColumn('user_id', function ($query, $keyword) {
117
+                $keyword = trim((string) $keyword);
118
+                if ($keyword === '') {
119
+                    return;
120
+                }
121
+                $query->whereHas('user', function ($q) use ($keyword) {
122
+                    $q->where(function ($sub) use ($keyword) {
123
+                        $sub->where('nome', 'like', '%' . $keyword . '%')
124
+                            ->orWhere('cognome', 'like', '%' . $keyword . '%')
125
+                            ->orWhere('email', 'like', '%' . $keyword . '%');
126
+                    });
127
+                });
128
+            })
129
+            ->filterColumn('assegnato_a', function ($query, $keyword) {
130
+                $keyword = trim((string) $keyword);
131
+                if ($keyword === '') {
132
+                    return;
133
+                }
134
+                $query->whereHas('assegnataA', function ($q) use ($keyword) {
135
+                    $q->where(function ($sub) use ($keyword) {
136
+                        $sub->where('nome', 'like', '%' . $keyword . '%')
137
+                            ->orWhere('cognome', 'like', '%' . $keyword . '%')
138
+                            ->orWhere('email', 'like', '%' . $keyword . '%');
139
+                    });
140
+                });
141
+            })
142
+            ->filterColumn('created_at', function ($query, $keyword) {
143
+                $this->filterDateTimeColumn($query, $keyword, 'segnalazione.created_at');
144
+            })
145
+            ->filterColumn('updated_at', function ($query, $keyword) {
146
+                $this->filterDateTimeColumn($query, $keyword, 'segnalazione.updated_at');
147
+            })
148
+            ->rawColumns(['stato_display', 'priorita_display', 'tipo_display'])
149
+            ->setRowId('id');
150
+    }
151
+
152
+    /**
153
+     * Get the query source of dataTable.
154
+     *
155
+     * @return QueryBuilder<Segnalazione>
156
+     */
157
+    public function query(Segnalazione $model): QueryBuilder
158
+    {
159
+        $query = $model->newQuery()
160
+            ->leftJoin('users as azienda_user', 'segnalazione.azienda_id', '=', 'azienda_user.id')
161
+            ->select('segnalazione.*');
162
+
163
+        $authUser = Auth::user();
164
+        $canViewAll = $authUser && $authUser->can('view-segnalazione');
165
+        $canOwn = $authUser && $authUser->can('view-own-segnalazione');
166
+        $canAssegnate = $authUser && $authUser->can('view-segnalazione-assegnate');
167
+        // Solo "proprie": mostra anche le chiuse (altrimenti l’unica segnalazione chiusa sparisce dall’elenco).
168
+        $soloProprie = $authUser && ! $canViewAll && $canOwn && ! $canAssegnate;
169
+
170
+        if ($this->status) {
171
+            if ($this->status == 'urgente') {
172
+                $query->whereIn('segnalazione.priorita', ['urgente', 'emergenza_immediata']);
173
+            }
174
+
175
+            if ($this->status == 'aperte') {
176
+                $query->whereIn('segnalazione.stato', Config::getStatiSegnalazione('aperte'));
177
+            }
178
+
179
+            if ($this->status == 'in_lavorazione') {
180
+                $query->whereIn('segnalazione.stato', Config::getStatiSegnalazione('in_lavorazione'));
181
+            }
182
+
183
+            if ($this->status == 'chiuse') {
184
+                $query->whereIn('segnalazione.stato', Config::getStatiSegnalazione('chiuse'));
185
+            }
186
+
187
+            if ($this->status == 'senza_assegnazione') {
188
+                $query->whereNull('segnalazione.assegnata_a_id');
189
+            }
190
+
191
+            if ($this->status == 'in_ritardo') {
192
+                $query->where('segnalazione.created_at', '<', Carbon::now()->subDays(7));
193
+            }
194
+        } elseif (! $soloProprie) {
195
+            $query->whereNotIn('segnalazione.stato', Config::getStatiSegnalazione('chiuse'));
196
+        }
197
+
198
+        // view-segnalazione = tutte; altrimenti restringi a proprie e/o assegnate
199
+        if ($authUser && ! $canViewAll) {
200
+            if ($canOwn || $canAssegnate) {
201
+                $query->where(function ($q) use ($authUser, $canOwn, $canAssegnate) {
202
+                    if ($canOwn) {
203
+                        $q->orWhere('segnalazione.user_id', $authUser->id);
204
+                    }
205
+                    if ($canAssegnate) {
206
+                        $q->orWhereIn(
207
+                            'segnalazione.assegnata_a_id',
208
+                            Segnalazione::assigneeIdsForViewSegnalazioneAssegnate($authUser)
209
+                        );
210
+                    }
211
+                });
212
+            } else {
213
+                $query->whereRaw('0 = 1');
214
+            }
215
+        }
216
+
217
+        return $query->with(['user', 'assegnataA', 'azienda']);
218
+    }
219
+
220
+    private function filterDateTimeColumn($query, $keyword, string $column): void
221
+    {
222
+        $keyword = (string) $keyword;
223
+        if ($keyword === '') {
224
+            return;
225
+        }
226
+        $isYmd = static fn (string $d): bool => (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
227
+        if (str_contains($keyword, '|')) {
228
+            [$from, $to] = array_pad(explode('|', $keyword, 2), 2, '');
229
+            $from = trim($from);
230
+            $to = trim($to);
231
+            if ($from !== '' && ! $isYmd($from)) {
232
+                return;
233
+            }
234
+            if ($to !== '' && ! $isYmd($to)) {
235
+                return;
236
+            }
237
+            if ($from !== '' && $to !== '') {
238
+                if ($from > $to) {
239
+                    [$from, $to] = [$to, $from];
240
+                }
241
+                $query->whereBetween($column, [$from . ' 00:00:00', $to . ' 23:59:59']);
242
+            } elseif ($from !== '') {
243
+                $query->where($column, '>=', $from . ' 00:00:00');
244
+            } elseif ($to !== '') {
245
+                $query->where($column, '<=', $to . ' 23:59:59');
246
+            }
247
+
248
+            return;
249
+        }
250
+        if (! $isYmd($keyword)) {
251
+            return;
252
+        }
253
+        $query->whereDate($column, $keyword);
254
+    }
255
+
256
+    private function filterSegnalazioneConfigColumn($query, $keyword, string $gruppo, string $column): void
257
+    {
258
+        $keyword = trim((string) $keyword);
259
+        if ($keyword === '') {
260
+            return;
261
+        }
262
+        $query->where(function ($q) use ($keyword, $gruppo, $column) {
263
+            $q->where("segnalazione.{$column}", 'like', '%' . $keyword . '%')
264
+                ->orWhereIn("segnalazione.{$column}", function ($sub) use ($keyword, $gruppo) {
265
+                    $sub->select('value')
266
+                        ->from('configura_segnalazione')
267
+                        ->where('gruppo', $gruppo)
268
+                        ->where('label', 'like', '%' . $keyword . '%');
269
+                });
270
+        });
271
+    }
272
+
273
+    /**
274
+     * Optional method if you want to use the html builder.
275
+     */
276
+    public function html(): HtmlBuilder
277
+    {
278
+        $buttons = [];
279
+        if (Auth::user()->can('create-segnalazione')) {
280
+            array_push(
281
+                $buttons,
282
+                Button::raw()
283
+                    ->className('btn btn-sm btn-primary')
284
+                    ->text('<i class="fas fa-plus"></i> Nuova segnalazione')
285
+                    ->action('location.href="'.route('admin.segnalazione.nuova').'"'),
286
+            );
287
+        }
288
+
289
+        return $this->builder()
290
+            ->setTableId($this->dataTableVariable)
291
+            ->initComplete('function(settings, json){
292
+        initComplete_segnalazione();
293
+                    }')
294
+            ->language(asset('assets/Italian.json'))
295
+            ->columns($this->getColumns())
296
+            ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
297
+            ->responsive()
298
+            ->minifiedAjax()
299
+            ->orderBy(8, 'desc')
300
+            ->buttons($buttons)
301
+            ->editor(
302
+                Editor::make()
303
+            );
304
+    }
305
+
306
+    /**
307
+     * Get the dataTable columns definition.
308
+     */
309
+    public function getColumns(): array
310
+    {
311
+        return [
312
+            Column::computed('numero_label')->title('Numero')->responsivePriority(0),
313
+            Column::make('titolo')->title('Titolo')->responsivePriority(1),
314
+            Column::make('azienda_id')->title('Azienda')->data('azienda_label'),
315
+            Column::make('stato')->title('Stato')->data('stato_display'),
316
+            Column::make('priorita')->title('Priorità')->data('priorita_display'),
317
+            Column::make('tipo')->title('Tipo')->data('tipo_display'),
318
+            Column::make('user_id')->title('Aperta da')->data('user_label'),
319
+            Column::make('assegnato_a')->title('Assegnata a'),
320
+            Column::make('created_at')->title('Creato il')->data('creato_il'),
321
+            Column::make('updated_at')->title('Ultimo aggiornamento')->data('aggiornato_il'),
322
+            Column::computed('action')
323
+                ->exportable(false)
324
+                ->printable(false)
325
+                ->width('10%')
326
+                ->title(' ')
327
+                ->addClass('text-end')
328
+                ->responsivePriority(2),
329
+        ];
330
+    }
331
+
332
+    /**
333
+     * Get the filename for export.
334
+     */
335
+    protected function filename(): string
336
+    {
337
+        return 'Segnalazione_'.date('YmdHis');
338
+    }
339
+}

+ 119
- 0
app/DataTables/SegnalazioneDataTableEditor.php 파일 보기

@@ -0,0 +1,119 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Segnalazione;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Validation\Rule;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Validation\ValidationException;
11
+use Storage;
12
+use App\Models\Reparto;
13
+
14
+class SegnalazioneDataTableEditor extends DataTablesEditor
15
+{
16
+  protected $model = Segnalazione::class;
17
+  // protected $uploadDir = '';
18
+  // protected $disk = 'modelliRicevute';
19
+
20
+  protected $messages = [
21
+    'titolo.required' => 'Il titolo deve essere una stringa',
22
+    'descrizione.required' => 'La descrizione è richiesta',
23
+    'email.required' => 'L\'email è richiesta',
24
+    'telefono.required' => 'Il telefono è richiesto',
25
+    'reparto_id.required' => 'Il reparto è richiesto',
26
+    'stato_id.required' => 'Il stato è richiesto',
27
+    'priorita_id.required' => 'La priorità è richiesta',
28
+    'tipo_id.required' => 'Il tipo è richiesto',
29
+    'user_id.required' => 'L\'utente è richiesto',
30
+  ];
31
+
32
+  /**
33
+  * Get create action validation rules.
34
+  *
35
+  * @return array
36
+  */
37
+  public function createRules(): array
38
+  {
39
+    return [
40
+      'titolo'  => 'required',
41
+      'descrizione' => 'required',
42
+    //   'email' => 'required',
43
+    //   'telefono' => 'required',
44
+    //   'reparto_id' => 'required',
45
+    //   'stato_id' => 'required',
46
+      'luogo_id' => 'required',
47
+    //   'priorita_id' => 'required',
48
+    //   'tipo_id' => 'required',
49
+    //   'user_id' => 'required',
50
+    ];
51
+  }
52
+
53
+  public function createMessages(): array{
54
+
55
+    $messages = [
56
+      'titolo.required' => 'Il titolo è richiesto',
57
+      'descrizione.required' => 'La descrizione è richiesta',
58
+      'luogo_id.required' => 'Il luogo è richiesto',
59
+    ];
60
+    return $this->messages;
61
+  }
62
+
63
+  /**
64
+  * Get edit action validation rules.
65
+  *
66
+  * @param Model $model
67
+  * @return array
68
+  */
69
+  public function editRules(Model $model): array
70
+  {
71
+    return [
72
+     'titolo'  => 'required',
73
+      'descrizione' => 'required',
74
+    //   'email' => 'required',
75
+    //   'telefono' => 'required',
76
+    //   'reparto_id' => 'required',
77
+    //   'stato_id' => 'required',
78
+      'luogo_id' => 'required',
79
+    //   'priorita_id' => 'required',
80
+    //   'tipo_id' => 'required',
81
+    //   'user_id' => 'required',
82
+    ];
83
+  }
84
+
85
+  public function editMessages(): array{
86
+    $messages = [
87
+        'titolo.required' => 'Il titolo è richiesto',
88
+        'descrizione.required' => 'La descrizione è richiesta',
89
+        'luogo_id.required' => 'Il luogo è richiesto',
90
+      ];
91
+      
92
+    return $this->messages;
93
+  }
94
+
95
+  /**
96
+  * Get remove action validation rules.
97
+  *
98
+  * @param Model $model
99
+  * @return array
100
+  */
101
+  public function removeRules(Model $model): array
102
+  {
103
+    return [];
104
+  }
105
+
106
+  public function creating(Model $model, array $data): array
107
+  {
108
+    // dd($data);
109
+    $data['email'] = Reparto::find($data['reparto_id'])->email;
110
+    $data['telefono'] = Reparto::find($data['reparto_id'])->telefono;
111
+    return $data;
112
+  }
113
+
114
+  public function updating(Model $model, array $data): array
115
+  {
116
+    return $data;
117
+  }
118
+
119
+}

+ 215
- 0
app/DataTables/TempoImpiegatoDataTable.php 파일 보기

@@ -0,0 +1,215 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Annotazione;
6
+use App\Models\Config;
7
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
8
+use Yajra\DataTables\EloquentDataTable;
9
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
10
+use Yajra\DataTables\Html\Button;
11
+use Yajra\DataTables\Html\Column;
12
+use Yajra\DataTables\Html\Editor\Editor;
13
+use Yajra\DataTables\Html\Editor\Fields;
14
+use Yajra\DataTables\Services\DataTable;
15
+use Illuminate\Support\Facades\Auth;
16
+use Illuminate\Support\Str;
17
+use Carbon\Carbon;
18
+
19
+class TempoImpiegatoDataTable extends DataTable
20
+{
21
+  public function __construct(){
22
+    $this->dataTableVariable = 'dataTable_tempo_impiegato';
23
+  }
24
+  /**
25
+  * Build the DataTable class.
26
+  *
27
+  * @param QueryBuilder<Annotazione> $query Results from query() method.
28
+  */
29
+  public function dataTable(QueryBuilder $query): EloquentDataTable
30
+  {
31
+    return (new EloquentDataTable($query))
32
+    ->addColumn('action', function($entity){
33
+      return view('tempo_impiegato.menu', ['entity' => $entity]);
34
+    })
35
+    ->addColumn('segnalazione_label', function($entity){
36
+      return $entity->segnalazione ? $entity->segnalazione->getLink() : 'N/A';
37
+    })
38
+    ->addColumn('user_label', function($entity){
39
+      return $entity->user ? $entity->user->full_name : 'N/A';
40
+    })
41
+    ->addColumn('text_excerpt', function($entity){
42
+      return $entity->text ? Str::limit($entity->text, 50) : 'N/A';
43
+    })
44
+    ->addColumn('tempo_minuti_label', function($entity){
45
+      return Config::formatTempo($entity->tempo_minuti);
46
+    })
47
+    ->filterColumn('tempo_data', function ($query, $keyword) {
48
+      CostoSegnalazioneDataTable::applyDateTimeColumnFilter($query, $keyword, 'annotazione.tempo_data');
49
+    })
50
+    ->filterColumn('user_id', function ($query, $keyword) {
51
+      $keyword = trim((string) $keyword);
52
+      if ($keyword === '') {
53
+        return;
54
+      }
55
+      $query->whereHas('user', function ($q) use ($keyword) {
56
+        $q->where(function ($sub) use ($keyword) {
57
+          $sub->where('nome', 'like', '%' . $keyword . '%')
58
+            ->orWhere('cognome', 'like', '%' . $keyword . '%')
59
+            ->orWhere('email', 'like', '%' . $keyword . '%');
60
+        });
61
+      });
62
+    })
63
+    ->filterColumn('segnalazione_id', function ($query, $keyword) {
64
+      $keyword = trim((string) $keyword);
65
+      if ($keyword === '') {
66
+        return;
67
+      }
68
+      $query->whereHas('segnalazione', function ($q) use ($keyword) {
69
+        $q->where(function ($sub) use ($keyword) {
70
+          $sub->where('titolo', 'like', '%' . $keyword . '%');
71
+          if (ctype_digit($keyword)) {
72
+            $sub->orWhere('segnalazione.id', 'like', $keyword . '%');
73
+          }
74
+        });
75
+      });
76
+    })
77
+    ->filterColumn('tempo_minuti', function ($query, $keyword) {
78
+      $keyword = trim((string) $keyword);
79
+      if ($keyword === '') {
80
+        return;
81
+      }
82
+      $query->where('annotazione.tempo_minuti', 'like', '%' . $keyword . '%');
83
+    })
84
+    ->filterColumn('text', function ($query, $keyword) {
85
+      $keyword = trim((string) $keyword);
86
+      if ($keyword === '') {
87
+        return;
88
+      }
89
+      $query->where('annotazione.text', 'like', '%' . $keyword . '%');
90
+    })
91
+    ->setRowId('id')
92
+    ->rawColumns(['segnalazione_label', 'text_excerpt']);
93
+  }
94
+
95
+  /**
96
+  * Get the query source of dataTable.
97
+  *
98
+  * @return QueryBuilder<Annotazione>
99
+  */
100
+  public function query(Annotazione $model): QueryBuilder
101
+  {
102
+    $builder = $model->newQuery()->where('tempo_minuti', '>', 0);
103
+    if($this->segnalazione){
104
+      $builder->where('segnalazione_id', $this->segnalazione->id);
105
+    }
106
+    return $builder->with(['user', 'segnalazione']);
107
+  }
108
+
109
+  /**
110
+  * Optional method if you want to use the html builder.
111
+  */
112
+  public function html(): HtmlBuilder
113
+  {
114
+    $buttons = [];
115
+    if(Auth::user()->can('create-tempo-impiegato')){
116
+      array_push($buttons, Button::make('create')
117
+      ->editor('editor')
118
+      ->className('btn btn-sm btn-primary mb-4')
119
+      ->formTitle('Registra nuovo tempo impiegato')
120
+      ->formButtons(
121
+        [Button::raw('Annulla')
122
+        ->className('btn btn-secondary ml-2')
123
+        ->actionClose(),
124
+        Button::raw('Salva')
125
+        ->className('btn btn-success ml-2')
126
+        ->actionHandler('create')]
127
+        )
128
+        ->text('<i class="fas fa-plus"></i> Registra tempo'));
129
+      }
130
+
131
+      return $this->builder()
132
+      ->setTableId($this->dataTableVariable)
133
+      ->columns($this->getColumns())
134
+      ->buttons($buttons)
135
+      ->initComplete("function(settings, json){
136
+        initComplete_tempo_impiegato();
137
+                    }")
138
+      ->language(asset('assets/Italian.json'))
139
+      ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
140
+      ->responsive()
141
+      ->minifiedAjax()
142
+      ->orderBy(0)
143
+      ->editor(
144
+        Editor::make()
145
+        ->ajax(route('admin.tempo_impiegato.store'))
146
+        ->fields([
147
+          Fields\Select2::make('user_id', 'Utente')
148
+          ->placeholder('Seleziona l\'utente')
149
+          ->default(Auth::id())
150
+          ->ajax(['url' => route('admin.user.list'), 'dataType' => 'json', 'data' => 'function(params) {
151
+                             return {
152
+                               term: params.term || "",
153
+                               page: params.page || 1
154
+                             }
155
+                           }']),
156
+          Fields\Text::make('tempo_minuti_label', 'Tempo impiegato (1,50 o 1h30m)'),
157
+          Fields\DateTime::make('tempo_data', 'Data/ora')->format('DD/MM/YYYY HH:mm')->default(Carbon::now()->format('d/m/Y H:i')),
158
+          Fields\TextArea::make('text', 'Note')->type('quill'),
159
+          Fields\Select2::make('segnalazione_id', 'Segnalazione')
160
+          ->default($this->segnalazione ? $this->segnalazione->id : 0)
161
+          ->placeholder('Seleziona la segnalazione')
162
+          ->ajax(['url' => route('admin.segnalazione.list'), 'dataType' => 'json', 'data' => 'function(params) {
163
+                             return {
164
+                               term: params.term || "",
165
+                               page: params.page || 1
166
+                             };
167
+                           }']),
168
+          ])
169
+          ->on('initEdit', "function( e, node, data, items, type ){
170
+            if(".(($this->segnalazione != null) ? '1' : '0')."){
171
+              window.LaravelDataTables['dataTable_tempo_impiegato-editor'].hide(['segnalazione_id']);
172
+            }else{
173
+              window.LaravelDataTables['dataTable_tempo_impiegato-editor'].show(['segnalazione_id']);
174
+            }
175
+          }")
176
+          ->on('initCreate', "function( e, node, data, items, type ){
177
+            var editor = window.LaravelDataTables['dataTable_tempo_impiegato-editor'];
178
+            var userField = editor.field('user_id');
179
+            var \$select = userField.input();
180
+            var authId = '".(int) Auth::id()."';
181
+            var authLabel = ".json_encode(Auth::user()->full_name).";
182
+            if (\$select.find(\"option[value='\" + authId + \"']\").length === 0) {
183
+              \$select.append(new Option(authLabel, authId, true, true));
184
+            }
185
+            userField.val(authId);
186
+            if(".(($this->segnalazione != null) ? '1' : '0')."){
187
+              editor.hide(['segnalazione_id']);
188
+            }else{
189
+              editor.show(['segnalazione_id']);
190
+            }
191
+          }")
192
+        );
193
+      }
194
+
195
+      /**
196
+      * Get the dataTable columns definition.
197
+      */
198
+      public function getColumns(): array
199
+      {
200
+        return [
201
+          Column::make('tempo_data')->title('Data/ora')->responsivePriority(1),
202
+          Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
203
+          Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
204
+          Column::make('tempo_minuti')->title('Tempo impiegato')->responsivePriority(3)->data('tempo_minuti_label'),
205
+          Column::make('text')->title('Note')->responsivePriority(3)->data('text_excerpt'),
206
+          Column::computed('action')
207
+          ->title(' ')
208
+          ->exportable(false)
209
+          ->printable(false)
210
+          ->width('10%')
211
+          ->addClass('text-end')
212
+          ->responsivePriority(2),
213
+        ];
214
+      }
215
+    }

+ 99
- 0
app/DataTables/TempoImpiegatoDataTableEditor.php 파일 보기

@@ -0,0 +1,99 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Annotazione;
6
+use App\Models\Config;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Validation\Rule;
9
+use Yajra\DataTables\DataTablesEditor;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Validation\ValidationException;
12
+use Storage;
13
+
14
+class TempoImpiegatoDataTableEditor extends DataTablesEditor
15
+{
16
+  protected $model = Annotazione::class;
17
+  // protected $uploadDir = '';
18
+  // protected $disk = 'modelliRicevute';
19
+
20
+  protected $messages = [
21
+    'segnalazione_id.required' => 'La segnalazione è richiesta',
22
+    'user_id.required' => 'L\'utente è richiesto',
23
+    'tempo_minuti_label.required' => 'Il tempo impiegato è richiesto',
24
+    'tempo_minuti_label.regex' => 'Formato tempo non valido. Usa 1,5 oppure 1h 30m.',
25
+    'tempo_minuti_label.not_regex' => 'Il tempo impiegato deve essere diverso da 0.',
26
+    'tempo_data.required' => 'La data/ora è richiesta',
27
+  ];
28
+
29
+  /**
30
+  * Get create action validation rules.
31
+  *
32
+  * @return array
33
+  */
34
+  public function createRules(): array
35
+  {
36
+    return [
37
+      'segnalazione_id'  => 'required',
38
+      'user_id' => 'required',
39
+      'tempo_minuti_label' => [
40
+        'required',
41
+        'regex:/^\s*(?:\d+(?:[.,]\d+)?|\d+\s*h(?:\s*\d+\s*m)?|\d+\s*m)\s*$/i',
42
+        'not_regex:/^\s*(?:0+(?:[.,]0+)?|0+\s*h(?:\s*0+\s*m)?|0+\s*m)\s*$/i',
43
+      ],
44
+      'tempo_data' => 'required',
45
+    ];
46
+  }
47
+
48
+  public function createMessages(): array{
49
+    return $this->messages;
50
+  }
51
+
52
+  /**
53
+  * Get edit action validation rules.
54
+  *
55
+  * @param Model $model
56
+  * @return array
57
+  */
58
+  public function editRules(Model $model): array
59
+  {
60
+    return [
61
+      'segnalazione_id'  => 'required',
62
+      'user_id' => 'required',
63
+      'tempo_minuti_label' => [
64
+        'required',
65
+        'regex:/^\s*(?:\d+(?:[.,]\d+)?|\d+\s*h(?:\s*\d+\s*m)?|\d+\s*m)\s*$/i',
66
+        'not_regex:/^\s*(?:0+(?:[.,]0+)?|0+\s*h(?:\s*0+\s*m)?|0+\s*m)\s*$/i',
67
+      ],
68
+      'tempo_data' => 'required',
69
+    ];
70
+  }
71
+
72
+  public function editMessages(): array{
73
+    return $this->messages;
74
+  }
75
+
76
+  /**
77
+  * Get remove action validation rules.
78
+  *
79
+  * @param Model $model
80
+  * @return array
81
+  */
82
+  public function removeRules(Model $model): array
83
+  {
84
+    return [];
85
+  }
86
+
87
+  public function creating(Model $model, array $data): array
88
+  {
89
+    $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
90
+    return $data;
91
+  }
92
+
93
+  public function updating(Model $model, array $data): array
94
+  {
95
+    $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
96
+    return $data;
97
+  }
98
+
99
+}

+ 135
- 0
app/DataTables/UserDataTable.php 파일 보기

@@ -0,0 +1,135 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\Azienda;
6
+use App\Models\Role;
7
+use App\Models\User;
8
+use Illuminate\Database\Eloquent\Builder as QueryBuilder;
9
+use Yajra\DataTables\EloquentDataTable;
10
+use Yajra\DataTables\Html\Builder as HtmlBuilder;
11
+use Yajra\DataTables\Html\Button;
12
+use Yajra\DataTables\Html\Column;
13
+use Yajra\DataTables\Html\Editor\Editor;
14
+use Yajra\DataTables\Html\Editor\Fields;
15
+use Yajra\DataTables\Services\DataTable;
16
+use Illuminate\Support\Facades\Auth;
17
+
18
+class UserDataTable extends DataTable
19
+{
20
+  public function __construct(){
21
+    $this->dataTableVariable = 'dataTable_user';
22
+  }
23
+  /**
24
+  * Build the DataTable class.
25
+  *
26
+  * @param QueryBuilder<User> $query Results from query() method.
27
+  */
28
+  public function dataTable(QueryBuilder $query): EloquentDataTable
29
+  {
30
+    return (new EloquentDataTable($query))
31
+    ->addColumn('action', function($entity){
32
+      return view('user.menu', ['entity' => $entity]);
33
+    })
34
+    ->addColumn('ruolo_display', function($entity){
35
+      return $entity->roles->pluck('display_name')->implode(', ');
36
+    })
37
+    ->addColumn('ruolo_id', function($entity){
38
+      return optional($entity->roles->first())->id;
39
+    })
40
+    ->addColumn('azienda_display', function($entity){
41
+      return $entity->azienda ? $entity->azienda->nome : '';
42
+    })
43
+    ->editColumn('azienda_id', function ($entity) {
44
+      return $entity->azienda_id ? (string) $entity->azienda_id : '';
45
+    })
46
+    ->setRowId('id');
47
+  }
48
+
49
+  /**
50
+  * Get the query source of dataTable.
51
+  *
52
+  * @return QueryBuilder<User>
53
+  */
54
+  public function query(User $model): QueryBuilder
55
+  {
56
+    return $model->newQuery()
57
+    ->soloUtenti()
58
+    ->with(['roles', 'azienda']);
59
+  }
60
+
61
+  /**
62
+  * Optional method if you want to use the html builder.
63
+  */
64
+  public function html(): HtmlBuilder
65
+  {
66
+    $buttons = [];
67
+    if(Auth::user()->can('create-user')){
68
+      array_push($buttons, Button::make('create')
69
+      ->editor('editor')
70
+      ->formTitle('Crea nuovo utente')
71
+      ->className('btn btn-sm btn-primary mb-4')
72
+      ->text('<i class="fas fa-plus"></i> Nuovo utente'));
73
+    }
74
+
75
+    if(Auth::user()->hasRole('superadmin')){
76
+      $roles = Role::orderBy('display_name', 'asc')->pluck('id', 'display_name');
77
+    }else{
78
+      $roles = Role::whereNotIn('name', ['superadmin'])->orderBy('display_name', 'asc')->pluck('id', 'display_name');
79
+    }
80
+
81
+    $aziende = Azienda::orderBy('nome', 'asc')->get()->pluck('id', 'nome');
82
+
83
+    $fields = [
84
+      Fields\Text::make('nome')->label('Nome'),
85
+      Fields\Text::make('cognome')->label('Cognome'),
86
+      Fields\Text::make('email')->label('Email'),
87
+      Fields\Text::make('telefono')->label('Telefono'),
88
+      Fields\Select::make('ruolo_id')->label('Ruolo')->options($roles)->default(Role::where('name', 'user')->first()->id),
89
+      Fields\Select::make('azienda_id')->label('Azienda')->options($aziende)->placeholder('Nessuna'),
90
+      Fields\Password::make('password_new')->label('Password (lasciare vuoto per non modificare)'),
91
+      Fields\Password::make('password_new_confirmation')->label('Conferma password'),
92
+    ];
93
+
94
+    return $this->builder()
95
+    ->setTableId($this->dataTableVariable)
96
+    ->initComplete("function(settings, json){
97
+        initComplete_user();
98
+                    }")
99
+    ->language(asset('assets/Italian.json'))
100
+    ->columns($this->getColumns())
101
+    ->dom('<"py-2"<"head-label text-center"><"dt-action-buttons"B>><"d-flex justify-content-between align-items-center row mb-2"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>t<"d-flex justify-content-between row"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6"p>>')
102
+    ->responsive()
103
+    ->minifiedAjax()
104
+    ->editor(
105
+      Editor::make()
106
+      ->ajax(route('admin.user.store'))
107
+      ->fields($fields)
108
+      )
109
+      ->buttons($buttons)
110
+      ->orderBy(0, 'asc');
111
+    }
112
+
113
+    /**
114
+    * Get the dataTable columns definition.
115
+    */
116
+    public function getColumns(): array
117
+    {
118
+      $columns = [
119
+        Column::make('nome')->title('Nome'),
120
+        Column::make('cognome')->title('Cognome'),
121
+        Column::make('email')->title('Email'),
122
+        Column::make('telefono')->title('Telefono'),
123
+        Column::make('ruolo_id')->title('Ruolo')->searchable(false)->name('ruolo_id')->data('ruolo_display'),
124
+        Column::make('azienda_id')->title('Azienda')->data('azienda_display')->name('azienda_id')->searchable(false),
125
+        Column::computed('action')
126
+        ->exportable(false)
127
+        ->printable(false)
128
+        ->width('10%')
129
+        ->title(' ')
130
+        ->addClass('text-end')
131
+      ];
132
+
133
+      return $columns;
134
+    }
135
+  }

+ 127
- 0
app/DataTables/UserDataTableEditor.php 파일 보기

@@ -0,0 +1,127 @@
1
+<?php
2
+
3
+namespace App\DataTables;
4
+
5
+use App\Models\User;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Validation\Rule;
8
+use Yajra\DataTables\DataTablesEditor;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Validation\ValidationException;
11
+use Storage;
12
+use Illuminate\Support\Facades\Hash;
13
+use Illuminate\Support\Str;
14
+
15
+class UserDataTableEditor extends DataTablesEditor
16
+{
17
+  protected $model = User::class;
18
+  // protected $uploadDir = '';
19
+  // protected $disk = 'modelliRicevute';
20
+
21
+  protected $messages = [
22
+    'nome.required' => 'Il nome è richiesto',
23
+    'cognome.required' => 'Il cognome è richiesto',
24
+    'email.required' => 'L\'email è richiesta',
25
+    'ruolo.required' => 'Il ruolo è richiesto',
26
+  ];
27
+
28
+  /**
29
+  * Get create action validation rules.
30
+  *
31
+  * @return array
32
+  */
33
+  public function createRules(): array
34
+  {
35
+    return [
36
+      'nome'  => 'required',
37
+      // 'cognome' => 'required',
38
+      // 'email' => 'required',
39
+      'ruolo_id' => 'required|exists:roles,id'
40
+    ];
41
+  }
42
+
43
+  public function createMessages(): array{
44
+    return $this->messages;
45
+  }
46
+
47
+  /**
48
+  * Get edit action validation rules.
49
+  *
50
+  * @param Model $model
51
+  * @return array
52
+  */
53
+  public function editRules(Model $model): array
54
+  {
55
+    return [
56
+      'nome'  => 'required',
57
+      // 'cognome' => 'required',
58
+      // 'email' => 'required',
59
+      'ruolo_id' => 'required|exists:roles,id'
60
+    ];
61
+  }
62
+
63
+  public function editMessages(): array{
64
+    return $this->messages;
65
+  }
66
+
67
+  /**
68
+  * Get remove action validation rules.
69
+  *
70
+  * @param Model $model
71
+  * @return array
72
+  */
73
+  public function removeRules(Model $model): array
74
+  {
75
+    return [];
76
+  }
77
+
78
+  public function creating(Model $model, array $data): array
79
+  {
80
+    if (array_key_exists('azienda_id', $data) && ($data['azienda_id'] === '' || $data['azienda_id'] === null)) {
81
+      $data['azienda_id'] = null;
82
+    }
83
+
84
+    return $data;
85
+  }
86
+
87
+  public function saving(Model $model, array $data): array
88
+    {
89
+        unset($data['password_new_confirmation']);
90
+
91
+        if (array_key_exists('azienda_id', $data) && ($data['azienda_id'] === '' || $data['azienda_id'] === null)) {
92
+            $data['azienda_id'] = null;
93
+        }
94
+
95
+        if (array_key_exists('password_new', $data)) {
96
+            $password = $data['password_new'];
97
+            unset($data['password_new']);
98
+            if ($password === null || $password === '') {
99
+                // edit: lasciare vuoto = non modificare password
100
+            } else {
101
+                $data['password'] = Hash::make($password);
102
+            }
103
+        }
104
+
105
+        return parent::saving($model, $data);
106
+    }
107
+
108
+  public function created(Model $model, array $data): Model
109
+  {
110
+
111
+    $model->roles()->sync([$data['ruolo_id']]);
112
+
113
+    return parent::created($model, $data);
114
+  }
115
+
116
+  public function updating(Model $model, array $data): array
117
+  {
118
+    // dd($data['ruolo']);
119
+    $model->roles()->sync([$data['ruolo_id']]);
120
+    return $data;
121
+  }
122
+  public function messages(): array
123
+  {
124
+    return $this->messages;
125
+  }
126
+
127
+}

+ 35
- 0
app/Events/NuovaSegnalazione.php 파일 보기

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+namespace App\Events;
4
+
5
+use Illuminate\Broadcasting\InteractsWithSockets;
6
+use Illuminate\Broadcasting\PrivateChannel;
7
+use Illuminate\Foundation\Events\Dispatchable;
8
+use Illuminate\Queue\SerializesModels;
9
+use App\Models\Segnalazione;
10
+
11
+class NuovaSegnalazione
12
+{
13
+    use Dispatchable, InteractsWithSockets, SerializesModels;
14
+
15
+    /**
16
+     * Create a new event instance.
17
+     */
18
+    public function __construct(
19
+        public Segnalazione $segnalazione,
20
+        public bool $notificaApertaDa = false,
21
+    ) {
22
+    }
23
+
24
+    /**
25
+     * Get the channels the event should broadcast on.
26
+     *
27
+     * @return array<int, \Illuminate\Broadcasting\Channel>
28
+     */
29
+    public function broadcastOn(): array
30
+    {
31
+        return [
32
+            new PrivateChannel('channel-name'),
33
+        ];
34
+    }
35
+}

+ 311
- 0
app/Helpers/Helpers.php 파일 보기

@@ -0,0 +1,311 @@
1
+<?php
2
+
3
+namespace App\Helpers;
4
+
5
+use Illuminate\Support\Facades\Config;
6
+use Illuminate\Support\Str;
7
+
8
+class Helpers
9
+{
10
+  /**
11
+   * Generate menu attributes for semi-dark mode
12
+   *
13
+   * @param bool $semiDarkEnabled Whether semi-dark mode is enabled
14
+   * @return array HTML attributes for the menu element
15
+   */
16
+  public static function getMenuAttributes($semiDarkEnabled)
17
+  {
18
+    $attributes = [];
19
+
20
+    if ($semiDarkEnabled) {
21
+      $attributes['data-bs-theme'] = 'dark';
22
+    }
23
+
24
+    return $attributes;
25
+  }
26
+
27
+  public static function appClasses()
28
+  {
29
+
30
+    $data = config('custom.custom');
31
+
32
+
33
+    // default data array
34
+    $DefaultData = [
35
+      'myLayout' => 'vertical',
36
+      'myTheme' => 'light',
37
+      'mySkins' => 'default',
38
+      'hasSemiDark' => false,
39
+      'myRTLMode' => true,
40
+      'hasCustomizer' => true,
41
+      'showDropdownOnHover' => true,
42
+      'displayCustomizer' => true,
43
+      'contentLayout' => 'compact',
44
+      'headerType' => 'fixed',
45
+      'navbarType' => 'sticky',
46
+      'menuFixed' => true,
47
+      'menuCollapsed' => false,
48
+      'footerFixed' => false,
49
+      'customizerControls' => [
50
+        'color',
51
+        'theme',
52
+        'skins',
53
+        'semiDark',
54
+        'layoutCollapsed',
55
+        'layoutNavbarOptions',
56
+        'headerType',
57
+        'contentLayout',
58
+        'rtl'
59
+      ],
60
+      //   'defaultLanguage'=>'en',
61
+    ];
62
+
63
+    // if any key missing of array from custom.php file it will be merge and set a default value from dataDefault array and store in data variable
64
+    $data = array_merge($DefaultData, $data);
65
+
66
+    // All options available in the template
67
+    $allOptions = [
68
+      'myLayout' => ['vertical', 'horizontal', 'blank', 'front'],
69
+      'menuCollapsed' => [true, false],
70
+      'hasCustomizer' => [true, false],
71
+      'showDropdownOnHover' => [true, false],
72
+      'displayCustomizer' => [true, false],
73
+      'contentLayout' => ['compact', 'wide'],
74
+      'headerType' => ['fixed', 'static'],
75
+      'navbarType' => ['sticky', 'static', 'hidden'],
76
+      'myTheme' => ['light', 'dark', 'system'],
77
+      'mySkins' => ['default', 'bordered', 'raspberry'],
78
+      'hasSemiDark' => [true, false],
79
+      'myRTLMode' => [true, false],
80
+      'menuFixed' => [true, false],
81
+      'footerFixed' => [true, false],
82
+      'customizerControls' => [],
83
+      // 'defaultLanguage'=>array('en'=>'en','fr'=>'fr','de'=>'de','ar'=>'ar'),
84
+    ];
85
+
86
+    //if myLayout value empty or not match with default options in custom.php config file then set a default value
87
+    foreach ($allOptions as $key => $value) {
88
+      if (array_key_exists($key, $DefaultData)) {
89
+        if (gettype($DefaultData[$key]) === gettype($data[$key])) {
90
+          // data key should be string
91
+          if (is_string($data[$key])) {
92
+            // data key should not be empty
93
+            if (isset($data[$key]) && $data[$key] !== null) {
94
+              // data key should not be exist inside allOptions array's sub array
95
+              if (!array_key_exists($data[$key], $value)) {
96
+                // ensure that passed value should be match with any of allOptions array value
97
+                $result = array_search($data[$key], $value, 'strict');
98
+                if (empty($result) && $result !== 0) {
99
+                  $data[$key] = $DefaultData[$key];
100
+                }
101
+              }
102
+            } else {
103
+              // if data key not set or
104
+              $data[$key] = $DefaultData[$key];
105
+            }
106
+          }
107
+        } else {
108
+          $data[$key] = $DefaultData[$key];
109
+        }
110
+      }
111
+    }
112
+    $themeVal = $data['myTheme'] == "dark" ? "dark" : "light";
113
+    $themeUpdatedVal = $data['myTheme'] == "dark" ? "dark" : $data['myTheme'];
114
+
115
+    // Determine if the layout is admin or front based on template name
116
+    $layoutName = $data['myLayout'];
117
+    $isAdmin = !Str::contains($layoutName, 'front');
118
+
119
+    $modeCookieName = $isAdmin ? 'admin-mode' : 'front-mode';
120
+    $colorPrefCookieName = $isAdmin ? 'admin-colorPref' : 'front-colorPref';
121
+    $primaryColorCookieName = $isAdmin ? 'admin-primaryColor' : 'front-primaryColor';
122
+
123
+    // Get primary color from custom.php if explicitly set
124
+    $primaryColor = null;
125
+    if (array_key_exists('primaryColor', $data)) {
126
+      $primaryColor = $data['primaryColor'];
127
+    }
128
+
129
+    // Check for primary color in cookie
130
+    if (isset($_COOKIE[$primaryColorCookieName])) {
131
+      $primaryColor = $_COOKIE[$primaryColorCookieName];
132
+    }
133
+
134
+    // Determine style based on cookies, only if not 'blank-layout'
135
+    if ($layoutName !== 'blank') {
136
+      if (isset($_COOKIE[$modeCookieName])) {
137
+        $themeVal = $_COOKIE[$modeCookieName];
138
+        if ($themeVal === 'system') {
139
+          $themeVal = isset($_COOKIE[$colorPrefCookieName]) ? $_COOKIE[$colorPrefCookieName] : 'light';
140
+        }
141
+        $themeUpdatedVal = $_COOKIE[$modeCookieName];
142
+      }
143
+    }
144
+
145
+    // Define standardized cookie names
146
+    $skinCookieName = 'customize_skin';
147
+    $semiDarkCookieName = 'customize_semi_dark';
148
+
149
+    // Process skin and semi-dark settings only for admin layouts
150
+    if ($isAdmin) {
151
+      // Get skin from cookie or fall back to config
152
+      $skinFromCookie = isset($_COOKIE[$skinCookieName]) ? $_COOKIE[$skinCookieName] : null;
153
+      $configSkin = isset($data['mySkins']) ? $data['mySkins'] : 'default';
154
+      $skinName = $skinFromCookie ?: $configSkin;
155
+
156
+      // Get semi-dark setting from cookie or fall back to config
157
+      $semiDarkFromCookie = isset($_COOKIE[$semiDarkCookieName]) ? $_COOKIE[$semiDarkCookieName] : null;
158
+      // Ensure we have a proper boolean conversion
159
+      $semiDarkEnabled = $semiDarkFromCookie !== null ?
160
+        filter_var($semiDarkFromCookie, FILTER_VALIDATE_BOOLEAN) :
161
+        (bool)$data['hasSemiDark'];
162
+    } else {
163
+      // For front-end layouts, use defaults
164
+      $skinName = 'default';
165
+      $semiDarkEnabled = false;
166
+    }
167
+
168
+    // Get menu Collapsed state from cookie or fall back to config
169
+    $menuCollapsedFromCookie = isset($_COOKIE['LayoutCollapsed']) ? $_COOKIE['LayoutCollapsed'] : $data['menuCollapsed'];
170
+
171
+    // Get content layout from cookie or fall back to config
172
+    $contentLayoutFromCookie = isset($_COOKIE['contentLayout']) ? $_COOKIE['contentLayout'] : $data['contentLayout'];
173
+
174
+    // Get header type from cookie or fall back to config
175
+    $navbarTypeFromCookie = isset($_COOKIE['navbarType']) ? $_COOKIE['navbarType'] : $data['navbarType'];
176
+
177
+    // Get Header type from cookie or fall back to config
178
+    $headerTypeFromCookie = isset($_COOKIE['headerType']) ? $_COOKIE['headerType'] : $data['headerType'];
179
+
180
+    $directionVal = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === 'true' ? 'rtl' : 'ltr') : $data['myRTLMode'];
181
+
182
+    //layout classes
183
+    $layoutClasses = [
184
+      'layout' => $data['myLayout'],
185
+      'skins' => $data['mySkins'],
186
+      'skinName' => $skinName,
187
+      'semiDark' => $semiDarkEnabled,
188
+      'color' => $primaryColor,
189
+      'theme' => $themeVal,
190
+      'themeOpt' => $data['myTheme'],
191
+      'themeOptVal' => $themeUpdatedVal,
192
+      'rtlMode' => $data['myRTLMode'],
193
+      'textDirection' => $directionVal,
194
+      'menuCollapsed' => $menuCollapsedFromCookie,
195
+      'hasCustomizer' => $data['hasCustomizer'],
196
+      'showDropdownOnHover' => $data['showDropdownOnHover'],
197
+      'displayCustomizer' => $data['displayCustomizer'],
198
+      'contentLayout' => $contentLayoutFromCookie,
199
+      'headerType' => $headerTypeFromCookie,
200
+      'navbarType' => $navbarTypeFromCookie,
201
+      'menuFixed' => $data['menuFixed'],
202
+      'footerFixed' => $data['footerFixed'],
203
+      'customizerControls' => $data['customizerControls'],
204
+      'menuAttributes' => self::getMenuAttributes($semiDarkEnabled),
205
+    ];
206
+
207
+    // sidebar Collapsed
208
+    if ($layoutClasses['menuCollapsed'] === 'true' || $layoutClasses['menuCollapsed'] === true) {
209
+      $layoutClasses['menuCollapsed'] = 'layout-menu-collapsed';
210
+    } else {
211
+      $layoutClasses['menuCollapsed'] = '';
212
+    }
213
+
214
+    // Header Type
215
+    if ($layoutClasses['headerType'] == 'fixed') {
216
+      $layoutClasses['headerType'] = 'layout-menu-fixed';
217
+    }
218
+    // Navbar Type
219
+    if ($layoutClasses['navbarType'] == 'sticky') {
220
+      $layoutClasses['navbarType'] = 'layout-navbar-fixed';
221
+    } elseif ($layoutClasses['navbarType'] == 'static') {
222
+      $layoutClasses['navbarType'] = '';
223
+    } else {
224
+      $layoutClasses['navbarType'] = 'layout-navbar-hidden';
225
+    }
226
+
227
+    // Menu Fixed
228
+    if ($layoutClasses['menuFixed'] == true) {
229
+      $layoutClasses['menuFixed'] = 'layout-menu-fixed';
230
+    }
231
+
232
+
233
+    // Footer Fixed
234
+    if ($layoutClasses['footerFixed'] == true) {
235
+      $layoutClasses['footerFixed'] = 'layout-footer-fixed';
236
+    }
237
+
238
+    // RTL Layout/Mode
239
+    if ($layoutClasses['rtlMode'] == true) {
240
+      $layoutClasses['rtlMode'] = 'rtl';
241
+      $layoutClasses['textDirection'] = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === 'true' ? 'rtl' : 'ltr') : 'rtl';
242
+    } else {
243
+      $layoutClasses['rtlMode'] = 'ltr';
244
+      $layoutClasses['textDirection'] = isset($_COOKIE['direction']) && $_COOKIE['direction'] === 'true' ? 'rtl' : 'ltr';
245
+    }
246
+
247
+    // Show DropdownOnHover for Horizontal Menu
248
+    if ($layoutClasses['showDropdownOnHover'] == true) {
249
+      $layoutClasses['showDropdownOnHover'] = true;
250
+    } else {
251
+      $layoutClasses['showDropdownOnHover'] = false;
252
+    }
253
+
254
+    // To hide/show display customizer UI, not js
255
+    if ($layoutClasses['displayCustomizer'] == true) {
256
+      $layoutClasses['displayCustomizer'] = true;
257
+    } else {
258
+      $layoutClasses['displayCustomizer'] = false;
259
+    }
260
+
261
+    return $layoutClasses;
262
+  }
263
+
264
+  public static function updatePageConfig($pageConfigs)
265
+  {
266
+    $demo = 'custom';
267
+    if (isset($pageConfigs)) {
268
+      if (count($pageConfigs) > 0) {
269
+        foreach ($pageConfigs as $config => $val) {
270
+          Config::set('custom.' . $demo . '.' . $config, $val);
271
+        }
272
+      }
273
+    }
274
+  }
275
+
276
+  /**
277
+   * Generate CSS for primary color
278
+   *
279
+   * @param string $color Hex color code for primary color
280
+   * @return string CSS for primary color
281
+   */
282
+  public static function generatePrimaryColorCSS($color)
283
+  {
284
+    if (!$color) return '';
285
+
286
+    // Check if the color actually came from a cookie or explicit configuration
287
+    // Don't generate CSS if there's no specific need for a custom color
288
+    $configColor = config('custom.custom.primaryColor', null);
289
+    $isFromCookie = isset($_COOKIE['admin-primaryColor']) || isset($_COOKIE['front-primaryColor']);
290
+
291
+    if (!$configColor && !$isFromCookie) return '';
292
+
293
+    $r = hexdec(substr($color, 1, 2));
294
+    $g = hexdec(substr($color, 3, 2));
295
+    $b = hexdec(substr($color, 5, 2));
296
+
297
+    // Calculate contrast color based on YIQ formula
298
+    $yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
299
+    $contrastColor = ($yiq >= 150) ? '#000' : '#fff';
300
+
301
+    return <<<CSS
302
+:root, [data-bs-theme=light], [data-bs-theme=dark] {
303
+  --bs-primary: {$color};
304
+  --bs-primary-rgb: {$r}, {$g}, {$b};
305
+  --bs-primary-bg-subtle: rgba({$r}, {$g}, {$b}, 0.1);
306
+  --bs-primary-border-subtle: rgba({$r}, {$g}, {$b}, 0.3);
307
+  --bs-primary-contrast: {$contrastColor};
308
+}
309
+CSS;
310
+  }
311
+}

+ 137
- 0
app/Http/Controllers/AcquistoServizioFirmaController.php 파일 보기

@@ -0,0 +1,137 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\AcquistoServizio;
6
+use App\Services\AcquistoServizioAccettazioneService;
7
+use App\Services\AcquistoServizioRicevutaPdfService;
8
+use App\Services\AcquistoServizioRiconsegnaService;
9
+use Illuminate\Http\Request;
10
+
11
+class AcquistoServizioFirmaController extends Controller
12
+{
13
+    public function show(string $token)
14
+    {
15
+        $acquisto = $this->findByFirmaConsegnaToken($token);
16
+        abort_unless($acquisto->canOpenFirmaConsegna(), 404);
17
+
18
+        return view('acquisto_servizio.public.firma-consegna', [
19
+            'acquisto' => $acquisto->loadMissing('prodotto'),
20
+            'alreadySigned' => $acquisto->has_firma_consegna,
21
+            'accessoriLabels' => $acquisto->accessoriLabelsForFirma(),
22
+            'noteOperatore' => $acquisto->note_operatore
23
+                ?: (is_array($acquisto->accettazione_pending)
24
+                    ? ($acquisto->accettazione_pending['note_operatore'] ?? null)
25
+                    : null),
26
+        ]);
27
+    }
28
+
29
+    public function store(
30
+        Request $request,
31
+        string $token,
32
+        AcquistoServizioRicevutaPdfService $pdfService,
33
+        AcquistoServizioAccettazioneService $accettazione
34
+    ) {
35
+        $acquisto = $this->findByFirmaConsegnaToken($token);
36
+        abort_unless($acquisto->canOpenFirmaConsegna(), 404);
37
+
38
+        if ($acquisto->has_firma_consegna) {
39
+            return redirect()->route('shop.firma_consegna.show', $token)
40
+                ->with('success', 'Hai già firmato questa ricevuta.');
41
+        }
42
+
43
+        $binary = $this->decodeFirmaPng($request);
44
+        if ($binary instanceof \Illuminate\Http\RedirectResponse) {
45
+            return $binary;
46
+        }
47
+
48
+        $accettazione->completeFromSmsFirma($acquisto, $binary, (string) $request->ip(), $pdfService);
49
+
50
+        return redirect()->route('shop.firma_consegna.show', $token)
51
+            ->with('success', 'Grazie: firma registrata. Il PC è stato preso in carico. Puoi chiudere questa pagina.');
52
+    }
53
+
54
+    public function showRiconsegna(string $token)
55
+    {
56
+        $acquisto = $this->findByFirmaRiconsegnaToken($token);
57
+        abort_unless($acquisto->canOpenFirmaRiconsegna(), 404);
58
+
59
+        return view('acquisto_servizio.public.firma-riconsegna', [
60
+            'acquisto' => $acquisto->loadMissing('prodotto'),
61
+            'alreadySigned' => $acquisto->has_firma_riconsegna,
62
+        ]);
63
+    }
64
+
65
+    public function storeRiconsegna(
66
+        Request $request,
67
+        string $token,
68
+        AcquistoServizioRicevutaPdfService $pdfService,
69
+        AcquistoServizioRiconsegnaService $riconsegna
70
+    ) {
71
+        $acquisto = $this->findByFirmaRiconsegnaToken($token);
72
+        abort_unless($acquisto->canOpenFirmaRiconsegna(), 404);
73
+
74
+        if ($acquisto->has_firma_riconsegna) {
75
+            return redirect()->route('shop.firma_riconsegna.show', $token)
76
+                ->with('success', 'Hai già firmato il ritiro.');
77
+        }
78
+
79
+        $binary = $this->decodeFirmaPng($request);
80
+        if ($binary instanceof \Illuminate\Http\RedirectResponse) {
81
+            return $binary;
82
+        }
83
+
84
+        $riconsegna->completeFromSmsFirma($acquisto, $binary, (string) $request->ip(), $pdfService);
85
+
86
+        return redirect()->route('shop.firma_riconsegna.show', $token)
87
+            ->with('success', 'Grazie: firma di ritiro registrata. La riconsegna è completata. Puoi chiudere questa pagina.');
88
+    }
89
+
90
+    /**
91
+     * @return string|\Illuminate\Http\RedirectResponse
92
+     */
93
+    private function decodeFirmaPng(Request $request)
94
+    {
95
+        $validated = $request->validate([
96
+            'firma' => 'required|string',
97
+            'privacy_firma' => 'accepted',
98
+        ], [
99
+            'privacy_firma.accepted' => 'Per salvare la firma è necessario accettare l’informativa privacy.',
100
+        ]);
101
+
102
+        $dataUrl = $validated['firma'];
103
+        if (! preg_match('#^data:image/(png|jpeg|jpg);base64,#i', $dataUrl)) {
104
+            return back()->withErrors(['firma' => 'Firma non valida. Disegna la firma nel riquadro.']);
105
+        }
106
+
107
+        $base64 = preg_replace('#^data:image/\w+;base64,#i', '', $dataUrl);
108
+        $binary = base64_decode($base64, true);
109
+        if ($binary === false || strlen($binary) < 100) {
110
+            return back()->withErrors(['firma' => 'Firma non valida o troppo corta.']);
111
+        }
112
+
113
+        return $binary;
114
+    }
115
+
116
+    private function findByFirmaConsegnaToken(string $token): AcquistoServizio
117
+    {
118
+        $acquisto = AcquistoServizio::query()
119
+            ->where('meta->firma_consegna_token', $token)
120
+            ->first();
121
+
122
+        abort_if(! $acquisto, 404);
123
+
124
+        return $acquisto;
125
+    }
126
+
127
+    private function findByFirmaRiconsegnaToken(string $token): AcquistoServizio
128
+    {
129
+        $acquisto = AcquistoServizio::query()
130
+            ->where('meta->firma_riconsegna_token', $token)
131
+            ->first();
132
+
133
+        abort_if(! $acquisto, 404);
134
+
135
+        return $acquisto;
136
+    }
137
+}

+ 577
- 0
app/Http/Controllers/AcquistoServizioOperatoreController.php 파일 보기

@@ -0,0 +1,577 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\AcquistoServizio;
6
+use App\Models\Annotazione;
7
+use App\Models\Prodotto;
8
+use App\Models\PromoCode;
9
+use App\Notifications\AcquistoServizioPcProntoRitiro;
10
+use App\Services\AcquistoServizioAccettazioneService;
11
+use App\Services\AcquistoServizioPaymentService;
12
+use App\Services\AcquistoServizioRicevutaPdfService;
13
+use App\Services\AcquistoServizioRiconsegnaService;
14
+use App\Services\SmsService;
15
+use Illuminate\Http\Request;
16
+use Illuminate\Routing\Controllers\HasMiddleware;
17
+use Illuminate\Routing\Controllers\Middleware;
18
+use Illuminate\Support\Facades\Auth;
19
+use Illuminate\Support\Facades\Notification;
20
+use Illuminate\Validation\ValidationException;
21
+
22
+class AcquistoServizioOperatoreController extends Controller implements HasMiddleware
23
+{
24
+    public static $permission_group = 'Acquisti servizio PC';
25
+
26
+    public static $permissions = [
27
+        'view-acquisto-servizio' => 'Vedi coda accettazione/riconsegna',
28
+        'manage-acquisto-servizio' => 'Accettazione e riconsegna PC',
29
+    ];
30
+
31
+    public static function middleware(): array
32
+    {
33
+        return [
34
+            new Middleware('permission:view-acquisto-servizio', only: [
35
+                'admin_index', 'admin_show', 'admin_pdf', 'admin_firma_status',
36
+                'admin_invia_firma_sms', 'admin_invia_firma_riconsegna_sms',
37
+            ]),
38
+            new Middleware('permission:manage-acquisto-servizio', only: [
39
+                'admin_create', 'admin_store', 'admin_accetta', 'admin_pronto', 'admin_riconsegna',
40
+            ]),
41
+        ];
42
+    }
43
+
44
+    public function admin_index(Request $request)
45
+    {
46
+        $tab = $request->input('tab', 'da_accettare');
47
+
48
+        $daAccettare = AcquistoServizio::query()
49
+            ->with(['prodotto', 'segnalazione'])
50
+            ->daAccettare()
51
+            ->orderByDesc('id')
52
+            ->get();
53
+
54
+        $inLavorazione = AcquistoServizio::query()
55
+            ->with(['prodotto', 'segnalazione'])
56
+            ->inLavorazione()
57
+            ->orderByDesc('id')
58
+            ->get();
59
+
60
+        $daRiconsegnare = AcquistoServizio::query()
61
+            ->with(['prodotto', 'segnalazione'])
62
+            ->daRiconsegnare()
63
+            ->orderByDesc('id')
64
+            ->get();
65
+
66
+        $domicilio = AcquistoServizio::query()
67
+            ->with(['prodotto', 'segnalazione'])
68
+            ->where('payment_status', AcquistoServizio::PAYMENT_PAID)
69
+            ->where('modalita', AcquistoServizio::MODALITA_DOMICILIO)
70
+            ->where(function ($q) {
71
+                $q->whereNull('meta->riconsegnato_at')->orWhere('meta->riconsegnato_at', '');
72
+            })
73
+            ->orderByDesc('id')
74
+            ->get();
75
+
76
+        $riconsegnati = AcquistoServizio::query()
77
+            ->with(['prodotto', 'segnalazione'])
78
+            ->riconsegnati()
79
+            ->orderByDesc('id')
80
+            ->get();
81
+
82
+        return view('acquisto_servizio.operatore.index', compact(
83
+            'tab', 'daAccettare', 'inLavorazione', 'daRiconsegnare', 'domicilio', 'riconsegnati'
84
+        ));
85
+    }
86
+
87
+    public function admin_create()
88
+    {
89
+        $servizi = Prodotto::query()
90
+            ->where('attivo', true)
91
+            ->where('tipo', 'servizio')
92
+            ->where('prenotabile_online', true)
93
+            ->orderBy('nome')
94
+            ->get();
95
+
96
+        return view('acquisto_servizio.operatore.create', [
97
+            'servizi' => $servizi,
98
+            'costoDomicilio' => AcquistoServizio::costoDomicilio(),
99
+        ]);
100
+    }
101
+
102
+    public function admin_store(
103
+        Request $request,
104
+        AcquistoServizioPaymentService $payments,
105
+        AcquistoServizioRicevutaPdfService $pdfService,
106
+        SmsService $sms,
107
+        AcquistoServizioAccettazioneService $accettazione
108
+    ) {
109
+        $validated = $request->validate([
110
+            'prodotto_id' => 'required|exists:prodotto,id',
111
+            'nome' => 'required|string|max:255',
112
+            'codice_fiscale' => 'required|string|min:11|max:16',
113
+            'email' => 'required|email|max:255',
114
+            'telefono' => 'required|string|max:50',
115
+            'indirizzo' => 'required|string|max:500',
116
+            'note_cliente' => 'nullable|string|max:5000',
117
+            'promo_codice' => 'nullable|string|max:50',
118
+            'foto' => 'nullable|image|max:10240',
119
+            'note_operatore' => 'nullable|string|max:255',
120
+            'foto_accettazione' => 'nullable|array',
121
+            'foto_accettazione.*' => 'image|max:10240',
122
+            'importo_contanti' => 'nullable|numeric|min:0',
123
+            'accessori' => 'nullable|array',
124
+            'accessori.*' => 'string|in:'.implode(',', array_keys(AcquistoServizio::ACCESSORI)),
125
+            'accessori_altro' => 'nullable|string|max:255',
126
+            'modalita_firma' => 'required|in:'.implode(',', array_keys(AcquistoServizio::MODALITA_FIRMA)),
127
+            'privacy_firma' => 'accepted',
128
+        ], [
129
+            'privacy_firma.accepted' => 'È necessario che il cliente accetti l’informativa privacy.',
130
+        ]);
131
+
132
+        $prodotto = Prodotto::query()
133
+            ->where('attivo', true)
134
+            ->where('tipo', 'servizio')
135
+            ->where('prenotabile_online', true)
136
+            ->findOrFail($validated['prodotto_id']);
137
+
138
+        $promo = null;
139
+        if (! empty($validated['promo_codice'])) {
140
+            $promo = PromoCode::findValidByCodice($validated['promo_codice'], (int) $prodotto->id);
141
+            if (! $promo) {
142
+                return back()->withInput()->withErrors(['promo_codice' => 'Codice promo non valido per questo servizio.']);
143
+            }
144
+        }
145
+
146
+        $modalita = AcquistoServizio::MODALITA_NEGOZIO;
147
+        $totals = AcquistoServizio::computeTotals($prodotto, $modalita, $promo);
148
+
149
+        $meta = [
150
+            'origine_registrazione' => 'negozio',
151
+            'registrato_da_id' => Auth::id(),
152
+            'privacy_firma_accepted_at' => now()->toIso8601String(),
153
+        ];
154
+        if ($request->hasFile('foto')) {
155
+            $meta['temp_foto'] = [
156
+                $request->file('foto')->store('acquisto_servizio/'.uniqid('foto_', true), 'temp'),
157
+            ];
158
+        }
159
+
160
+        $acquisto = new AcquistoServizio();
161
+        $acquisto->prodotto_id = $prodotto->id;
162
+        $acquisto->promo_code_id = $promo?->id;
163
+        $acquisto->nome = $validated['nome'];
164
+        $acquisto->email = $validated['email'];
165
+        $acquisto->telefono = $validated['telefono'];
166
+        $acquisto->codice_fiscale = strtoupper(trim($validated['codice_fiscale']));
167
+        $acquisto->indirizzo = trim($validated['indirizzo']);
168
+        $acquisto->note_cliente = isset($validated['note_cliente']) && trim((string) $validated['note_cliente']) !== ''
169
+            ? trim((string) $validated['note_cliente'])
170
+            : null;
171
+        $acquisto->modalita = $modalita;
172
+        $acquisto->prezzo_servizio = $totals['prezzo_servizio'];
173
+        $acquisto->costo_domicilio = $totals['costo_domicilio'];
174
+        $acquisto->sconto = $totals['sconto'];
175
+        $acquisto->totale = $totals['totale'];
176
+        $acquisto->provider = 'contanti';
177
+        $acquisto->payment_status = AcquistoServizio::PAYMENT_PENDING;
178
+        $acquisto->condizioni_accettate_at = now();
179
+        $acquisto->meta = $meta;
180
+        $acquisto->save();
181
+
182
+        $acquisto = $payments->markPaidContanti($acquisto);
183
+        $acquisto->refresh();
184
+        $acquisto->load(['segnalazione', 'prodotto', 'promoCode']);
185
+
186
+        $importoContanti = $request->filled('importo_contanti')
187
+            ? (float) $validated['importo_contanti']
188
+            : (float) $acquisto->totale;
189
+        $acquisto->mergeMeta(['importo_contanti' => $importoContanti]);
190
+        $acquisto->save();
191
+
192
+        $this->performAccettazione(
193
+            $acquisto,
194
+            $validated['note_operatore'] ?? null,
195
+            $request,
196
+            $pdfService,
197
+            'foto_accettazione',
198
+            $validated['accessori'] ?? [],
199
+            $validated['accessori_altro'] ?? null,
200
+            $validated['modalita_firma'],
201
+            $sms,
202
+            $accettazione
203
+        );
204
+
205
+        return $this->redirectAfterAccettazione($acquisto, $validated['modalita_firma'], $sms);
206
+    }
207
+
208
+    public function admin_show($id)
209
+    {
210
+        $acquisto = AcquistoServizio::with(['prodotto', 'promoCode', 'segnalazione.todos.completatoDa'])->findOrFail($id);
211
+
212
+        return view('acquisto_servizio.operatore.show', compact('acquisto'));
213
+    }
214
+
215
+    public function admin_firma_status($id)
216
+    {
217
+        $acquisto = AcquistoServizio::findOrFail($id);
218
+
219
+        return response()->json([
220
+            'accettazione_pending' => (bool) $acquisto->has_accettazione_pending,
221
+            'riconsegna_pending' => (bool) $acquisto->has_riconsegna_pending,
222
+            'is_accettato' => (bool) $acquisto->is_accettato,
223
+            'is_riconsegnato' => (bool) $acquisto->is_riconsegnato,
224
+            'has_firma_consegna' => (bool) $acquisto->has_firma_consegna,
225
+            'has_firma_riconsegna' => (bool) $acquisto->has_firma_riconsegna,
226
+        ]);
227
+    }
228
+
229
+    public function admin_accetta(
230
+        Request $request,
231
+        $id,
232
+        AcquistoServizioRicevutaPdfService $pdfService,
233
+        SmsService $sms,
234
+        AcquistoServizioAccettazioneService $accettazione
235
+    ) {
236
+        $acquisto = AcquistoServizio::with(['segnalazione', 'prodotto', 'promoCode'])->findOrFail($id);
237
+        abort_unless($acquisto->is_paid && $acquisto->is_negozio && ! $acquisto->is_accettato, 422);
238
+
239
+        $validated = $request->validate([
240
+            'note_operatore' => 'nullable|string|max:255',
241
+            'foto' => 'nullable|array',
242
+            'foto.*' => 'image|max:10240',
243
+            'accessori' => 'nullable|array',
244
+            'accessori.*' => 'string|in:'.implode(',', array_keys(AcquistoServizio::ACCESSORI)),
245
+            'accessori_altro' => 'nullable|string|max:255',
246
+            'modalita_firma' => 'required|in:'.implode(',', array_keys(AcquistoServizio::MODALITA_FIRMA)),
247
+            'firma' => 'exclude_unless:modalita_firma,device|required|string',
248
+            'privacy_firma' => 'exclude_unless:modalita_firma,device|accepted',
249
+        ], [
250
+            'firma.required' => 'Disegna la firma del cliente sul dispositivo.',
251
+            'privacy_firma.accepted' => 'È necessario accettare l’informativa privacy per salvare la firma.',
252
+        ]);
253
+
254
+        $this->performAccettazione(
255
+            $acquisto,
256
+            $validated['note_operatore'] ?? null,
257
+            $request,
258
+            $pdfService,
259
+            'foto',
260
+            $validated['accessori'] ?? [],
261
+            $validated['accessori_altro'] ?? null,
262
+            $validated['modalita_firma'],
263
+            $sms,
264
+            $accettazione
265
+        );
266
+
267
+        return $this->redirectAfterAccettazione($acquisto, $validated['modalita_firma'], $sms);
268
+    }
269
+
270
+    public function admin_invia_firma_sms($id, SmsService $sms)
271
+    {
272
+        $acquisto = AcquistoServizio::findOrFail($id);
273
+        $canResend = ($acquisto->has_accettazione_pending && ! $acquisto->has_firma_consegna)
274
+            || ($acquisto->is_accettato && ! $acquisto->has_firma_consegna);
275
+        abort_unless($canResend, 422);
276
+
277
+        $acquisto->ensureFirmaConsegnaToken();
278
+        $acquisto->mergeMeta(['modalita_firma' => AcquistoServizio::FIRMA_SMS]);
279
+        $acquisto->save();
280
+
281
+        $result = $sms->sendFirmaConsegnaLink($acquisto);
282
+        $redirect = redirect()->route('admin.acquisto_servizio.show', $acquisto->id);
283
+
284
+        if ($result['ok']) {
285
+            return $redirect->with('success', 'Link di firma inviato via SMS. Il PC sarà preso in carico dopo la firma del cliente.');
286
+        }
287
+
288
+        return $redirect->with('error', 'Invio SMS non riuscito: '.($result['error'] ?? 'errore sconosciuto'));
289
+    }
290
+
291
+    public function admin_invia_firma_riconsegna_sms($id, SmsService $sms)
292
+    {
293
+        $acquisto = AcquistoServizio::findOrFail($id);
294
+        $canResend = ($acquisto->has_riconsegna_pending && ! $acquisto->has_firma_riconsegna)
295
+            || ($acquisto->is_riconsegnato && ! $acquisto->has_firma_riconsegna);
296
+        abort_unless($canResend, 422);
297
+
298
+        $acquisto->ensureFirmaRiconsegnaToken();
299
+        $acquisto->mergeMeta(['modalita_firma_riconsegna' => AcquistoServizio::FIRMA_SMS]);
300
+        $acquisto->save();
301
+
302
+        $result = $sms->sendFirmaRiconsegnaLink($acquisto);
303
+        $redirect = redirect()->route('admin.acquisto_servizio.show', $acquisto->id);
304
+
305
+        if ($result['ok']) {
306
+            return $redirect->with('success', 'Link di firma ritiro inviato via SMS. La riconsegna sarà completata dopo la firma del cliente.');
307
+        }
308
+
309
+        return $redirect->with('error', 'Invio SMS non riuscito: '.($result['error'] ?? 'errore sconosciuto'));
310
+    }
311
+
312
+    public function admin_pdf($id, AcquistoServizioRicevutaPdfService $pdfService)
313
+    {
314
+        $acquisto = AcquistoServizio::with(['prodotto', 'promoCode', 'segnalazione'])->findOrFail($id);
315
+        abort_unless(
316
+            $acquisto->is_accettato || $acquisto->has_accettazione_pending || $acquisto->has_riconsegna_pending,
317
+            404
318
+        );
319
+
320
+        $filename = $pdfService->filename($acquisto);
321
+        $binary = $pdfService->binary($acquisto);
322
+
323
+        // Aggiorna la copia allegata solo se il PDF non è ancora definitivo
324
+        // (dopo la firma di riconsegna i PNG sono stati eliminati).
325
+        if (! $pdfService->isFinale($acquisto)) {
326
+            $stored = $acquisto->getMetaValue('ricevuta_pdf');
327
+            if (is_string($stored) && $stored !== '') {
328
+                Storage::disk('allegatiAnnotazioni')->put($stored, $binary);
329
+            }
330
+        }
331
+
332
+        return response()->stream(function () use ($binary) {
333
+            echo $binary;
334
+        }, 200, [
335
+            'Content-Type' => 'application/pdf',
336
+            'Content-Disposition' => 'inline; filename="'.$filename.'"',
337
+        ]);
338
+    }
339
+
340
+    public function admin_pronto($id)
341
+    {
342
+        $acquisto = AcquistoServizio::with(['segnalazione', 'prodotto'])->findOrFail($id);
343
+        abort_unless($acquisto->is_paid && ! $acquisto->is_riconsegnato, 422);
344
+        if ($acquisto->is_negozio) {
345
+            abort_unless($acquisto->is_accettato, 422);
346
+        }
347
+
348
+        $acquisto->markProntoRitiro();
349
+
350
+        if ($acquisto->segnalazione) {
351
+            $acquisto->segnalazione->stato = 'pronto_ritiro';
352
+            $acquisto->segnalazione->save();
353
+
354
+            $annotazione = new Annotazione();
355
+            $annotazione->segnalazione_id = $acquisto->segnalazione_id;
356
+            $annotazione->user_id = Auth::id();
357
+            $annotazione->text = 'PC pronto per il ritiro. Contattare il cliente.';
358
+            $annotazione->riservata = true;
359
+            $annotazione->save();
360
+        }
361
+
362
+        $emailInviata = false;
363
+        if (filled($acquisto->email) && ! $acquisto->getMetaValue('email_pronto_ritiro_inviata_at')) {
364
+            Notification::route('mail', $acquisto->email)
365
+                ->notify(new AcquistoServizioPcProntoRitiro($acquisto));
366
+            $acquisto->mergeMeta([
367
+                'email_pronto_ritiro_inviata_at' => now()->toIso8601String(),
368
+            ]);
369
+            $acquisto->save();
370
+            $emailInviata = true;
371
+        }
372
+
373
+        $msg = 'Segnato come pronto per il ritiro.';
374
+        if ($emailInviata) {
375
+            $msg .= ' Email di avviso inviata al cliente.';
376
+        } elseif (! filled($acquisto->email)) {
377
+            $msg .= ' Nessuna email cliente: avviso non inviato.';
378
+        }
379
+
380
+        return redirect()->route('admin.acquisto_servizio.show', $acquisto->id)
381
+            ->with('success', $msg);
382
+    }
383
+
384
+    public function admin_riconsegna(
385
+        Request $request,
386
+        $id,
387
+        AcquistoServizioRicevutaPdfService $pdfService,
388
+        SmsService $sms,
389
+        AcquistoServizioRiconsegnaService $riconsegna
390
+    ) {
391
+        $acquisto = AcquistoServizio::with(['segnalazione', 'prodotto'])->findOrFail($id);
392
+        if ($acquisto->is_negozio) {
393
+            abort_unless($acquisto->is_pronto_ritiro && ! $acquisto->is_riconsegnato, 422);
394
+        } else {
395
+            abort_unless($acquisto->is_paid && ! $acquisto->is_riconsegnato, 422);
396
+        }
397
+
398
+        $validated = $request->validate([
399
+            'note_riconsegna' => 'nullable|string',
400
+            'modalita_firma' => 'required|in:'.implode(',', array_keys(AcquistoServizio::MODALITA_FIRMA)),
401
+            'firma' => 'exclude_unless:modalita_firma,device|required|string',
402
+            'privacy_firma' => 'exclude_unless:modalita_firma,device|accepted',
403
+        ], [
404
+            'firma.required' => 'Disegna la firma del cliente sul dispositivo.',
405
+            'privacy_firma.accepted' => 'È necessario accettare l’informativa privacy per salvare la firma.',
406
+        ]);
407
+
408
+        $modalitaFirma = $validated['modalita_firma'];
409
+        $note = $validated['note_riconsegna'] ?? null;
410
+
411
+        if ($acquisto->segnalazione) {
412
+            $blockClose = $acquisto->segnalazione->closingBlockedMessage('chiusa');
413
+            if ($blockClose) {
414
+                return redirect()->back()->withInput()->with('error', $blockClose);
415
+            }
416
+        }
417
+
418
+        if ($modalitaFirma === AcquistoServizio::FIRMA_SMS) {
419
+            $riconsegna->prepareSmsPending($acquisto, (int) Auth::id(), $note);
420
+
421
+            return $this->redirectAfterRiconsegna($acquisto, $modalitaFirma, $sms);
422
+        }
423
+
424
+        $firmaBinary = null;
425
+        if ($modalitaFirma === AcquistoServizio::FIRMA_DEVICE) {
426
+            $firmaBinary = $this->decodeFirmaPng($request->input('firma'));
427
+        }
428
+
429
+        $riconsegna->complete(
430
+            $acquisto,
431
+            (int) Auth::id(),
432
+            $note,
433
+            $modalitaFirma,
434
+            $pdfService,
435
+            $firmaBinary,
436
+            $request->ip()
437
+        );
438
+
439
+        return $this->redirectAfterRiconsegna($acquisto, $modalitaFirma, $sms);
440
+    }
441
+
442
+    private function redirectAfterRiconsegna(
443
+        AcquistoServizio $acquisto,
444
+        string $modalitaFirma,
445
+        SmsService $sms
446
+    ) {
447
+        $acquisto->refresh();
448
+        $redirect = redirect()->route('admin.acquisto_servizio.show', $acquisto->id);
449
+
450
+        if ($modalitaFirma === AcquistoServizio::FIRMA_SMS) {
451
+            $result = $sms->sendFirmaRiconsegnaLink($acquisto);
452
+
453
+            if ($result['ok']) {
454
+                return $redirect
455
+                    ->with('success', 'Link di firma ritiro inviato via SMS. La riconsegna sarà completata quando il cliente firmerà.')
456
+                    ->with('open_pdf', false);
457
+            }
458
+
459
+            return $redirect
460
+                ->with('success', 'Richiesta di firma ritiro preparata. La riconsegna non è ancora completata.')
461
+                ->with('error', 'Invio SMS non riuscito: '.($result['error'] ?? 'errore sconosciuto'))
462
+                ->with('open_pdf', false);
463
+        }
464
+
465
+        $doneMsg = $acquisto->is_domicilio ? 'Intervento chiuso.' : 'Riconsegna completata.';
466
+
467
+        if ($modalitaFirma === AcquistoServizio::FIRMA_DEVICE) {
468
+            return $redirect
469
+                ->with('success', $doneMsg.' Firma di ritiro registrata sul dispositivo.')
470
+                ->with('open_pdf', true);
471
+        }
472
+
473
+        return $redirect
474
+            ->with('success', $doneMsg.' Stampa la ricevuta e fai firmare il ritiro al cliente.')
475
+            ->with('open_pdf', true);
476
+    }
477
+
478
+    private function performAccettazione(
479
+        AcquistoServizio $acquisto,
480
+        ?string $noteOperatore,
481
+        Request $request,
482
+        AcquistoServizioRicevutaPdfService $pdfService,
483
+        string $fotoField,
484
+        array $accessori,
485
+        ?string $accessoriAltro,
486
+        string $modalitaFirma,
487
+        SmsService $sms,
488
+        AcquistoServizioAccettazioneService $accettazione
489
+    ): void {
490
+        if ($modalitaFirma === AcquistoServizio::FIRMA_SMS) {
491
+            $accettazione->prepareSmsPending(
492
+                $acquisto,
493
+                (int) Auth::id(),
494
+                $noteOperatore,
495
+                $accessori,
496
+                $accessoriAltro,
497
+                $request,
498
+                $fotoField
499
+            );
500
+
501
+            return;
502
+        }
503
+
504
+        $firmaBinary = null;
505
+        if ($modalitaFirma === AcquistoServizio::FIRMA_DEVICE) {
506
+            $firmaBinary = $this->decodeFirmaPng($request->input('firma'));
507
+        }
508
+
509
+        $accettazione->complete(
510
+            $acquisto,
511
+            (int) Auth::id(),
512
+            $noteOperatore,
513
+            $accessori,
514
+            $accessoriAltro,
515
+            $modalitaFirma,
516
+            $pdfService,
517
+            $request,
518
+            $fotoField,
519
+            $firmaBinary,
520
+            null,
521
+            $request->ip()
522
+        );
523
+    }
524
+
525
+    private function redirectAfterAccettazione(
526
+        AcquistoServizio $acquisto,
527
+        string $modalitaFirma,
528
+        SmsService $sms
529
+    ) {
530
+        $acquisto->refresh();
531
+        $redirect = redirect()->route('admin.acquisto_servizio.show', $acquisto->id);
532
+
533
+        if ($modalitaFirma === AcquistoServizio::FIRMA_SMS) {
534
+            $result = $sms->sendFirmaConsegnaLink($acquisto);
535
+
536
+            if ($result['ok']) {
537
+                return $redirect
538
+                    ->with('success', 'Link di firma inviato via SMS. Il PC sarà preso in carico quando il cliente firmerà.')
539
+                    ->with('open_pdf', false);
540
+            }
541
+
542
+            return $redirect
543
+                ->with('success', 'Richiesta di firma preparata. Il PC non è ancora preso in carico.')
544
+                ->with('error', 'Invio SMS non riuscito: '.($result['error'] ?? 'errore sconosciuto'))
545
+                ->with('open_pdf', false);
546
+        }
547
+
548
+        if ($modalitaFirma === AcquistoServizio::FIRMA_DEVICE) {
549
+            return $redirect
550
+                ->with('success', 'PC preso in carico. Firma registrata sul dispositivo.')
551
+                ->with('open_pdf', true);
552
+        }
553
+
554
+        return $redirect
555
+            ->with('success', 'PC preso in carico. Stampa la ricevuta e fai firmare il cliente.')
556
+            ->with('open_pdf', true);
557
+    }
558
+
559
+    private function decodeFirmaPng(?string $dataUrl): string
560
+    {
561
+        if (! is_string($dataUrl) || ! preg_match('#^data:image/(png|jpeg|jpg);base64,#i', $dataUrl)) {
562
+            throw ValidationException::withMessages([
563
+                'firma' => 'Firma non valida. Disegna la firma nel riquadro.',
564
+            ]);
565
+        }
566
+
567
+        $base64 = preg_replace('#^data:image/\w+;base64,#i', '', $dataUrl);
568
+        $binary = base64_decode($base64, true);
569
+        if ($binary === false || strlen($binary) < 100) {
570
+            throw ValidationException::withMessages([
571
+                'firma' => 'Firma non valida o troppo corta.',
572
+            ]);
573
+        }
574
+
575
+        return $binary;
576
+    }
577
+}

+ 247
- 0
app/Http/Controllers/AcquistoServizioPublicController.php 파일 보기

@@ -0,0 +1,247 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\AcquistoServizio;
6
+use App\Models\Prodotto;
7
+use App\Models\PromoCode;
8
+use App\Services\AcquistoServizioPaymentService;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Support\Facades\Storage;
11
+use Illuminate\Validation\Rule;
12
+
13
+class AcquistoServizioPublicController extends Controller
14
+{
15
+    public function index()
16
+    {
17
+        $servizi = Prodotto::query()
18
+            ->where('attivo', true)
19
+            ->where('prenotabile_online', true)
20
+            ->where('tipo', 'servizio')
21
+            ->orderBy('nome')
22
+            ->get();
23
+
24
+        return view('acquisto_servizio.public.index', [
25
+            'servizi' => $servizi,
26
+            'costoDomicilio' => AcquistoServizio::costoDomicilio(),
27
+        ]);
28
+    }
29
+
30
+    public function create(Request $request, $prodottoId)
31
+    {
32
+        $prodotto = Prodotto::query()
33
+            ->where('attivo', true)
34
+            ->where('prenotabile_online', true)
35
+            ->where('tipo', 'servizio')
36
+            ->findOrFail($prodottoId);
37
+
38
+        return view('acquisto_servizio.public.checkout', [
39
+            'prodotto' => $prodotto,
40
+            'costoDomicilio' => AcquistoServizio::costoDomicilio(),
41
+            'condizioni' => AcquistoServizio::condizioniTesto(),
42
+            'stripeOk' => app(AcquistoServizioPaymentService::class)->stripeConfigured(),
43
+            'paypalOk' => app(AcquistoServizioPaymentService::class)->paypalConfigured(),
44
+            'simulaOk' => app()->environment('local'),
45
+        ]);
46
+    }
47
+
48
+    public function validatePromo(Request $request)
49
+    {
50
+        $data = $request->validate([
51
+            'codice' => 'required|string',
52
+            'prodotto_id' => 'required|exists:prodotto,id',
53
+            'modalita' => ['required', Rule::in(array_keys(AcquistoServizio::MODALITA))],
54
+        ]);
55
+
56
+        $prodotto = Prodotto::findOrFail($data['prodotto_id']);
57
+        $promo = PromoCode::findValidByCodice($data['codice'], (int) $prodotto->id);
58
+        if (! $promo) {
59
+            return response()->json([
60
+                'ok' => false,
61
+                'message' => 'Codice non valido o non applicabile a questo servizio.',
62
+            ], 422);
63
+        }
64
+
65
+        $totals = AcquistoServizio::computeTotals($prodotto, $data['modalita'], $promo);
66
+
67
+        return response()->json([
68
+            'ok' => true,
69
+            'promo' => [
70
+                'id' => $promo->id,
71
+                'codice' => $promo->codice,
72
+                'sconto_percentuale' => (float) $promo->sconto_percentuale,
73
+            ],
74
+            'totals' => $totals,
75
+        ]);
76
+    }
77
+
78
+    public function store(Request $request, AcquistoServizioPaymentService $payments)
79
+    {
80
+        $validated = $request->validate([
81
+            'prodotto_id' => 'required|exists:prodotto,id',
82
+            'nome' => 'required|string|max:255',
83
+            'codice_fiscale' => 'required|string|min:11|max:16',
84
+            'email' => 'required|email|max:255',
85
+            'telefono' => 'required|string|max:50',
86
+            'indirizzo' => 'required|string|max:500',
87
+            'note_cliente' => 'nullable|string|max:5000',
88
+            'modalita' => ['required', Rule::in(array_keys(AcquistoServizio::MODALITA))],
89
+            'promo_codice' => 'nullable|string|max:50',
90
+            'provider' => ['required', Rule::in(['stripe', 'paypal', 'simula'])],
91
+            'condizioni' => 'accepted',
92
+            'privacy_firma' => 'accepted',
93
+            'foto' => 'required|image|max:10240',
94
+        ], [
95
+            'privacy_firma.accepted' => 'Per proseguire è necessario accettare l’informativa privacy.',
96
+        ]);
97
+
98
+        $prodotto = Prodotto::query()
99
+            ->where('attivo', true)
100
+            ->where('prenotabile_online', true)
101
+            ->where('tipo', 'servizio')
102
+            ->findOrFail($validated['prodotto_id']);
103
+
104
+        $promo = null;
105
+        if (! empty($validated['promo_codice'])) {
106
+            $promo = PromoCode::findValidByCodice($validated['promo_codice'], (int) $prodotto->id);
107
+            if (! $promo) {
108
+                return back()->withInput()->withErrors(['promo_codice' => 'Codice promo non valido per questo servizio.']);
109
+            }
110
+        }
111
+
112
+        if ($validated['provider'] === 'stripe' && ! $payments->stripeConfigured()) {
113
+            return back()->withInput()->withErrors(['provider' => 'Stripe non configurato.']);
114
+        }
115
+        if ($validated['provider'] === 'paypal' && ! $payments->paypalConfigured()) {
116
+            return back()->withInput()->withErrors(['provider' => 'PayPal non configurato.']);
117
+        }
118
+        if ($validated['provider'] === 'simula' && ! app()->environment('local')) {
119
+            return back()->withInput()->withErrors(['provider' => 'Pagamento simulato non disponibile.']);
120
+        }
121
+
122
+        $totals = AcquistoServizio::computeTotals($prodotto, $validated['modalita'], $promo);
123
+
124
+        $tempPath = $request->file('foto')->store('acquisto_servizio/'.uniqid('foto_', true), 'temp');
125
+
126
+        $acquisto = new AcquistoServizio();
127
+        $acquisto->prodotto_id = $prodotto->id;
128
+        $acquisto->promo_code_id = $promo?->id;
129
+        $acquisto->nome = $validated['nome'];
130
+        $acquisto->email = $validated['email'];
131
+        $acquisto->telefono = $validated['telefono'];
132
+        $acquisto->codice_fiscale = strtoupper(trim($validated['codice_fiscale']));
133
+        $acquisto->indirizzo = trim($validated['indirizzo']);
134
+        $acquisto->note_cliente = isset($validated['note_cliente']) && trim((string) $validated['note_cliente']) !== ''
135
+            ? trim((string) $validated['note_cliente'])
136
+            : null;
137
+        $acquisto->modalita = $validated['modalita'];
138
+        $acquisto->prezzo_servizio = $totals['prezzo_servizio'];
139
+        $acquisto->costo_domicilio = $totals['costo_domicilio'];
140
+        $acquisto->sconto = $totals['sconto'];
141
+        $acquisto->totale = $totals['totale'];
142
+        $acquisto->provider = $validated['provider'];
143
+        $acquisto->payment_status = AcquistoServizio::PAYMENT_PENDING;
144
+        $acquisto->condizioni_accettate_at = now();
145
+        $acquisto->meta = [
146
+            'temp_foto' => [$tempPath],
147
+            'privacy_firma_accepted_at' => now()->toIso8601String(),
148
+        ];
149
+        $acquisto->save();
150
+
151
+        if ($validated['provider'] === 'simula') {
152
+            $payments->simulatePaid($acquisto);
153
+
154
+            return redirect()->route('shop.success', $acquisto->token);
155
+        }
156
+
157
+        try {
158
+            $url = $validated['provider'] === 'stripe'
159
+                ? $payments->createStripeCheckout($acquisto)
160
+                : $payments->createPaypalOrder($acquisto);
161
+        } catch (\Throwable $e) {
162
+            report($e);
163
+
164
+            return redirect()->route('shop.create', $prodotto->id)
165
+                ->withInput()
166
+                ->withErrors(['provider' => 'Errore avvio pagamento: '.$e->getMessage()]);
167
+        }
168
+
169
+        return redirect()->away($url);
170
+    }
171
+
172
+    public function success(string $token, Request $request, AcquistoServizioPaymentService $payments)
173
+    {
174
+        $acquisto = AcquistoServizio::where('token', $token)->with(['prodotto', 'segnalazione'])->firstOrFail();
175
+
176
+        if ($request->provider === 'stripe' && $request->filled('session_id') && $acquisto->payment_status !== AcquistoServizio::PAYMENT_PAID) {
177
+            try {
178
+                \Stripe\Stripe::setApiKey(config('services.stripe.secret'));
179
+                $session = \Stripe\Checkout\Session::retrieve($request->session_id);
180
+                if (($session->payment_status ?? '') === 'paid') {
181
+                    $acquisto->mergeMeta(['stripe_session_id' => $session->id, 'payment_ref' => $session->payment_intent]);
182
+                    $acquisto->save();
183
+                    app(\App\Services\AcquistoServizioFulfillmentService::class)->fulfillIfNeeded($acquisto);
184
+                    $acquisto->refresh();
185
+                }
186
+            } catch (\Throwable $e) {
187
+                report($e);
188
+            }
189
+        }
190
+
191
+        return view('acquisto_servizio.public.success', compact('acquisto'));
192
+    }
193
+
194
+    public function cancel(string $token)
195
+    {
196
+        $acquisto = AcquistoServizio::where('token', $token)->with('prodotto')->firstOrFail();
197
+
198
+        return view('acquisto_servizio.public.cancel', compact('acquisto'));
199
+    }
200
+
201
+    public function tracking(string $token)
202
+    {
203
+        $acquisto = AcquistoServizio::where('token', $token)->with('prodotto')->firstOrFail();
204
+
205
+        return view('acquisto_servizio.public.tracking', [
206
+            'acquisto' => $acquisto,
207
+            'stato' => $acquisto->cliente_stato_corrente,
208
+            'steps' => $acquisto->clienteTrackingSteps(),
209
+            'negozioIndirizzo' => AcquistoServizio::negozioIndirizzo(),
210
+            'negozioOrari' => AcquistoServizio::negozioOrari(),
211
+        ]);
212
+    }
213
+
214
+    public function paypalReturn(string $token, Request $request, AcquistoServizioPaymentService $payments)
215
+    {
216
+        $acquisto = AcquistoServizio::where('token', $token)->firstOrFail();
217
+        $orderId = $request->input('token') ?: $acquisto->getMetaValue('paypal_order_id');
218
+        if (! $orderId) {
219
+            return redirect()->route('shop.cancel', $acquisto->token);
220
+        }
221
+
222
+        try {
223
+            $payments->capturePaypalOrder($acquisto, $orderId);
224
+        } catch (\Throwable $e) {
225
+            report($e);
226
+
227
+            return redirect()->route('shop.cancel', $acquisto->token)
228
+                ->with('error', $e->getMessage());
229
+        }
230
+
231
+        return redirect()->route('shop.success', $acquisto->token);
232
+    }
233
+
234
+    public function stripeWebhook(Request $request, AcquistoServizioPaymentService $payments)
235
+    {
236
+        $payments->handleStripeWebhook($request->getContent(), $request->header('Stripe-Signature'));
237
+
238
+        return response()->json(['ok' => true]);
239
+    }
240
+
241
+    public function paypalWebhook(Request $request, AcquistoServizioPaymentService $payments)
242
+    {
243
+        $payments->handlePaypalWebhook($request->all());
244
+
245
+        return response()->json(['ok' => true]);
246
+    }
247
+}

+ 101
- 0
app/Http/Controllers/AllegatoAnnotazioneController.php 파일 보기

@@ -0,0 +1,101 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\AllegatoAnnotazione;
7
+use App\Models\Annotazione;
8
+use Illuminate\Support\Facades\Storage;
9
+use Illuminate\Support\Facades\Validator;
10
+use Illuminate\Support\Facades\File;
11
+use Illuminate\Support\Facades\Auth;
12
+use Illuminate\Routing\Controllers\Middleware;
13
+use Illuminate\Routing\Controllers\HasMiddleware;
14
+
15
+class AllegatoAnnotazioneController extends Controller implements HasMiddleware
16
+{
17
+  public static $permission_group = "Allegati Annotazioni";
18
+  public static $permissions = [
19
+    'create-allegato-annotazione' => 'Crea',
20
+    'edit-allegato-annotazione' => 'Modifica',
21
+    'delete-allegato-annotazione' => 'Elimina',
22
+    'view-allegato-annotazione' => 'Vedi',
23
+  ];
24
+
25
+  public static function middleware(): array
26
+  {
27
+    return [
28
+      // new Middleware('permission:create-allegato-annotazione', only: ['store']),
29
+      // new Middleware('permission:edit-allegato-annotazione', only: ['edit']),
30
+      // new Middleware('permission:delete-allegato-annotazione', only: ['destroy']),
31
+      // new Middleware('permission:view-allegato-annotazione', only: ['show']),
32
+    ];
33
+  }
34
+
35
+  public function show(Request $request)
36
+  {
37
+    $allegatoAnnotazione = AllegatoAnnotazione::find($request->allegato_annotazione_id);
38
+    $prevId = null;
39
+    $nextId = null;
40
+
41
+    if ($allegatoAnnotazione) {
42
+      $annotazione = Annotazione::find($allegatoAnnotazione->annotazione_id);
43
+      if ($annotazione) {
44
+        $annotazioniIds = Annotazione::where('segnalazione_id', $annotazione->segnalazione_id)->pluck('id');
45
+        $allegatiIds = AllegatoAnnotazione::whereIn('annotazione_id', $annotazioniIds)
46
+          ->orderBy('created_at')
47
+          ->pluck('id')
48
+          ->values();
49
+        $currentIndex = $allegatiIds->search($allegatoAnnotazione->id);
50
+        if ($currentIndex !== false) {
51
+          $prevId = $allegatiIds->get($currentIndex - 1);
52
+          $nextId = $allegatiIds->get($currentIndex + 1);
53
+        }
54
+      }
55
+    }
56
+
57
+    return view('allegato_annotazione.show', [
58
+      'allegatoAnnotazione' => $allegatoAnnotazione,
59
+      'prevId' => $prevId,
60
+      'nextId' => $nextId
61
+    ]);
62
+  }
63
+
64
+  public function download(Request $request, $allegato_annotazione_id){
65
+    $allegatoAnnotazione = AllegatoAnnotazione::find($allegato_annotazione_id);
66
+    if($allegatoAnnotazione != null){
67
+      if(Storage::disk('allegatiAnnotazioni')->exists($allegatoAnnotazione->path_file)){
68
+        $storagePath  = Storage::disk('allegatiAnnotazioni')->path($allegatoAnnotazione->path_file);
69
+        if($request->has('type') && $request->type == 'file'){
70
+          return response()->file($storagePath);
71
+        }else{
72
+          return response()->download($storagePath);
73
+        }
74
+
75
+      }else{
76
+        Session::flash('flash_message', 'Il file non esiste!');
77
+        return redirect()->route('dashboard');
78
+      }
79
+    }else{
80
+      Session::flash('flash_message', "Il documento non esiste");
81
+      return redirect()->route('dashboard');
82
+    }
83
+  }
84
+
85
+  public function delete(Request $request)
86
+  {
87
+    $allegatoAnnotazione = AllegatoAnnotazione::find($request->allegato_annotazione_id);
88
+    if (!$allegatoAnnotazione) {
89
+      return response()->json(['success' => false, 'message' => 'Allegato non trovato'], 404);
90
+    }
91
+
92
+    if ($allegatoAnnotazione->path_file && Storage::disk('allegatiAnnotazioni')->exists($allegatoAnnotazione->path_file)) {
93
+      Storage::disk('allegatiAnnotazioni')->delete($allegatoAnnotazione->path_file);
94
+    }
95
+
96
+    $allegatoAnnotazione->delete();
97
+
98
+    return response()->json(['success' => true]);
99
+  }
100
+
101
+}

+ 112
- 0
app/Http/Controllers/AnnotazioniController.php 파일 보기

@@ -0,0 +1,112 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Annotazione;
7
+use App\Models\AllegatoAnnotazione;
8
+use App\Models\Segnalazione;
9
+use Illuminate\Support\Facades\Auth;
10
+use Illuminate\Support\Facades\Validator;
11
+use Illuminate\Support\Facades\File;
12
+use Illuminate\Routing\Controllers\Middleware;
13
+use Illuminate\Routing\Controllers\HasMiddleware;
14
+
15
+class AnnotazioniController extends Controller implements HasMiddleware
16
+{
17
+    public static $permission_group = "Annotazioni";
18
+    public static $permissions = [
19
+        'create-annotazione' => 'Crea',
20
+        'edit-annotazione' => 'Modifica',
21
+        'delete-annotazione' => 'Elimina',
22
+        'view-annotazione' => 'Vedi',
23
+    ];
24
+
25
+    public static function middleware(): array
26
+    {
27
+        return [
28
+            new Middleware('permission:create-annotazione', only: ['create']),
29
+            new Middleware('permission:edit-annotazione', only: ['edit']),
30
+            new Middleware('permission:delete-annotazione', only: ['destroy']),
31
+            new Middleware('permission:view-annotazione', only: ['show']),
32
+        ];
33
+    }
34
+
35
+    public function create(Request $request)
36
+    {
37
+        // Validazione base
38
+        $validator = Validator::make($request->all(), [
39
+            'annotazione' => 'required|string',
40
+            'segnalazione_id' => 'required|exists:segnalazioni,id',
41
+            'file' => 'nullable|file|max:10240', // max 10MB
42
+            'nome_allegato' => 'nullable|string|max:255',
43
+            'descrizione_allegato' => 'nullable|string',
44
+        ]);
45
+
46
+        if ($validator->fails()) {
47
+            return redirect()->back()
48
+                ->withErrors($validator)
49
+                ->withInput();
50
+        }
51
+
52
+        // Crea l'annotazione
53
+        $annotazione = new Annotazione();
54
+        $annotazione->annotazione = $request->annotazione;
55
+        $annotazione->segnalazione_id = $request->segnalazione_id;
56
+        $annotazione->user_id = Auth::user()->id;
57
+        $annotazione->save();
58
+
59
+        // Se è stato caricato un file, crea l'allegato
60
+        if ($request->hasFile('file')) {
61
+            // Assicurati che la directory esista
62
+            $directory = storage_path('app/public/allegatiAnnotazioni/' . $annotazione->id);
63
+            if (!File::exists($directory)) {
64
+                File::makeDirectory($directory, 0755, true);
65
+            }
66
+            
67
+            // Carica il file
68
+            $file = $request->file('file');
69
+            $filename = time() . '_' . $file->getClientOriginalName();
70
+            $path = $file->storeAs($annotazione->id, $filename, 'allegatiAnnotazioni');
71
+
72
+            // Crea l'allegato
73
+            $allegato = new AllegatoAnnotazione();
74
+            $allegato->annotazione_id = $annotazione->id;
75
+            $allegato->nome = $request->nome_allegato ?? $file->getClientOriginalName();
76
+            $allegato->descrizione = $request->descrizione_allegato;
77
+            $allegato->path_file = $path;
78
+            $allegato->user_id = Auth::user()->id;
79
+            $allegato->save();
80
+        }
81
+
82
+        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione creata con successo');
83
+    }
84
+
85
+    public function show(Request $request)
86
+    {
87
+        $annotazione = Annotazione::find($request->id);
88
+        return view('annotazioni.show', ['annotazione' => $annotazione]);
89
+    }
90
+
91
+    public function edit(Request $request)
92
+    {
93
+        $annotazione = Annotazione::find($request->id);
94
+        return view('annotazioni.edit', ['annotazione' => $annotazione]);
95
+    }
96
+
97
+    public function update(Request $request)
98
+    {
99
+        $annotazione = Annotazione::find($request->id);
100
+        $annotazione->annotazione = $request->annotazione;
101
+        $annotazione->segnalazione_id = $request->segnalazione_id;
102
+        $annotazione->save();
103
+        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione aggiornata con successo');
104
+    }
105
+
106
+    public function destroy(Request $request)
107
+    {
108
+        $annotazione = Annotazione::find($request->id);
109
+        $annotazione->delete();
110
+        return redirect()->route('segnalazione.show', $request->segnalazione_id)->with('success', 'Annotazione eliminata con successo');
111
+    }
112
+}

+ 140
- 0
app/Http/Controllers/AziendaController.php 파일 보기

@@ -0,0 +1,140 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\AziendaDataTable;
6
+use App\Models\Azienda;
7
+use App\Models\Role;
8
+use Illuminate\Http\Request;
9
+use Illuminate\Routing\Controllers\HasMiddleware;
10
+use Illuminate\Routing\Controllers\Middleware;
11
+use Illuminate\Support\Facades\Auth;
12
+
13
+class AziendaController extends Controller implements HasMiddleware
14
+{
15
+    public static $permission_group = 'Aziende';
16
+
17
+    public static $permissions = [
18
+        'view-azienda' => 'Vedi',
19
+        'create-azienda' => 'Crea',
20
+        'edit-azienda' => 'Modifica',
21
+        'remove-azienda' => 'Rimuovi',
22
+    ];
23
+
24
+    public static function middleware(): array
25
+    {
26
+        return [
27
+            new Middleware('permission:view-azienda', only: ['admin_index', 'admin_show']),
28
+            new Middleware('permission:create-azienda|edit-azienda', only: ['admin_store']),
29
+            new Middleware('permission:remove-azienda', only: ['admin_delete']),
30
+        ];
31
+    }
32
+
33
+    public function admin_index(AziendaDataTable $dataTable)
34
+    {
35
+        return $dataTable->render('azienda.index');
36
+    }
37
+
38
+    public function admin_show($id)
39
+    {
40
+        $azienda = Azienda::findOrFail($id);
41
+
42
+        return response()->json($this->payload($azienda));
43
+    }
44
+
45
+    public function admin_store(Request $request)
46
+    {
47
+        $isUpdate = $request->filled('id');
48
+        if ($isUpdate && ! Auth::user()->can('edit-azienda')) {
49
+            abort(403);
50
+        }
51
+        if (! $isUpdate && ! Auth::user()->can('create-azienda')) {
52
+            abort(403);
53
+        }
54
+
55
+        $validated = $request->validate([
56
+            'id' => [
57
+                'nullable',
58
+                'exists:users,id',
59
+                function ($attribute, $value, $fail) {
60
+                    if ($value && ! Azienda::where('id', $value)->exists()) {
61
+                        $fail('Azienda non valida.');
62
+                    }
63
+                },
64
+            ],
65
+            'ragione_sociale' => 'required|string|max:255',
66
+            'nome' => 'nullable|string|max:255',
67
+            'piva' => 'nullable|string|max:50',
68
+            'cod_fiscale' => 'nullable|string|max:16',
69
+            'email' => 'nullable|email|max:255',
70
+            'telefono' => 'nullable|string|max:255',
71
+            'indirizzo' => 'nullable|string|max:500',
72
+            'note' => 'nullable|string',
73
+        ]);
74
+
75
+        $azienda = $isUpdate
76
+            ? Azienda::findOrFail($validated['id'])
77
+            : new Azienda();
78
+
79
+        $ragione = trim($validated['ragione_sociale']);
80
+        $nome = isset($validated['nome']) && trim((string) $validated['nome']) !== ''
81
+            ? trim($validated['nome'])
82
+            : $ragione;
83
+
84
+        $azienda->is_azienda = true;
85
+        $azienda->is_gruppo = false;
86
+        $azienda->ragione_sociale = $ragione;
87
+        $azienda->nome = $nome;
88
+        $azienda->cognome = null;
89
+        $azienda->piva = $validated['piva'] ?? null;
90
+        $azienda->cod_fiscale = isset($validated['cod_fiscale']) && trim((string) $validated['cod_fiscale']) !== ''
91
+            ? strtoupper(trim($validated['cod_fiscale']))
92
+            : null;
93
+        $azienda->email = $validated['email'] ?? null;
94
+        $azienda->telefono = $validated['telefono'] ?? null;
95
+        $azienda->indirizzo = $validated['indirizzo'] ?? null;
96
+        $azienda->note = $validated['note'] ?? null;
97
+        $azienda->save();
98
+
99
+        // if (! $isUpdate) {
100
+        //     $role = Role::where('name', 'user')->first();
101
+        //     if ($role) {
102
+        //         $azienda->syncRoles([$role->name]);
103
+        //     }
104
+        // }
105
+
106
+        return response()->json([
107
+            'success' => true,
108
+            'message' => $isUpdate ? 'Azienda aggiornata.' : 'Azienda creata.',
109
+            'azienda' => $this->payload($azienda),
110
+        ]);
111
+    }
112
+
113
+    public function admin_delete($id)
114
+    {
115
+        $azienda = Azienda::findOrFail($id);
116
+        $azienda->delete();
117
+
118
+        return response()->json([
119
+            'success' => true,
120
+            'message' => 'Azienda eliminata.',
121
+        ]);
122
+    }
123
+
124
+    private function payload(Azienda $azienda): array
125
+    {
126
+        return [
127
+            'id' => $azienda->id,
128
+            'ragione_sociale' => $azienda->ragione_sociale,
129
+            'nome' => $azienda->nome,
130
+            'piva' => $azienda->piva,
131
+            'cod_fiscale' => $azienda->cod_fiscale,
132
+            'email' => $azienda->email,
133
+            'telefono' => $azienda->telefono,
134
+            'indirizzo' => $azienda->indirizzo,
135
+            'note' => $azienda->note,
136
+            'text' => $azienda->full_name,
137
+            'full_name' => $azienda->full_name,
138
+        ];
139
+    }
140
+}

+ 70
- 0
app/Http/Controllers/ComuneController.php 파일 보기

@@ -0,0 +1,70 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Comune;
7
+use Auth;
8
+
9
+class ComuneController extends Controller
10
+{
11
+
12
+  public function list(Request $request){
13
+    $input = $request->all();
14
+    $list = array();
15
+
16
+    if($request->has('initialValue') && $input['initialValue'] == "true"){
17
+      $val = trim(str_replace('"', '', $input['value']));
18
+      $entity = Comune::find($val);
19
+      return response()->json(array('id' => $entity->id, 'text' => $entity->nome.' ('.$entity->provincia->sigla.')' ));
20
+
21
+    }else{
22
+      $page = $input['page'];
23
+      $resultCount = 25;
24
+      $offset = ($page - 1) * $resultCount;
25
+
26
+      $entitys = Comune::where('nome', 'LIKE', '%'.$input['term'].'%')
27
+      ->orderBy('id', 'ASC');
28
+
29
+      if($request->has('nazione_id') && $request->nazione_id != null && $request->nazione_id != ''){
30
+        $entitys->where('nazione_id', $request->nazione_id);
31
+      }
32
+
33
+      foreach ($entitys->skip($offset)->take($resultCount)->get() as $e) {
34
+        array_push($list, array('id' => $e->id, 'text' => $e->nome.' ('.$e->provincia->sigla.')' ));
35
+      }
36
+
37
+      $count = $entitys->count();
38
+      $endCount = $offset + $resultCount;
39
+      $morePages = $count > $endCount;
40
+
41
+      $results = array(
42
+        "results" => $list,
43
+        "pagination" => array(
44
+          "more" => $morePages
45
+        )
46
+      );
47
+
48
+    }
49
+    return response()->json($results);
50
+  }
51
+
52
+  public function api_listComuni(Request $request){
53
+    $arrayComuni = [];
54
+    $resultCount = 25;
55
+    $offset = ($request->page) * $resultCount;
56
+    $comuni = Comune::orderBy('nome')->skip($offset)->take($resultCount);
57
+    if($request->has('search') && $request->search != null && $request->search != ''){
58
+      $nazioni->where('nome', 'LIKE', '%'.$request->search.'%');
59
+    }
60
+
61
+    foreach($comuni->get() as $comune){
62
+      $arrayComuni[] = [
63
+        'id' => $comune->id,
64
+        'nome' => $comune->nome.' ('.$comune->provincia->sigla.')'
65
+      ];
66
+    }
67
+    return response()->json($arrayComuni);
68
+  }
69
+
70
+}

+ 87
- 0
app/Http/Controllers/ConfigController.php 파일 보기

@@ -0,0 +1,87 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Routing\Controllers\Middleware;
6
+use Illuminate\Routing\Controllers\HasMiddleware;
7
+use Illuminate\Http\Request;
8
+use App\Models\Config;
9
+use App\Models\Evento;
10
+use Illuminate\Support\Facades\Auth;
11
+use Notification;
12
+use Session;
13
+use PDF;
14
+use Illuminate\Support\Str;
15
+use Carbon\Carbon;
16
+use Illuminate\Support\Facades\Storage;
17
+use Intervention\Image\ImageManager;
18
+use Intervention\Image\Drivers\Imagick\Driver;
19
+use Illuminate\Support\Facades\Log;
20
+
21
+class ConfigController extends Controller implements HasMiddleware
22
+{
23
+  public static $permission_group = "Config";
24
+  public static $permissions = [
25
+    'view-config' => 'Vedi',
26
+    'edit-config' => 'Modifica',
27
+  ];
28
+
29
+  public static function middleware(): array
30
+  {
31
+    return [
32
+      new Middleware('permission:view-config', only: ['index']),
33
+      new Middleware('permission:edit-config', only: ['store']),
34
+    ];
35
+  }
36
+
37
+  public function index()
38
+  {
39
+    return view('config.index', ['user' => Auth::user()]);
40
+  }
41
+
42
+  public function createToken(Request $request){
43
+    $token = $request->user()->createToken("tokenEventi");
44
+
45
+    return response()->json(['token' => $token->plainTextToken]);
46
+  }
47
+
48
+  public function traduci(Request $request){
49
+    try{
50
+      $authKey = config('app.deepl_key');
51
+      $deeplClient = new \DeepL\DeepLClient($authKey);
52
+      $result = $deeplClient->translateText($request->text, 'it', 'en-GB');
53
+      return response()->json(['text' => $result->text]);
54
+    }catch(\Exception $e){
55
+      Log::info($e);
56
+      return response()->json(['text' => '']);
57
+    }catch(\Error $e){
58
+      Log::info($e);
59
+      return response()->json(['text' => '']);
60
+    }
61
+  }
62
+
63
+  public function showCropImage(Request $request){
64
+    return view('config.cropImage', ['filename' => $request->filename]);
65
+  }
66
+
67
+  public function cropImage(Request $request){
68
+    // $evento = Evento::find($request->evento_id);
69
+    $fileName = explode(".", $request->filename);
70
+    $extension = array_pop($fileName);
71
+    $filename = implode("_", $fileName);
72
+
73
+    // create new image instance
74
+    $manager = new ImageManager(new Driver());
75
+    $image = $manager->read(Storage::disk('temp')->path($request->filename));
76
+    $image->crop($request->cropBoxData['width'], $request->cropBoxData['height'], $request->cropBoxData['x'], $request->cropBoxData['y']);
77
+    $newFileName = $filename.'_'.time().'_cropped.'.$extension;
78
+    Storage::disk('temp')->put($filename.'_'.time().'_cropped.'.$extension, $image->encodeByExtension($extension, quality: 100));
79
+    // $evento->path_img = $newFileName;
80
+    // $evento->save();
81
+
82
+    return response()->json(['url' => Storage::disk('temp')->url($newFileName), 'filename' => $newFileName]);
83
+}
84
+
85
+
86
+
87
+}

+ 70
- 0
app/Http/Controllers/ConfigNotificheController.php 파일 보기

@@ -0,0 +1,70 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\ConfigNotifiche;
7
+use App\Models\User;
8
+use Illuminate\Routing\Controllers\Middleware;
9
+
10
+class ConfigNotificheController extends Controller
11
+{
12
+    public static $permission_group = "Config";
13
+    public static $permissions = [
14
+        'view-config-notifiche' => 'Gestisci notifiche',
15
+    ];
16
+    public static function middleware(): array
17
+    {
18
+        return [
19
+            new Middleware('permission:view-config-notifiche', only: ['index', 'update']),
20
+        ];
21
+    }
22
+
23
+    public function index(Request $request)
24
+    {
25
+        $configList = ConfigNotifiche::getConfigList();
26
+        $existing = ConfigNotifiche::whereIn('event', array_keys($configList))->get()->keyBy('event');
27
+        $configNotifiche = [];
28
+
29
+        foreach ($configList as $event => $meta) {
30
+            $configNotifiche[$event] = $existing[$event] ?? new ConfigNotifiche([
31
+                'event' => $event,
32
+                'is_attivo' => true,
33
+                'users' => []
34
+            ]);
35
+        }
36
+
37
+        $users = User::where('is_azienda', false)->orderBy('cognome')->orderBy('nome')->get();
38
+
39
+        return view('config_notifiche.index', [
40
+            'configList' => $configList,
41
+            'configNotifiche' => $configNotifiche,
42
+            'users' => $users
43
+        ]);
44
+    }
45
+
46
+    public function update(Request $request)
47
+    {
48
+        $configList = ConfigNotifiche::getConfigList();
49
+        $input = $request->input('config', []);
50
+        $validUserIds = User::pluck('id')->all();
51
+        $validUserMap = array_flip($validUserIds);
52
+
53
+        foreach ($configList as $event => $meta) {
54
+            $configInput = $input[$event] ?? [];
55
+            $isAttivo = isset($configInput['is_attivo']) && $configInput['is_attivo'] ? true : false;
56
+            $users = $meta['has_users'] ? ($configInput['users'] ?? []) : [];
57
+
58
+            $users = array_values(array_unique(array_filter(array_map('intval', (array) $users), function ($id) use ($validUserMap) {
59
+                return isset($validUserMap[$id]);
60
+            })));
61
+
62
+            $config = ConfigNotifiche::firstOrNew(['event' => $event]);
63
+            $config->is_attivo = $isAttivo;
64
+            $config->users = $meta['has_users'] ? $users : [];
65
+            $config->save();
66
+        }
67
+
68
+        return redirect()->route('admin.config_notifiche.index')->with('success', 'Configurazioni notifiche aggiornate');
69
+    }
70
+}

+ 31
- 0
app/Http/Controllers/ConfiguraSegnalazioneController.php 파일 보기

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\DataTables\ConfiguraSegnalazioneDataTable;
7
+use App\DataTables\ConfiguraSegnalazioneDataTableEditor;
8
+use Illuminate\Support\Facades\Auth;
9
+use Illuminate\Routing\Controllers\Middleware;
10
+use Illuminate\Routing\Controllers\HasMiddleware;
11
+
12
+class ConfiguraSegnalazioneController extends Controller implements HasMiddleware
13
+{
14
+    public static function middleware(): array
15
+    {
16
+        return [
17
+            new Middleware('permission:view-config'),
18
+        ];
19
+    }
20
+
21
+    public function admin_index(ConfiguraSegnalazioneDataTable $dataTable, Request $request)
22
+    {
23
+        return $dataTable->render('configura_segnalazione.index', ['user' => Auth::user()]);
24
+    }
25
+    public function admin_store(ConfiguraSegnalazioneDataTableEditor $editor)
26
+    {
27
+      $request = request();
28
+      $input = $request->all();
29
+      return $editor->process(request());
30
+    }
31
+}

+ 339
- 0
app/Http/Controllers/ContrattoServizioController.php 파일 보기

@@ -0,0 +1,339 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\ContrattoServizioDataTable;
6
+use App\DataTables\ContrattoServizioDataTableEditor;
7
+use App\DataTables\ContrattoServizioIndexDataTable;
8
+use App\Models\ContrattoServizio;
9
+use App\Models\ContrattoServizioRiga;
10
+use App\Models\ContrattoServizioTodo;
11
+use App\Models\ContrattoTemplate;
12
+use App\Models\Prodotto;
13
+use App\Models\User;
14
+use App\Services\ContrattoServizioPdfService;
15
+use Carbon\Carbon;
16
+use Illuminate\Http\Request;
17
+use Illuminate\Routing\Controllers\HasMiddleware;
18
+use Illuminate\Routing\Controllers\Middleware;
19
+use Illuminate\Support\Facades\DB;
20
+
21
+class ContrattoServizioController extends Controller implements HasMiddleware
22
+{
23
+  public static $permission_group = "Contratti servizi";
24
+  public static $permissions = [
25
+    'view-contratto-servizio' => 'Vedi',
26
+    'create-contratto-servizio' => 'Crea',
27
+    'edit-contratto-servizio' => 'Modifica',
28
+    'remove-contratto-servizio' => 'Rimuovi',
29
+  ];
30
+
31
+  public static function middleware(): array
32
+  {
33
+    return [
34
+      new Middleware('permission:view-contratto-servizio', only: [
35
+        'admin_index', 'admin_list', 'admin_show', 'admin_pdf',
36
+      ]),
37
+      new Middleware('permission:create-contratto-servizio', only: ['admin_create', 'admin_crea']),
38
+      new Middleware('permission:edit-contratto-servizio', only: ['admin_edit', 'admin_update', 'admin_email']),
39
+      new Middleware('permission:remove-contratto-servizio', only: ['admin_destroy']),
40
+      new Middleware('permission:create-contratto-servizio|edit-contratto-servizio|remove-contratto-servizio', only: ['admin_store']),
41
+    ];
42
+  }
43
+
44
+  public function admin_index(Request $request, ContrattoServizioDataTable $embedDataTable, ContrattoServizioIndexDataTable $indexDataTable)
45
+  {
46
+    if ($request->filled('user_id')) {
47
+      $user = User::findOrFail($request->user_id);
48
+
49
+      return $embedDataTable->with(['user' => $user])->render('contratto_servizio.embed', compact('user'));
50
+    }
51
+
52
+    return $indexDataTable->render('contratto_servizio.index');
53
+  }
54
+
55
+  public function admin_create(Request $request)
56
+  {
57
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
58
+    $templates = ContrattoTemplate::where('attivo', true)->orderBy('nome')->get();
59
+
60
+    return view('contratto_servizio.create', [
61
+      'clienti' => $clienti,
62
+      'templates' => $templates,
63
+      'user_id' => $request->input('user_id'),
64
+    ]);
65
+  }
66
+
67
+  public function admin_crea(Request $request)
68
+  {
69
+    $validated = $request->validate([
70
+      'user_id' => 'required|exists:users,id',
71
+      'origine' => 'required|in:vuoto,template',
72
+      'contratto_template_id' => 'nullable|required_if:origine,template|exists:contratto_template,id',
73
+      'nome' => 'nullable|string|max:255',
74
+      'costo' => 'nullable|numeric|min:0',
75
+      'periodo' => 'nullable|string|max:50',
76
+      'data_inizio' => 'nullable|date',
77
+      'data_fine' => 'nullable|date|after_or_equal:data_inizio',
78
+      'note' => 'nullable|string',
79
+      'attivo' => 'nullable|boolean',
80
+    ]);
81
+
82
+    if (($validated['origine'] ?? '') === 'vuoto') {
83
+      $validated['contratto_template_id'] = null;
84
+    }
85
+
86
+    $contratto = DB::transaction(function () use ($validated) {
87
+      $contratto = new ContrattoServizio();
88
+      $this->fillHeader($contratto, $validated);
89
+      $contratto->save();
90
+
91
+      if (! empty($contratto->contratto_template_id)) {
92
+        $contratto->importaDaTemplate();
93
+      }
94
+
95
+      return $contratto;
96
+    });
97
+
98
+    return redirect()->route('admin.contratto_servizio.edit', $contratto->id)
99
+      ->with('success', 'Contratto creato. Puoi modificare servizi e todo.');
100
+  }
101
+
102
+  public function admin_show($id)
103
+  {
104
+    $contratto = ContrattoServizio::with([
105
+      'user',
106
+      'contrattoTemplate',
107
+      'righe.prodotto',
108
+      'todos',
109
+    ])->findOrFail($id);
110
+
111
+    return view('contratto_servizio.show', compact('contratto'));
112
+  }
113
+
114
+  public function admin_edit($id)
115
+  {
116
+    $contratto = ContrattoServizio::with(['user', 'contrattoTemplate', 'todos', 'righe.prodotto'])
117
+      ->findOrFail($id);
118
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
119
+    $templates = ContrattoTemplate::where('attivo', true)->orderBy('nome')->get();
120
+    $prodotti = Prodotto::where('attivo', true)->orderBy('nome')->get();
121
+
122
+    return view('contratto_servizio.form', compact('contratto', 'clienti', 'templates', 'prodotti'));
123
+  }
124
+
125
+  public function admin_update(Request $request, $id)
126
+  {
127
+    $contratto = ContrattoServizio::findOrFail($id);
128
+    $validated = $request->validate([
129
+      'user_id' => 'required|exists:users,id',
130
+      'contratto_template_id' => 'nullable|exists:contratto_template,id',
131
+      'nome' => 'nullable|string|max:255',
132
+      'costo' => 'nullable|numeric|min:0',
133
+      'periodo' => 'nullable|string|max:50',
134
+      'data_inizio' => 'nullable|date',
135
+      'data_fine' => 'nullable|date|after_or_equal:data_inizio',
136
+      'note' => 'nullable|string',
137
+      'attivo' => 'nullable|boolean',
138
+      'todos' => 'nullable|array',
139
+      'todos.*.id' => 'nullable|integer',
140
+      'todos.*.titolo' => 'nullable|string|max:255',
141
+      'todos.*.descrizione' => 'nullable|string',
142
+      'todos.*.tipo_schedulazione' => 'nullable|string|max:50',
143
+      'todos.*.cron' => 'nullable|string|max:100',
144
+      'todos.*.mese' => 'nullable|integer|min:1|max:12',
145
+      'todos.*.giorno' => 'nullable|integer|min:1|max:31',
146
+      'todos.*.offset_giorni' => 'nullable|integer|min:0',
147
+      'todos.*.data_prossima' => 'nullable|date',
148
+      'todos.*.attivo' => 'nullable',
149
+      'righe' => 'nullable|array',
150
+      'righe.*.id' => 'nullable|integer',
151
+      'righe.*.prodotto_id' => 'nullable|exists:prodotto,id',
152
+      'righe.*.quantita' => 'nullable|numeric|min:0',
153
+      'righe.*.unita_misura' => 'nullable|string|in:'.implode(',', array_keys(Prodotto::UNITA_MISURA)),
154
+      'righe.*.prezzo_unitario' => 'nullable|numeric|min:0',
155
+      'righe.*.periodo' => 'nullable|string|max:50',
156
+      'righe.*.note' => 'nullable|string',
157
+    ]);
158
+
159
+    DB::transaction(function () use ($contratto, $validated, $request) {
160
+      $this->fillHeader($contratto, $validated);
161
+      $contratto->save();
162
+      $this->syncTodos($contratto, $request->input('todos', []));
163
+      $this->syncRighe($contratto, $request->input('righe', []));
164
+    });
165
+
166
+    return redirect()->route('admin.contratto_servizio.show', $contratto->id)
167
+      ->with('success', 'Contratto aggiornato.');
168
+  }
169
+
170
+  public function admin_destroy(Request $request, $id)
171
+  {
172
+    $contratto = ContrattoServizio::findOrFail($id);
173
+    $contratto->contrattoServizioTodos()->delete();
174
+    $contratto->contrattoServizioRigas()->delete();
175
+    $contratto->delete();
176
+
177
+    if ($request->expectsJson() || $request->ajax()) {
178
+      return response()->json([
179
+        'success' => true,
180
+        'message' => 'Contratto eliminato.',
181
+      ]);
182
+    }
183
+
184
+    return redirect()->route('admin.contratto_servizio.index')
185
+      ->with('success', 'Contratto eliminato.');
186
+  }
187
+
188
+  public function admin_pdf($id, ContrattoServizioPdfService $pdfService)
189
+  {
190
+    $contratto = ContrattoServizio::findOrFail($id);
191
+    $filename = $pdfService->filename($contratto);
192
+
193
+    return response()->stream(function () use ($pdfService, $contratto) {
194
+      echo $pdfService->generateBinary($contratto);
195
+    }, 200, [
196
+      'Content-Type' => 'application/pdf',
197
+      'Content-Disposition' => 'inline; filename="'.$filename.'"',
198
+    ]);
199
+  }
200
+
201
+  public function admin_email($id)
202
+  {
203
+    ContrattoServizio::findOrFail($id);
204
+
205
+    return redirect()->back()->with('warning', 'TODO: invio email del contratto non ancora implementato.');
206
+  }
207
+
208
+  public function admin_store(ContrattoServizioDataTableEditor $editor)
209
+  {
210
+    return $editor->process(request());
211
+  }
212
+
213
+  public function admin_list(Request $request)
214
+  {
215
+    $userId = $request->input('user_id');
216
+    $soloAttivi = $request->boolean('collegabili') || $request->boolean('attivi');
217
+
218
+    $query = ContrattoServizio::query()
219
+      ->with('contrattoTemplate')
220
+      ->when($userId, fn ($q) => $q->where('user_id', $userId))
221
+      ->when($soloAttivi, fn ($q) => $q->attivi())
222
+      ->orderByDesc('data_inizio')
223
+      ->get();
224
+
225
+    return response()->json([
226
+      'results' => $query->map(fn (ContrattoServizio $c) => [
227
+        'id' => $c->id,
228
+        'text' => $c->label.($c->isAttivoOggi() ? '' : ' (non attivo)'),
229
+      ])->values(),
230
+    ]);
231
+  }
232
+
233
+  private function fillHeader(ContrattoServizio $contratto, array $validated): void
234
+  {
235
+    $contratto->user_id = $validated['user_id'];
236
+    $contratto->contratto_template_id = $validated['contratto_template_id'] ?? null;
237
+    $contratto->nome = $validated['nome'] ?? null;
238
+    $contratto->costo = array_key_exists('costo', $validated) && $validated['costo'] !== '' && $validated['costo'] !== null
239
+      ? $validated['costo']
240
+      : null;
241
+    $contratto->periodo = $validated['periodo'] ?? null;
242
+    $contratto->data_inizio = $validated['data_inizio'] ?? null;
243
+    $contratto->data_fine = $validated['data_fine'] ?? null;
244
+    $contratto->note = $validated['note'] ?? null;
245
+    $contratto->attivo = request()->boolean('attivo');
246
+    // Ore residue e collegamento segnalazioni: gestiti sulle righe, non sull'header
247
+    $contratto->collegabile_segnalazioni = false;
248
+    $contratto->quantita_residua = null;
249
+
250
+    if ($contratto->contratto_template_id && empty($contratto->nome)) {
251
+      $tpl = ContrattoTemplate::find($contratto->contratto_template_id);
252
+      if ($tpl) {
253
+        $contratto->nome = $tpl->nome;
254
+        if ($contratto->costo === null && $tpl->costo_base !== null) {
255
+          $contratto->costo = $tpl->costo_base;
256
+        }
257
+        if (empty($contratto->periodo) && $tpl->periodo) {
258
+          $contratto->periodo = $tpl->periodo;
259
+        }
260
+      }
261
+    }
262
+  }
263
+
264
+  private function syncTodos(ContrattoServizio $contratto, array $todos): void
265
+  {
266
+    $keepIds = [];
267
+    $ordine = 0;
268
+    foreach ($todos as $row) {
269
+      $titolo = trim((string) ($row['titolo'] ?? ''));
270
+      if ($titolo === '') {
271
+        continue;
272
+      }
273
+      $todo = ! empty($row['id'])
274
+        ? ContrattoServizioTodo::where('contratto_servizio_id', $contratto->id)->find($row['id'])
275
+        : new ContrattoServizioTodo(['contratto_servizio_id' => $contratto->id]);
276
+      if (! $todo) {
277
+        continue;
278
+      }
279
+      $todo->fill([
280
+        'titolo' => $titolo,
281
+        'descrizione' => $row['descrizione'] ?? null,
282
+        'tipo_schedulazione' => $row['tipo_schedulazione'] ?? 'data_fissa',
283
+        'cron' => $row['cron'] ?? null,
284
+        'mese' => $row['mese'] ?? null,
285
+        'giorno' => $row['giorno'] ?? null,
286
+        'offset_giorni' => $row['offset_giorni'] ?? null,
287
+        'data_prossima' => $row['data_prossima'] ?? null,
288
+        'ordine' => $ordine++,
289
+        'attivo' => ! isset($row['attivo']) || filter_var($row['attivo'], FILTER_VALIDATE_BOOLEAN) || $row['attivo'] === '1' || $row['attivo'] === 'on',
290
+      ]);
291
+      $todo->contratto_servizio_id = $contratto->id;
292
+      if (! $todo->data_prossima) {
293
+        $todo->setRelation('contrattoServizio', $contratto);
294
+        $todo->data_prossima = $todo->calcolaProssimaData(
295
+          $contratto->data_inizio ? Carbon::parse($contratto->data_inizio) : Carbon::today()
296
+        )?->toDateString();
297
+      }
298
+      $todo->save();
299
+      $keepIds[] = $todo->id;
300
+    }
301
+    ContrattoServizioTodo::where('contratto_servizio_id', $contratto->id)
302
+      ->when(count($keepIds), fn ($q) => $q->whereNotIn('id', $keepIds), fn ($q) => $q)
303
+      ->delete();
304
+  }
305
+
306
+  private function syncRighe(ContrattoServizio $contratto, array $righe): void
307
+  {
308
+    $keepIds = [];
309
+    $ordine = 0;
310
+    foreach ($righe as $row) {
311
+      $prodottoId = $row['prodotto_id'] ?? null;
312
+      if (! $prodottoId) {
313
+        continue;
314
+      }
315
+      $riga = ! empty($row['id'])
316
+        ? ContrattoServizioRiga::where('contratto_servizio_id', $contratto->id)->find($row['id'])
317
+        : new ContrattoServizioRiga(['contratto_servizio_id' => $contratto->id]);
318
+      if (! $riga) {
319
+        continue;
320
+      }
321
+      $prodotto = Prodotto::find($prodottoId);
322
+      $riga->fill([
323
+        'prodotto_id' => $prodottoId,
324
+        'quantita' => $row['quantita'] ?? 1,
325
+        'unita_misura' => $row['unita_misura'] ?? ($prodotto->unita_misura ?? null),
326
+        'prezzo_unitario' => $row['prezzo_unitario'] ?? ($prodotto->prezzo ?? null),
327
+        'periodo' => $row['periodo'] ?? ($prodotto->periodo ?? null),
328
+        'note' => $row['note'] ?? null,
329
+        'ordine' => $ordine++,
330
+      ]);
331
+      $riga->contratto_servizio_id = $contratto->id;
332
+      $riga->save();
333
+      $keepIds[] = $riga->id;
334
+    }
335
+    ContrattoServizioRiga::where('contratto_servizio_id', $contratto->id)
336
+      ->when(count($keepIds), fn ($q) => $q->whereNotIn('id', $keepIds), fn ($q) => $q)
337
+      ->delete();
338
+  }
339
+}

+ 38
- 0
app/Http/Controllers/ContrattoServizioTodoController.php 파일 보기

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\ContrattoServizio;
6
+use Illuminate\Http\Request;
7
+use Illuminate\Routing\Controllers\HasMiddleware;
8
+use Illuminate\Routing\Controllers\Middleware;
9
+
10
+class ContrattoServizioTodoController extends Controller implements HasMiddleware
11
+{
12
+  public static function middleware(): array
13
+  {
14
+    return [
15
+      new Middleware('permission:view-contratto-servizio', only: ['admin_index']),
16
+      new Middleware('permission:edit-contratto-servizio', only: ['admin_update', 'admin_reclona']),
17
+    ];
18
+  }
19
+
20
+  public function admin_index($contratto_servizio_id)
21
+  {
22
+    return redirect()->route('admin.contratto_servizio.edit', $contratto_servizio_id);
23
+  }
24
+
25
+  public function admin_update(Request $request, $contratto_servizio_id)
26
+  {
27
+    return app(ContrattoServizioController::class)->admin_update($request, $contratto_servizio_id);
28
+  }
29
+
30
+  public function admin_reclona(Request $request, $contratto_servizio_id)
31
+  {
32
+    $contratto = ContrattoServizio::findOrFail($contratto_servizio_id);
33
+    $result = $contratto->importaDaTemplate(true);
34
+
35
+    return redirect()->route('admin.contratto_servizio.edit', $contratto->id)
36
+      ->with('success', "Importati {$result['righe']} servizi e {$result['todos']} todo dal template.");
37
+  }
38
+}

+ 228
- 0
app/Http/Controllers/ContrattoTemplateController.php 파일 보기

@@ -0,0 +1,228 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\ContrattoTemplateDataTable;
6
+use App\Models\ContrattoTemplate;
7
+use App\Models\ContrattoTemplateRiga;
8
+use App\Models\ContrattoTemplateTodo;
9
+use App\Models\Prodotto;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Routing\Controllers\HasMiddleware;
12
+use Illuminate\Routing\Controllers\Middleware;
13
+use Illuminate\Support\Facades\DB;
14
+
15
+class ContrattoTemplateController extends Controller implements HasMiddleware
16
+{
17
+  public static $permission_group = "Template contratti";
18
+  public static $permissions = [
19
+    'view-contratto-template' => 'Vedi',
20
+    'create-contratto-template' => 'Crea',
21
+    'edit-contratto-template' => 'Modifica',
22
+    'delete-contratto-template' => 'Elimina',
23
+  ];
24
+
25
+  public static function middleware(): array
26
+  {
27
+    return [
28
+      new Middleware('permission:view-contratto-template', only: ['admin_index', 'admin_show', 'admin_list']),
29
+      new Middleware('permission:create-contratto-template', only: ['admin_create', 'admin_store']),
30
+      new Middleware('permission:edit-contratto-template', only: ['admin_edit', 'admin_update']),
31
+      new Middleware('permission:delete-contratto-template', only: ['admin_destroy']),
32
+    ];
33
+  }
34
+
35
+  public function admin_index(ContrattoTemplateDataTable $dataTable)
36
+  {
37
+    return $dataTable->render('contratto_template.index');
38
+  }
39
+
40
+  public function admin_create()
41
+  {
42
+    return view('contratto_template.form', [
43
+      'template' => new ContrattoTemplate(['attivo' => true, 'periodo' => 'annuale']),
44
+      'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
45
+      'mode' => 'create',
46
+    ]);
47
+  }
48
+
49
+  public function admin_store(Request $request)
50
+  {
51
+    $validated = $this->validateTemplate($request);
52
+    $template = DB::transaction(function () use ($validated, $request) {
53
+      $template = new ContrattoTemplate();
54
+      $this->fillTemplate($template, $validated);
55
+      $template->save();
56
+      $this->syncTodos($template, $request->input('todos', []));
57
+      $this->syncRighe($template, $request->input('righe', []));
58
+
59
+      return $template;
60
+    });
61
+
62
+    return redirect()->route('admin.contratto_template.show', $template->id)
63
+      ->with('success', 'Template contratto creato.');
64
+  }
65
+
66
+  public function admin_show($id)
67
+  {
68
+    $template = ContrattoTemplate::with([
69
+      'todos',
70
+      'righe.prodotto',
71
+      'contrattiServizio.user',
72
+    ])->findOrFail($id);
73
+
74
+    return view('contratto_template.show', compact('template'));
75
+  }
76
+
77
+  public function admin_edit($id)
78
+  {
79
+    $template = ContrattoTemplate::with(['todos', 'righe'])->findOrFail($id);
80
+
81
+    return view('contratto_template.form', [
82
+      'template' => $template,
83
+      'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
84
+      'mode' => 'edit',
85
+    ]);
86
+  }
87
+
88
+  public function admin_update(Request $request, $id)
89
+  {
90
+    $template = ContrattoTemplate::findOrFail($id);
91
+    $validated = $this->validateTemplate($request);
92
+
93
+    DB::transaction(function () use ($template, $validated, $request) {
94
+      $this->fillTemplate($template, $validated);
95
+      $template->save();
96
+      $this->syncTodos($template, $request->input('todos', []));
97
+      $this->syncRighe($template, $request->input('righe', []));
98
+    });
99
+
100
+    return redirect()->route('admin.contratto_template.show', $template->id)
101
+      ->with('success', 'Template aggiornato.');
102
+  }
103
+
104
+  public function admin_destroy($id)
105
+  {
106
+    $template = ContrattoTemplate::findOrFail($id);
107
+    $template->contrattoTemplateTodos()->delete();
108
+    $template->contrattoTemplateRigas()->delete();
109
+    $template->delete();
110
+
111
+    return redirect()->route('admin.contratto_template.index')->with('success', 'Template eliminato.');
112
+  }
113
+
114
+  public function admin_list(Request $request)
115
+  {
116
+    $term = $request->input('term', '');
117
+    $items = ContrattoTemplate::query()
118
+      ->where('attivo', true)
119
+      ->where(function ($q) use ($term) {
120
+        $q->where('nome', 'LIKE', '%'.$term.'%')
121
+          ->orWhere('codice', 'LIKE', '%'.$term.'%');
122
+      })
123
+      ->orderBy('nome')
124
+      ->limit(25)
125
+      ->get();
126
+
127
+    return response()->json([
128
+      'results' => $items->map(fn (ContrattoTemplate $t) => [
129
+        'id' => $t->id,
130
+        'text' => $t->label,
131
+        'costo_base' => $t->costo_base,
132
+        'periodo' => $t->periodo,
133
+        'nome' => $t->nome,
134
+      ])->values(),
135
+    ]);
136
+  }
137
+
138
+  private function validateTemplate(Request $request): array
139
+  {
140
+    return $request->validate([
141
+      'codice' => 'nullable|string|max:255',
142
+      'nome' => 'required|string|max:255',
143
+      'descrizione' => 'nullable|string',
144
+      'periodo' => 'nullable|string|max:50',
145
+      'costo_base' => 'nullable|numeric|min:0',
146
+      'attivo' => 'nullable|boolean',
147
+      'todos' => 'nullable|array',
148
+      'todos.*.titolo' => 'nullable|string|max:255',
149
+      'todos.*.descrizione' => 'nullable|string',
150
+      'todos.*.tipo_schedulazione' => 'nullable|string|max:50',
151
+      'todos.*.cron' => 'nullable|string|max:100',
152
+      'todos.*.mese' => 'nullable|integer|min:1|max:12',
153
+      'todos.*.giorno' => 'nullable|integer|min:1|max:31',
154
+      'todos.*.offset_giorni' => 'nullable|integer|min:0',
155
+      'todos.*.priorita' => 'nullable|string|max:100',
156
+      'todos.*.tipo_segnalazione' => 'nullable|string|max:100',
157
+      'todos.*.attivo' => 'nullable',
158
+      'righe' => 'nullable|array',
159
+      'righe.*.prodotto_id' => 'nullable|exists:prodotto,id',
160
+      'righe.*.quantita' => 'nullable|numeric|min:0',
161
+      'righe.*.unita_misura' => 'nullable|string|in:'.implode(',', array_keys(\App\Models\Prodotto::UNITA_MISURA)),
162
+      'righe.*.prezzo_unitario' => 'nullable|numeric|min:0',
163
+      'righe.*.periodo' => 'nullable|string|max:50',
164
+      'righe.*.note' => 'nullable|string',
165
+    ]);
166
+  }
167
+
168
+  private function fillTemplate(ContrattoTemplate $template, array $validated): void
169
+  {
170
+    $template->codice = $validated['codice'] ?? null;
171
+    $template->nome = $validated['nome'];
172
+    $template->descrizione = $validated['descrizione'] ?? null;
173
+    $template->periodo = $validated['periodo'] ?? null;
174
+    $template->costo_base = $validated['costo_base'] ?? null;
175
+    $template->attivo = array_key_exists('attivo', $validated)
176
+      ? filter_var($validated['attivo'], FILTER_VALIDATE_BOOLEAN)
177
+      : (bool) request()->boolean('attivo');
178
+  }
179
+
180
+  private function syncTodos(ContrattoTemplate $template, array $todos): void
181
+  {
182
+    $template->contrattoTemplateTodos()->delete();
183
+    $ordine = 0;
184
+    foreach ($todos as $row) {
185
+      $titolo = trim((string) ($row['titolo'] ?? ''));
186
+      if ($titolo === '') {
187
+        continue;
188
+      }
189
+      ContrattoTemplateTodo::create([
190
+        'contratto_template_id' => $template->id,
191
+        'titolo' => $titolo,
192
+        'descrizione' => $row['descrizione'] ?? null,
193
+        'tipo_schedulazione' => $row['tipo_schedulazione'] ?? 'data_fissa',
194
+        'cron' => $row['cron'] ?? null,
195
+        'mese' => $row['mese'] ?? null,
196
+        'giorno' => $row['giorno'] ?? null,
197
+        'offset_giorni' => $row['offset_giorni'] ?? null,
198
+        'priorita' => $row['priorita'] ?? null,
199
+        'tipo_segnalazione' => $row['tipo_segnalazione'] ?? null,
200
+        'ordine' => $ordine++,
201
+        'attivo' => ! isset($row['attivo']) || filter_var($row['attivo'], FILTER_VALIDATE_BOOLEAN) || $row['attivo'] === '1',
202
+      ]);
203
+    }
204
+  }
205
+
206
+  private function syncRighe(ContrattoTemplate $template, array $righe): void
207
+  {
208
+    $template->contrattoTemplateRigas()->delete();
209
+    $ordine = 0;
210
+    foreach ($righe as $row) {
211
+      $prodottoId = $row['prodotto_id'] ?? null;
212
+      if (! $prodottoId) {
213
+        continue;
214
+      }
215
+      $prodotto = Prodotto::find($prodottoId);
216
+      ContrattoTemplateRiga::create([
217
+        'contratto_template_id' => $template->id,
218
+        'prodotto_id' => $prodottoId,
219
+        'quantita' => $row['quantita'] ?? 1,
220
+        'unita_misura' => $row['unita_misura'] ?? ($prodotto->unita_misura ?? null),
221
+        'prezzo_unitario' => $row['prezzo_unitario'] ?? ($prodotto->prezzo ?? null),
222
+        'periodo' => $row['periodo'] ?? ($prodotto->periodo ?? null),
223
+        'note' => $row['note'] ?? null,
224
+        'ordine' => $ordine++,
225
+      ]);
226
+    }
227
+  }
228
+}

+ 8
- 0
app/Http/Controllers/Controller.php 파일 보기

@@ -0,0 +1,8 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+abstract class Controller
6
+{
7
+  //
8
+}

+ 47
- 0
app/Http/Controllers/CostoSegnalazioneController.php 파일 보기

@@ -0,0 +1,47 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Segnalazione;
7
+use App\DataTables\CostoSegnalazioneDataTable;
8
+use App\DataTables\CostoSegnalazioneDataTableEditor;
9
+use Illuminate\Routing\Controllers\Middleware;
10
+use Illuminate\Routing\Controllers\HasMiddleware;
11
+
12
+class CostoSegnalazioneController extends Controller implements HasMiddleware
13
+{
14
+  public static $permission_group = "Costi Segnalazione";
15
+  public static $permissions = [
16
+    'create-costo-segnalazione' => 'Crea',
17
+    'edit-costo-segnalazione' => 'Modifica',
18
+    'delete-costo-segnalazione' => 'Elimina',
19
+    'view-costo-segnalazione' => 'Vedi',
20
+  ];
21
+
22
+  public static function middleware(): array
23
+  {
24
+    return [
25
+      new Middleware('permission:view-costo-segnalazione', only: ['admin_index']),
26
+      new Middleware('permission:create-costo-segnalazione|permission:edit-costo-segnalazione|permission:delete-costo-segnalazione', only: ['admin_store']),
27
+    ];
28
+  }
29
+  public function admin_index(CostoSegnalazioneDataTable $dataTable, Request $request)
30
+  {
31
+    if($request->has('segnalazione_id')){
32
+        $segnalazione = Segnalazione::find($request->segnalazione_id);
33
+        $params = [
34
+          'segnalazione' => $segnalazione
35
+        ];
36
+        return $dataTable->with($params)->render('costo_segnalazione.segnalazione', $params);
37
+      }
38
+        return $dataTable->render('costo_segnalazione.index');
39
+  }
40
+
41
+  public function admin_store(CostoSegnalazioneDataTableEditor $editor)
42
+  {
43
+    $request = request();
44
+    $input = $request->all();
45
+    return $editor->process(request());
46
+  }
47
+}

+ 40
- 0
app/Http/Controllers/GruppoController.php 파일 보기

@@ -0,0 +1,40 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Routing\Controllers\Middleware;
6
+use Illuminate\Routing\Controllers\HasMiddleware;
7
+
8
+use Illuminate\Http\Request;
9
+use App\Models\User;
10
+use App\DataTables\GruppoDataTableEditor;
11
+use App\DataTables\GruppoDataTable;
12
+use Illuminate\Support\Facades\Auth;
13
+use Session;
14
+use Illuminate\Support\Str;
15
+use Illuminate\Support\Facades\Storage;
16
+
17
+class GruppoController extends Controller implements HasMiddleware
18
+{
19
+
20
+  public static function middleware(): array
21
+  {
22
+    return [
23
+      new Middleware('permission:view-user', only: ['index']),
24
+      new Middleware('permission:create-user|edit-user|remove-user', only: ['store', 'admin_aggiorna']),
25
+    ];
26
+  }
27
+
28
+  public function index(GruppoDataTable $dataTable)
29
+  {
30
+    return $dataTable->render('gruppo.index');
31
+  }
32
+
33
+  public function store(GruppoDataTableEditor $editor)
34
+  {
35
+    $request = request();
36
+    $input = $request->all();
37
+    return $editor->process($request);
38
+  }
39
+
40
+}

+ 282
- 0
app/Http/Controllers/HomePageController.php 파일 보기

@@ -0,0 +1,282 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Http\Controllers\Controller;
6
+use Illuminate\Http\Request;
7
+use App\Models\Annotazione;
8
+use App\Models\Azienda;
9
+use App\Models\Config;
10
+use App\Models\ConfiguraSegnalazione;
11
+use App\Models\Segnalazione;
12
+use Carbon\Carbon;
13
+use Illuminate\Database\Eloquent\Builder;
14
+use Illuminate\Support\Facades\Auth;
15
+use Illuminate\Support\Facades\DB;
16
+
17
+class HomePageController extends Controller
18
+{
19
+  public static $permission_group = "Home";
20
+  public static $permissions = [
21
+    'view-admin_dashboard' => 'Vedi dashboard amministratore',
22
+  ];
23
+
24
+  public function welcome()
25
+  {
26
+    // return redirect()->route('reparto.dashboard');
27
+    $pageConfigs = ['myLayout' => 'front'];
28
+    return view('welcome', ['pageConfigs' => $pageConfigs]);
29
+  }
30
+
31
+  public function admin_dashboard()
32
+  {
33
+    if (! Auth::user()?->can('view-admin_dashboard')) {
34
+      return redirect()->route('dashboard');
35
+    }
36
+
37
+    return view('dashboard.dashboards-manutenzioni', $this->getDashboardData());
38
+  }
39
+
40
+  private function getDashboardData()
41
+  {
42
+    $labelsManutentori = [];
43
+    $datiManutentori = [];
44
+    $labelsCostiAzienda = [];
45
+    $datiCostiAzienda = [];
46
+    $labelsCostiTipo = [];
47
+    $datiCostiTipo = [];
48
+
49
+    // KPI - Statistiche generali
50
+    $totaleSegnalazioni = $this->dashboardSegnalazioneBase()->count();
51
+    $segnalazioniAperte = $this->dashboardSegnalazioneBase()->where('stato', Config::getStatiSegnalazione('aperte'))->count();
52
+    $segnalazioniInLavorazione = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('in_lavorazione'))->count();
53
+    $segnalazioniChiuse = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('chiuse'))->count();
54
+
55
+    // Statistiche mese corrente
56
+    $meseCorrente = Carbon::now()->startOfMonth();
57
+    $fineMeseCorrente = Carbon::now()->endOfMonth();
58
+    $mesePrecedente = Carbon::now()->subMonth()->startOfMonth();
59
+    $fineMesePrecedente = Carbon::now()->subMonth()->endOfMonth();
60
+
61
+    $costiMese = $this->annotazioniDashboardQuery()->whereBetween('created_at', [$meseCorrente, $fineMeseCorrente])->sum('costo');
62
+
63
+    $costiMesePrecedente = $this->annotazioniDashboardQuery()->whereBetween('created_at', [$mesePrecedente, $fineMesePrecedente])->sum('costo');
64
+
65
+    $variazioneCosti = $costiMesePrecedente > 0
66
+    ? round((($costiMese - $costiMesePrecedente) / $costiMesePrecedente) * 100, 2)
67
+    : ($costiMese > 0 ? 100 : 0);
68
+
69
+    $tempiMese = $this->annotazioniDashboardQuery()->whereBetween('tempo_data', [$meseCorrente, $fineMeseCorrente])->sum('tempo_minuti');
70
+    $oreTotaliMese = Config::formatTempo($tempiMese);
71
+
72
+    // Tempo medio di risoluzione (in giorni)
73
+    $segnalazioniChiuseConTempo = $this->dashboardSegnalazioneBase()->whereIn('stato', Config::getStatiSegnalazione('chiuse'))
74
+    ->whereNotNull('updated_at')
75
+    ->whereNotNull('created_at')
76
+    ->get();
77
+
78
+    $tempoMedioRisoluzione = 0;
79
+    if ($segnalazioniChiuseConTempo->count() > 0) {
80
+      $giorniTotali = $segnalazioniChiuseConTempo->sum(function($segnalazione) {
81
+        return $segnalazione->created_at->diffInDays($segnalazione->updated_at);
82
+      });
83
+      $tempoMedioRisoluzione = round($giorniTotali / $segnalazioniChiuseConTempo->count(), 1);
84
+    }
85
+
86
+
87
+    $segnalazioniUrgentiAperte = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
88
+    ->whereIn('priorita', ['urgente', 'emergenza_immediata'])
89
+    ->count();
90
+
91
+    // Segnalazioni senza assegnazione
92
+    $segnalazioniSenzaAssegnazione = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))->whereNull('assegnata_a_id')
93
+    ->count();
94
+
95
+    // Segnalazioni aperte da più di 7 giorni
96
+    $dataLimite = Carbon::now()->subDays(7);
97
+    $segnalazioniAperteVecchie = $this->dashboardSegnalazioneBase()->whereNotIn('stato', Config::getStatiSegnalazione('chiuse'))
98
+    ->where('created_at', '<', $dataLimite)
99
+    ->count();
100
+
101
+    // Grafico segnalazioni nel tempo (ultimi 30 giorni)
102
+    $giorni = 30;
103
+    $dataInizio = Carbon::now()->subDays($giorni);
104
+    $segnalazioniTempo = $this->dashboardSegnalazioneBase()->where('created_at', '>=', $dataInizio)
105
+    ->selectRaw('DATE(created_at) as data, COUNT(*) as totale')
106
+    ->groupBy('data')
107
+    ->orderBy('data')
108
+    ->get();
109
+
110
+    $labelsTempo = [];
111
+    $datiTempo = [];
112
+    $dataCorrente = $dataInizio->copy();
113
+    while ($dataCorrente <= Carbon::now()) {
114
+      $dataStr = $dataCorrente->format('Y-m-d');
115
+      $labelsTempo[] = $dataCorrente->format('d/m');
116
+      $datiTempo[] = $segnalazioniTempo->firstWhere('data', $dataStr)->totale ?? 0;
117
+      $dataCorrente->addDay();
118
+    }
119
+
120
+    // Distribuzione per stato
121
+    $distribuzioneStato = $this->dashboardSegnalazioneBase()->select('stato', DB::raw('COUNT(*) as totale'))
122
+    ->groupBy('stato')
123
+    ->get();
124
+
125
+    $labelsStato = [];
126
+    $datiStato = [];
127
+    foreach ($distribuzioneStato as $item) {
128
+      $config = ConfiguraSegnalazione::where('gruppo', 'stato')
129
+      ->where('value', $item->stato)
130
+      ->first();
131
+      $labelsStato[] = $config ? $config->label : $item->stato;
132
+      $datiStato[] = $item->totale;
133
+    }
134
+
135
+    // Distribuzione per priorità
136
+    $labelsPriorita = [];
137
+    $datiPriorita = [];
138
+    foreach(ConfiguraSegnalazione::where('gruppo', 'Priorità')->orderBy('order', 'asc')->get() as $config) {
139
+      $labelsPriorita[] = $config->label;
140
+      $datiPriorita[] = $this->dashboardSegnalazioneBase()->where('priorita', $config->value)->count();
141
+    }
142
+
143
+    // Distribuzione per tipo
144
+    $distribuzioneTipo = $this->dashboardSegnalazioneBase()->select('tipo', DB::raw('COUNT(*) as totale'))
145
+    ->whereNotNull('tipo')
146
+    ->groupBy('tipo')
147
+    ->get();
148
+
149
+    $labelsTipo = [];
150
+    $datiTipo = [];
151
+    foreach ($distribuzioneTipo as $item) {
152
+      $config = ConfiguraSegnalazione::where('gruppo', 'Tipo')
153
+      ->where('value', $item->tipo)
154
+      ->first();
155
+      $labelsTipo[] = $config ? $config->label : $item->tipo;
156
+      $datiTipo[] = $item->totale;
157
+    }
158
+
159
+    // Distribuzione per azienda
160
+    $labelsAzienda = [];
161
+    $datiAzienda = [];
162
+
163
+    foreach(Azienda::orderBy('nome', 'asc')->get() as $azienda) {
164
+      $labelsAzienda[] = $azienda->full_name;
165
+      $datiAzienda[] = $this->dashboardSegnalazioneBase()->where('azienda_id', $azienda->id)->count();
166
+    }
167
+
168
+    // Assicurati che tutti gli array non siano vuoti per evitare errori nei grafici
169
+    if (empty($labelsTempo)) {
170
+      $labelsTempo = ['Nessun dato'];
171
+      $datiTempo = [0];
172
+    }
173
+    if (empty($labelsStato)) {
174
+      $labelsStato = ['Nessun dato'];
175
+      $datiStato = [0];
176
+    }
177
+    if (empty($labelsPriorita)) {
178
+      $labelsPriorita = ['Nessun dato'];
179
+      $datiPriorita = [0];
180
+    }
181
+    if (empty($labelsAzienda)) {
182
+      $labelsAzienda = ['Nessun dato'];
183
+      $datiAzienda = [0];
184
+    }
185
+    if (empty($labelsManutentori)) {
186
+      $labelsManutentori = ['Nessun dato'];
187
+      $datiManutentori = [0];
188
+    }
189
+    if (empty($labelsCostiAzienda)) {
190
+      $labelsCostiAzienda = ['Nessun dato'];
191
+      $datiCostiAzienda = [0];
192
+    }
193
+    if (empty($labelsTipo)) {
194
+      $labelsTipo = ['Nessun dato'];
195
+      $datiTipo = [0];
196
+    }
197
+    if (empty($labelsCostiTipo)) {
198
+      $labelsCostiTipo = ['Nessun dato'];
199
+      $datiCostiTipo = [0];
200
+    }
201
+
202
+    return [
203
+      'totaleSegnalazioni' => $totaleSegnalazioni,
204
+      'segnalazioniAperte' => $segnalazioniAperte,
205
+      'segnalazioniInLavorazione' => $segnalazioniInLavorazione,
206
+      'segnalazioniChiuse' => $segnalazioniChiuse,
207
+      'costiMese' => $costiMese,
208
+      'costiMesePrecedente' => $costiMesePrecedente,
209
+      'variazioneCosti' => $variazioneCosti,
210
+      'oreTotaliMese' => $oreTotaliMese,
211
+      'tempoMedioRisoluzione' => $tempoMedioRisoluzione,
212
+      'segnalazioniUrgentiAperte' => $segnalazioniUrgentiAperte,
213
+      'segnalazioniSenzaAssegnazione' => $segnalazioniSenzaAssegnazione,
214
+      'segnalazioniAperteVecchie' => $segnalazioniAperteVecchie,
215
+      'labelsTempo' => $labelsTempo,
216
+      'datiTempo' => $datiTempo,
217
+      'labelsStato' => $labelsStato,
218
+      'datiStato' => $datiStato,
219
+      'labelsPriorita' => $labelsPriorita,
220
+      'datiPriorita' => $datiPriorita,
221
+      'labelsAzienda' => $labelsAzienda,
222
+      'datiAzienda' => $datiAzienda,
223
+      'labelsManutentori' => $labelsManutentori,
224
+      'datiManutentori' => $datiManutentori,
225
+      'labelsCostiAzienda' => $labelsCostiAzienda,
226
+      'datiCostiAzienda' => $datiCostiAzienda,
227
+      'labelsTipo' => $labelsTipo,
228
+      'datiTipo' => $datiTipo,
229
+      'labelsCostiTipo' => $labelsCostiTipo,
230
+      'datiCostiTipo' => $datiCostiTipo,
231
+    ];
232
+  }
233
+
234
+  /**
235
+   * Query base sulle segnalazioni per la dashboard (allineata a view-segnalazione / view-segnalazione-assegnate).
236
+   */
237
+  private function dashboardSegnalazioneBase(): Builder
238
+  {
239
+    return $this->restrictDashboardSegnalazioniQuery(Segnalazione::query());
240
+  }
241
+
242
+  private function restrictDashboardSegnalazioniQuery(Builder $query): Builder
243
+  {
244
+    $user = Auth::user();
245
+    if ($user && ! $user->can('view-segnalazione')) {
246
+      $canOwn = $user->can('view-own-segnalazione');
247
+      $canAssegnate = $user->can('view-segnalazione-assegnate');
248
+      if ($canOwn || $canAssegnate) {
249
+        $query->where(function (Builder $q) use ($user, $canOwn, $canAssegnate) {
250
+          if ($canOwn) {
251
+            $q->orWhere('user_id', $user->id);
252
+          }
253
+          if ($canAssegnate) {
254
+            $q->orWhereIn('assegnata_a_id', Segnalazione::assigneeIdsForViewSegnalazioneAssegnate($user));
255
+          }
256
+        });
257
+      } else {
258
+        $query->whereRaw('0 = 1');
259
+      }
260
+    }
261
+
262
+    return $query;
263
+  }
264
+
265
+  /**
266
+   * Annotazioni limitate alle segnalazioni visibili per la dashboard (stesso filtro su segnalazione).
267
+   */
268
+  private function annotazioniDashboardQuery(): Builder
269
+  {
270
+    $query = Annotazione::query();
271
+    $user = Auth::user();
272
+    if ($user && ! $user->can('view-segnalazione')) {
273
+      $query->whereHas('segnalazione', fn (Builder $sq) => $this->restrictDashboardSegnalazioniQuery($sq));
274
+    }
275
+
276
+    return $query;
277
+  }
278
+
279
+  public function dashboard(){
280
+    return view('dashboard.index');
281
+  }
282
+}

+ 70
- 0
app/Http/Controllers/NazioneController.php 파일 보기

@@ -0,0 +1,70 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\Models\Nazione;
7
+use Auth;
8
+
9
+class NazioneController extends Controller
10
+{
11
+
12
+  public function list(Request $request){
13
+    $input = $request->all();
14
+    $list = array();
15
+
16
+    if($request->has('initialValue') && $input['initialValue'] == "true"){
17
+      $val = trim(str_replace('"', '', $input['value']));
18
+      $entity = Nazione::find($val);
19
+      return response()->json(array('id' => $entity->id, 'text' => $entity->nome ));
20
+
21
+    }else{
22
+      $page = $input['page'];
23
+      $resultCount = 25;
24
+      $offset = ($page - 1) * $resultCount;
25
+
26
+      $entitys = Nazione::where('nome', 'LIKE', '%'.$input['term'].'%')
27
+      ->orderBy('id', 'ASC');
28
+
29
+      foreach ($entitys->skip($offset)->take($resultCount)->get() as $e) {
30
+        array_push($list, array('id' => $e->id, 'text' => $e->nome, 'hasComuni' => $e->comuni->count() > 0 ));
31
+      }
32
+
33
+      $count = $entitys->count();
34
+      $endCount = $offset + $resultCount;
35
+      $morePages = $count > $endCount;
36
+
37
+      $results = array(
38
+        "results" => $list,
39
+        "pagination" => array(
40
+          "more" => $morePages
41
+        )
42
+      );
43
+
44
+    }
45
+    return response()->json($results);
46
+  }
47
+
48
+  public function api_listNazioni(Request $request){
49
+    $arrayNazioni = [];
50
+    $resultCount = 25;
51
+    $offset = ($request->page) * $resultCount;
52
+    $nazioni = Nazione::orderBy('nome')->skip($offset)->take($resultCount);
53
+    if($request->has('search') && $request->search != null && $request->search != ''){
54
+      $nazioni->where('nome', 'LIKE', '%'.$request->search.'%');
55
+    }
56
+
57
+    $arrayNazioni[] = [
58
+      'id' => 118,
59
+      'nome' => 'Italia'
60
+    ];
61
+
62
+    foreach($nazioni->get() as $nazione){
63
+      $arrayNazioni[] = [
64
+        'id' => $nazione->id,
65
+        'nome' => $nazione->nome
66
+      ];
67
+    }
68
+    return response()->json($arrayNazioni);
69
+  }
70
+}

+ 426
- 0
app/Http/Controllers/OffertaController.php 파일 보기

@@ -0,0 +1,426 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\OffertaDataTable;
6
+use App\Models\Annotazione;
7
+use App\Models\ConfiguraSegnalazione;
8
+use App\Models\Offerta;
9
+use App\Models\OffertaRiga;
10
+use App\Models\OrdineLavoro;
11
+use App\Models\Prodotto;
12
+use App\Models\Segnalazione;
13
+use App\Models\User;
14
+use App\Services\OffertaPdfService;
15
+use Illuminate\Http\Request;
16
+use Illuminate\Routing\Controllers\HasMiddleware;
17
+use Illuminate\Routing\Controllers\Middleware;
18
+use Illuminate\Support\Facades\Auth;
19
+use Illuminate\Support\Facades\DB;
20
+use Illuminate\Support\Facades\File;
21
+use Illuminate\Support\Facades\Storage;
22
+use Illuminate\Validation\Rule;
23
+
24
+class OffertaController extends Controller implements HasMiddleware
25
+{
26
+  public static $permission_group = "Offerte";
27
+  public static $permissions = [
28
+    'view-offerta' => 'Vedi',
29
+    'create-offerta' => 'Crea',
30
+    'edit-offerta' => 'Modifica',
31
+    'delete-offerta' => 'Elimina',
32
+  ];
33
+
34
+  public static function middleware(): array
35
+  {
36
+    return [
37
+      new Middleware('permission:view-offerta', only: ['admin_index', 'admin_show', 'admin_list', 'admin_pdf']),
38
+      new Middleware('permission:create-offerta', only: ['admin_create', 'admin_store']),
39
+      new Middleware('permission:edit-offerta', only: ['admin_edit', 'admin_update', 'admin_conferma', 'downloadAllegato', 'deleteAllegato']),
40
+      new Middleware('permission:delete-offerta', only: ['admin_destroy']),
41
+    ];
42
+  }
43
+
44
+  public function admin_index(OffertaDataTable $dataTable)
45
+  {
46
+    return $dataTable->render('offerta.index');
47
+  }
48
+
49
+  public function admin_create(Request $request)
50
+  {
51
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
52
+
53
+    return view('offerta.form', [
54
+      'offerta' => new Offerta([
55
+        'stato' => 'bozza',
56
+        'data' => now()->toDateString(),
57
+        'numero' => Offerta::nextNumero(),
58
+        'user_id' => $request->user_id,
59
+      ]),
60
+      'clienti' => $clienti,
61
+      'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
62
+      'mode' => 'create',
63
+    ]);
64
+  }
65
+
66
+  public function admin_store(Request $request)
67
+  {
68
+    $validated = $this->validateOfferta($request);
69
+    $offerta = DB::transaction(function () use ($validated, $request) {
70
+      $offerta = new Offerta();
71
+      $this->fillOfferta($offerta, $validated);
72
+      $offerta->numero = $validated['numero'] ?? Offerta::nextNumero();
73
+      $offerta->save();
74
+      $this->syncRighe($offerta, $request->input('righe', []));
75
+      $this->storeAllegato($request, $offerta);
76
+      $offerta->ricalcolaTotale();
77
+
78
+      return $offerta;
79
+    });
80
+
81
+    return redirect()->route('admin.offerta.show', $offerta->id)
82
+      ->with('success', 'Offerta creata.');
83
+  }
84
+
85
+  public function admin_show($id)
86
+  {
87
+    $offerta = Offerta::with(['user', 'righe.prodotto', 'ordiniLavoro'])->findOrFail($id);
88
+    $assegnatari = User::where('is_azienda', false)->orderBy('cognome')->orderBy('nome')->get();
89
+
90
+    return view('offerta.show', compact('offerta', 'assegnatari'));
91
+  }
92
+
93
+  public function admin_conferma(Request $request, $id)
94
+  {
95
+    abort_unless(Auth::user()->can('create-ordine-lavoro'), 403);
96
+
97
+    $offerta = Offerta::with(['user', 'righe', 'ordiniLavoro'])->findOrFail($id);
98
+
99
+    if (! in_array($offerta->stato, ['bozza', 'inviata'], true)) {
100
+      return redirect()->route('admin.offerta.show', $offerta->id)
101
+        ->with('error', 'Solo le offerte in bozza o inviate possono essere confermate.');
102
+    }
103
+
104
+    if (! $offerta->user_id) {
105
+      return redirect()->route('admin.offerta.show', $offerta->id)
106
+        ->with('error', 'Assegna un cliente all\'offerta prima di confermarla.');
107
+    }
108
+
109
+    if ($offerta->ordiniLavoro->isNotEmpty()) {
110
+      $offerta->stato = 'accettata';
111
+      $offerta->save();
112
+
113
+      $esistente = $offerta->ordiniLavoro->first();
114
+
115
+      return redirect()->route('admin.ordine_lavoro.show', $esistente->id)
116
+        ->with('warning', 'Offerta già collegata a un ordine di lavoro: non sono stati creati nuovi ODL né segnalazioni.');
117
+    }
118
+
119
+    $validated = $request->validate([
120
+      'riga_ids' => 'nullable|array',
121
+      'riga_ids.*' => [
122
+        'integer',
123
+        Rule::exists('offerta_riga', 'id')->where('offerta_id', $offerta->id),
124
+      ],
125
+      'assegnazioni' => 'nullable|array',
126
+      'assegnazioni.*' => 'nullable|exists:users,id',
127
+    ]);
128
+
129
+    $rigaIds = collect($validated['riga_ids'] ?? [])->unique()->values();
130
+    if ($rigaIds->isNotEmpty()) {
131
+      abort_unless(Auth::user()->can('create-segnalazione'), 403);
132
+    }
133
+
134
+    $righeGiaConvertite = $rigaIds->isEmpty()
135
+      ? collect()
136
+      : Segnalazione::query()
137
+        ->where('origine', 'offerta')
138
+        ->whereIn('meta->offerta_riga_id', $rigaIds->all())
139
+        ->get()
140
+        ->map(fn (Segnalazione $s) => (int) data_get($s->meta, 'offerta_riga_id'))
141
+        ->unique()
142
+        ->values();
143
+
144
+    $rigaIdsDaCreare = $rigaIds->reject(fn ($id) => $righeGiaConvertite->contains((int) $id))->values();
145
+
146
+    $ordine = DB::transaction(function () use ($offerta, $validated, $rigaIdsDaCreare) {
147
+      $offerta->stato = 'accettata';
148
+      $offerta->save();
149
+
150
+      $ordine = new OrdineLavoro([
151
+        'user_id' => $offerta->user_id,
152
+        'numero' => OrdineLavoro::nextNumero(),
153
+        'titolo' => $offerta->titolo ?: ('ODL da '.$offerta->label),
154
+        'stato' => 'aperto',
155
+        'percentuale_avanzamento' => 0,
156
+        'data_apertura' => now()->toDateString(),
157
+      ]);
158
+      $ordine->save();
159
+      $ordine->offerte()->sync([$offerta->id]);
160
+
161
+      if ($rigaIdsDaCreare->isEmpty()) {
162
+        return $ordine;
163
+      }
164
+
165
+      $statoDefault = ConfiguraSegnalazione::where('gruppo', 'stato')->orderBy('order')->value('value') ?: 'nuova';
166
+      $prioritaDefault = ConfiguraSegnalazione::where('gruppo', 'Priorità')->orderBy('order')->value('value');
167
+      $tipoDefault = ConfiguraSegnalazione::where('gruppo', 'Tipo')->orderBy('order')->value('value');
168
+      $cliente = $offerta->user;
169
+      $aziendaId = $cliente?->is_azienda ? $cliente->id : $cliente?->azienda_id;
170
+      $assegnazioni = $validated['assegnazioni'] ?? [];
171
+
172
+      foreach ($offerta->righe->whereIn('id', $rigaIdsDaCreare) as $riga) {
173
+        $assegnataAId = ! empty($assegnazioni[$riga->id]) ? (int) $assegnazioni[$riga->id] : null;
174
+
175
+        $segnalazione = new Segnalazione();
176
+        $segnalazione->titolo = $riga->descrizione ?: ('Riga offerta '.$offerta->label);
177
+        $segnalazione->stato = $statoDefault;
178
+        $segnalazione->priorita = $prioritaDefault;
179
+        $segnalazione->tipo = $tipoDefault;
180
+        $segnalazione->origine = 'offerta';
181
+        $segnalazione->ordine_lavoro_id = $ordine->id;
182
+        $segnalazione->assegnata_a_id = $assegnataAId;
183
+        $segnalazione->azienda_id = $aziendaId;
184
+        $segnalazione->user_id = Auth::id();
185
+        $segnalazione->tempo_stimato = $riga->tempo_stimato;
186
+        $segnalazione->meta = ['offerta_riga_id' => $riga->id];
187
+        $segnalazione->notificaApertaDa = false;
188
+        $segnalazione->save();
189
+
190
+        $annotazione = new Annotazione();
191
+        $annotazione->segnalazione_id = $segnalazione->id;
192
+        $annotazione->user_id = Auth::id();
193
+        $annotazione->text = $this->descrizioneSegnalazioneDaRiga($riga, $offerta);
194
+        $annotazione->riservata = false;
195
+        $annotazione->save();
196
+      }
197
+
198
+      return $ordine;
199
+    });
200
+
201
+    $msg = 'Offerta confermata e ordine di lavoro creato.';
202
+    if ($rigaIdsDaCreare->isNotEmpty()) {
203
+      $msg .= ' Create '.$rigaIdsDaCreare->count().' segnalazioni.';
204
+    }
205
+    if ($righeGiaConvertite->isNotEmpty()) {
206
+      $msg .= ' Saltate '.$righeGiaConvertite->count().' righe già convertite in segnalazione.';
207
+    }
208
+
209
+    return redirect()->route('admin.ordine_lavoro.show', $ordine->id)->with('success', $msg);
210
+  }
211
+
212
+  private function descrizioneSegnalazioneDaRiga(OffertaRiga $riga, Offerta $offerta): string
213
+  {
214
+    $parts = [];
215
+    $parts[] = 'Da offerta '.$offerta->label;
216
+    if ($riga->descrizione) {
217
+      $parts[] = $riga->descrizione;
218
+    }
219
+    $um = Prodotto::UNITA_MISURA[$riga->unita_misura] ?? $riga->unita_misura;
220
+    if ($riga->quantita !== null || $um) {
221
+      $parts[] = 'Quantità: '.($riga->quantita ?? '—').($um ? ' '.$um : '');
222
+    }
223
+    if ($riga->unita_misura === 'forfait' && $riga->tempo_stimato !== null) {
224
+      $parts[] = 'Tempo stimato: '.rtrim(rtrim(number_format((float) $riga->tempo_stimato, 2, ',', ''), '0'), ',').' ore';
225
+    }
226
+    if ($riga->note_private) {
227
+      $parts[] = 'Note: '.$riga->note_private;
228
+    }
229
+
230
+    return implode("\n", $parts);
231
+  }
232
+
233
+  public function admin_edit($id)
234
+  {
235
+    $offerta = Offerta::with('righe')->findOrFail($id);
236
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
237
+
238
+    return view('offerta.form', [
239
+      'offerta' => $offerta,
240
+      'clienti' => $clienti,
241
+      'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
242
+      'mode' => 'edit',
243
+    ]);
244
+  }
245
+
246
+  public function admin_update(Request $request, $id)
247
+  {
248
+    $offerta = Offerta::findOrFail($id);
249
+    $validated = $this->validateOfferta($request);
250
+
251
+    DB::transaction(function () use ($offerta, $validated, $request) {
252
+      $this->fillOfferta($offerta, $validated);
253
+      $offerta->save();
254
+      $this->syncRighe($offerta, $request->input('righe', []));
255
+      $this->storeAllegato($request, $offerta);
256
+      $offerta->load('righe');
257
+      $offerta->ricalcolaTotale();
258
+    });
259
+
260
+    return redirect()->route('admin.offerta.show', $offerta->id)
261
+      ->with('success', 'Offerta aggiornata.');
262
+  }
263
+
264
+  public function admin_destroy(Request $request, $id)
265
+  {
266
+    $offerta = Offerta::findOrFail($id);
267
+    $this->deleteAllegatoFile($offerta);
268
+    $offerta->righe()->delete();
269
+    $offerta->delete();
270
+
271
+    if ($request->expectsJson()) {
272
+      return response()->json(['success' => true, 'message' => 'Offerta eliminata.']);
273
+    }
274
+
275
+    return redirect()->route('admin.offerta.index')->with('success', 'Offerta eliminata.');
276
+  }
277
+
278
+  public function admin_list(Request $request)
279
+  {
280
+    $term = $request->input('term', '');
281
+    $userId = $request->input('user_id');
282
+    $soloAccettate = $request->boolean('solo_accettate');
283
+
284
+    $query = Offerta::query()
285
+      ->when($userId, fn ($q) => $q->where('user_id', $userId))
286
+      ->when($soloAccettate, fn ($q) => $q->where('stato', 'accettata'))
287
+      ->where(function ($q) use ($term) {
288
+        $q->where('titolo', 'LIKE', '%'.$term.'%');
289
+        if (ctype_digit($term)) {
290
+          $q->orWhere('numero', (int) $term)->orWhere('id', (int) $term);
291
+        }
292
+      })
293
+      ->orderByDesc('data')
294
+      ->limit(25)
295
+      ->get();
296
+
297
+    return response()->json([
298
+      'results' => $query->map(fn (Offerta $o) => [
299
+        'id' => $o->id,
300
+        'text' => $o->label,
301
+      ])->values(),
302
+    ]);
303
+  }
304
+
305
+  public function admin_pdf($id, OffertaPdfService $pdfService)
306
+  {
307
+    $offerta = Offerta::findOrFail($id);
308
+    $filename = $pdfService->filename($offerta);
309
+
310
+    return response()->stream(function () use ($pdfService, $offerta) {
311
+      echo $pdfService->generateBinary($offerta);
312
+    }, 200, [
313
+      'Content-Type' => 'application/pdf',
314
+      'Content-Disposition' => 'inline; filename="'.$filename.'"',
315
+    ]);
316
+  }
317
+
318
+  public function downloadAllegato($offerta_id)
319
+  {
320
+    $offerta = Offerta::findOrFail($offerta_id);
321
+    abort_unless($offerta->hasAllegato() && Storage::disk('allegatiOfferta')->exists($offerta->path_file), 404);
322
+
323
+    return Storage::disk('allegatiOfferta')->download($offerta->path_file, $offerta->getNomeAllegato());
324
+  }
325
+
326
+  public function deleteAllegato(Request $request)
327
+  {
328
+    $offerta = Offerta::findOrFail($request->offerta_id);
329
+    $this->deleteAllegatoFile($offerta);
330
+    $offerta->path_file = null;
331
+    $offerta->save();
332
+
333
+    return response()->json(['success' => true]);
334
+  }
335
+
336
+  private function validateOfferta(Request $request): array
337
+  {
338
+    return $request->validate([
339
+      'user_id' => 'required|exists:users,id',
340
+      'numero' => 'nullable|integer|min:1',
341
+      'titolo' => 'nullable|string|max:255',
342
+      'stato' => 'required|string|in:'.implode(',', array_keys(Offerta::STATI)),
343
+      'data' => 'nullable|date',
344
+      'data_validita' => 'nullable|date',
345
+      'note' => 'nullable|string',
346
+      'path_file' => 'nullable|file|max:10240',
347
+      'righe' => 'nullable|array',
348
+      'righe.*.prodotto_id' => 'nullable|exists:prodotto,id',
349
+      'righe.*.descrizione' => 'nullable|string|max:255',
350
+      'righe.*.quantita' => 'nullable|numeric|min:0',
351
+      'righe.*.unita_misura' => 'nullable|string|in:'.implode(',', array_keys(\App\Models\Prodotto::UNITA_MISURA)),
352
+      'righe.*.prezzo_unitario' => 'nullable|numeric|min:0',
353
+      'righe.*.sconto' => 'nullable|numeric|min:0|max:100',
354
+      'righe.*.note_private' => 'nullable|string',
355
+      'righe.*.tempo_stimato' => 'nullable|numeric|min:0',
356
+    ]);
357
+  }
358
+
359
+  private function fillOfferta(Offerta $offerta, array $validated): void
360
+  {
361
+    $offerta->user_id = $validated['user_id'];
362
+    if (isset($validated['numero'])) {
363
+      $offerta->numero = $validated['numero'];
364
+    }
365
+    $offerta->titolo = $validated['titolo'] ?? null;
366
+    $offerta->stato = $validated['stato'];
367
+    $offerta->data = $validated['data'] ?? null;
368
+    $offerta->data_validita = $validated['data_validita'] ?? null;
369
+    $offerta->note = $validated['note'] ?? null;
370
+  }
371
+
372
+  private function syncRighe(Offerta $offerta, array $righe): void
373
+  {
374
+    $offerta->righe()->delete();
375
+    $ordine = 0;
376
+    foreach ($righe as $riga) {
377
+      $descrizione = trim((string) ($riga['descrizione'] ?? ''));
378
+      $prodottoId = $riga['prodotto_id'] ?? null;
379
+      if ($descrizione === '' && ! $prodottoId) {
380
+        continue;
381
+      }
382
+      if ($descrizione === '' && $prodottoId) {
383
+        $descrizione = optional(Prodotto::find($prodottoId))->nome ?? 'Riga';
384
+      }
385
+      OffertaRiga::create([
386
+        'offerta_id' => $offerta->id,
387
+        'prodotto_id' => $prodottoId ?: null,
388
+        'descrizione' => $descrizione,
389
+        'quantita' => $riga['quantita'] ?? 1,
390
+        'unita_misura' => $riga['unita_misura'] ?? null,
391
+        'prezzo_unitario' => $riga['prezzo_unitario'] ?? 0,
392
+        'sconto' => $riga['sconto'] ?? null,
393
+        'note_private' => isset($riga['note_private']) && trim((string) $riga['note_private']) !== ''
394
+          ? trim((string) $riga['note_private'])
395
+          : null,
396
+        'tempo_stimato' => (($riga['unita_misura'] ?? null) === 'forfait' && isset($riga['tempo_stimato']) && $riga['tempo_stimato'] !== '')
397
+          ? $riga['tempo_stimato']
398
+          : null,
399
+        'ordine' => $ordine++,
400
+      ]);
401
+    }
402
+  }
403
+
404
+  private function storeAllegato(Request $request, Offerta $offerta): void
405
+  {
406
+    if (! $request->hasFile('path_file')) {
407
+      return;
408
+    }
409
+    $this->deleteAllegatoFile($offerta);
410
+    $file = $request->file('path_file');
411
+    $filename = time().'_'.$file->getClientOriginalName();
412
+    $directory = storage_path('app/public/allegatiOfferta/'.$offerta->id);
413
+    if (! File::exists($directory)) {
414
+      File::makeDirectory($directory, 0755, true);
415
+    }
416
+    $offerta->path_file = $file->storeAs((string) $offerta->id, $filename, 'allegatiOfferta');
417
+    $offerta->save();
418
+  }
419
+
420
+  private function deleteAllegatoFile(Offerta $offerta): void
421
+  {
422
+    if (filled($offerta->path_file) && Storage::disk('allegatiOfferta')->exists($offerta->path_file)) {
423
+      Storage::disk('allegatiOfferta')->delete($offerta->path_file);
424
+    }
425
+  }
426
+}

+ 222
- 0
app/Http/Controllers/OrdineLavoroController.php 파일 보기

@@ -0,0 +1,222 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\OrdineLavoroDataTable;
6
+use App\Models\Offerta;
7
+use App\Models\OrdineLavoro;
8
+use App\Models\User;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Routing\Controllers\HasMiddleware;
11
+use Illuminate\Routing\Controllers\Middleware;
12
+use Illuminate\Support\Facades\DB;
13
+use Illuminate\Validation\Rule;
14
+
15
+class OrdineLavoroController extends Controller implements HasMiddleware
16
+{
17
+  public static $permission_group = "Ordini di lavoro";
18
+  public static $permissions = [
19
+    'view-ordine-lavoro' => 'Vedi',
20
+    'create-ordine-lavoro' => 'Crea',
21
+    'edit-ordine-lavoro' => 'Modifica',
22
+    'delete-ordine-lavoro' => 'Elimina',
23
+  ];
24
+
25
+  public static function middleware(): array
26
+  {
27
+    return [
28
+      new Middleware('permission:view-ordine-lavoro', only: ['admin_index', 'admin_show', 'admin_list']),
29
+      new Middleware('permission:create-ordine-lavoro', only: ['admin_create', 'admin_store']),
30
+      new Middleware('permission:edit-ordine-lavoro', only: ['admin_edit', 'admin_update']),
31
+      new Middleware('permission:delete-ordine-lavoro', only: ['admin_destroy']),
32
+    ];
33
+  }
34
+
35
+  public function admin_index(OrdineLavoroDataTable $dataTable)
36
+  {
37
+    return $dataTable->render('ordine_lavoro.index');
38
+  }
39
+
40
+  public function admin_create(Request $request)
41
+  {
42
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
43
+    $offertaIds = collect($request->input('offerta_ids', []))
44
+      ->when($request->filled('offerta_id'), fn ($c) => $c->push($request->offerta_id))
45
+      ->filter()
46
+      ->unique()
47
+      ->values();
48
+
49
+    $ordine = new OrdineLavoro([
50
+      'stato' => 'aperto',
51
+      'data_apertura' => now()->toDateString(),
52
+      'numero' => OrdineLavoro::nextNumero(),
53
+      'percentuale_avanzamento' => 0,
54
+      'user_id' => $request->user_id,
55
+    ]);
56
+
57
+    if ($offertaIds->isNotEmpty()) {
58
+      $offerte = Offerta::whereIn('id', $offertaIds)->get();
59
+      if ($offerte->isNotEmpty()) {
60
+        $ordine->user_id = $ordine->user_id ?: $offerte->first()->user_id;
61
+        if (! $ordine->titolo) {
62
+          $ordine->titolo = $offerte->first()->titolo ?: ('ODL da '.$offerte->first()->label);
63
+        }
64
+      }
65
+    }
66
+
67
+    return view('ordine_lavoro.form', [
68
+      'ordine' => $ordine,
69
+      'clienti' => $clienti,
70
+      'offerteSelezionate' => $offertaIds->all(),
71
+      'mode' => 'create',
72
+    ]);
73
+  }
74
+
75
+  public function admin_store(Request $request)
76
+  {
77
+    $validated = $this->validateOrdine($request);
78
+    $ordine = DB::transaction(function () use ($validated) {
79
+      $ordine = new OrdineLavoro();
80
+      $this->fillOrdine($ordine, $validated);
81
+      $ordine->numero = $validated['numero'] ?? OrdineLavoro::nextNumero();
82
+      $ordine->save();
83
+      $ordine->offerte()->sync($validated['offerta_ids'] ?? []);
84
+
85
+      return $ordine;
86
+    });
87
+
88
+    return redirect()->route('admin.ordine_lavoro.show', $ordine->id)
89
+      ->with('success', 'Ordine di lavoro creato.');
90
+  }
91
+
92
+  public function admin_show($id)
93
+  {
94
+    $ordine = OrdineLavoro::with([
95
+      'user',
96
+      'offerte.user',
97
+      'segnalazioni.annotazioni',
98
+    ])->findOrFail($id);
99
+
100
+    return view('ordine_lavoro.show', compact('ordine'));
101
+  }
102
+
103
+  public function admin_edit($id)
104
+  {
105
+    $ordine = OrdineLavoro::with('offerte')->findOrFail($id);
106
+    $clienti = User::where('is_gruppo', false)->orderBy('cognome')->orderBy('nome')->get();
107
+
108
+    return view('ordine_lavoro.form', [
109
+      'ordine' => $ordine,
110
+      'clienti' => $clienti,
111
+      'offerteSelezionate' => $ordine->offerte->pluck('id')->all(),
112
+      'mode' => 'edit',
113
+    ]);
114
+  }
115
+
116
+  public function admin_update(Request $request, $id)
117
+  {
118
+    $ordine = OrdineLavoro::findOrFail($id);
119
+    $validated = $this->validateOrdine($request, $ordine);
120
+
121
+    DB::transaction(function () use ($ordine, $validated) {
122
+      $this->fillOrdine($ordine, $validated);
123
+      $ordine->save();
124
+      $ordine->offerte()->sync($validated['offerta_ids'] ?? []);
125
+    });
126
+
127
+    return redirect()->route('admin.ordine_lavoro.show', $ordine->id)
128
+      ->with('success', 'Ordine di lavoro aggiornato.');
129
+  }
130
+
131
+  public function admin_destroy(Request $request, $id)
132
+  {
133
+    $ordine = OrdineLavoro::findOrFail($id);
134
+    $ordine->offerte()->detach();
135
+    $ordine->segnalazioni()->update(['ordine_lavoro_id' => null]);
136
+    $ordine->delete();
137
+
138
+    if ($request->expectsJson()) {
139
+      return response()->json(['success' => true]);
140
+    }
141
+
142
+    return redirect()->route('admin.ordine_lavoro.index')->with('success', 'Ordine di lavoro eliminato.');
143
+  }
144
+
145
+  public function admin_list(Request $request)
146
+  {
147
+    $term = $request->input('term', '');
148
+    $userId = $request->input('user_id');
149
+
150
+    $items = OrdineLavoro::query()
151
+      ->when($userId, fn ($q) => $q->where('user_id', $userId))
152
+      ->where(function ($q) use ($term) {
153
+        $q->where('titolo', 'LIKE', '%'.$term.'%');
154
+        if (ctype_digit($term)) {
155
+          $q->orWhere('numero', (int) $term)->orWhere('id', (int) $term);
156
+        }
157
+      })
158
+      ->orderByDesc('data_apertura')
159
+      ->limit(25)
160
+      ->get();
161
+
162
+    return response()->json([
163
+      'results' => $items->map(fn (OrdineLavoro $o) => [
164
+        'id' => $o->id,
165
+        'text' => $o->label,
166
+      ])->values(),
167
+    ]);
168
+  }
169
+
170
+  private function validateOrdine(Request $request, ?OrdineLavoro $ordine = null): array
171
+  {
172
+    $validated = $request->validate([
173
+      'user_id' => 'required|exists:users,id',
174
+      'numero' => 'nullable|integer|min:1',
175
+      'titolo' => 'nullable|string|max:255',
176
+      'stato' => 'required|string|in:'.implode(',', array_keys(OrdineLavoro::STATI)),
177
+      'percentuale_avanzamento' => 'nullable|integer|min:0|max:100',
178
+      'data_apertura' => 'nullable|date',
179
+      'data_chiusura' => 'nullable|date',
180
+      'note' => 'nullable|string',
181
+      'offerta_ids' => 'nullable|array',
182
+      'offerta_ids.*' => [
183
+        'integer',
184
+        Rule::exists('offerta', 'id')->where(fn ($q) => $q->where('user_id', $request->user_id)),
185
+      ],
186
+    ]);
187
+
188
+    $offertaIds = collect($validated['offerta_ids'] ?? [])->filter()->unique()->values();
189
+    if ($offertaIds->isNotEmpty()) {
190
+      $giàCollegate = Offerta::query()
191
+        ->whereIn('id', $offertaIds)
192
+        ->whereHas('ordiniLavoro', function ($q) use ($ordine) {
193
+          if ($ordine?->id) {
194
+            $q->where('ordine_lavoro.id', '!=', $ordine->id);
195
+          }
196
+        })
197
+        ->pluck('id');
198
+
199
+      if ($giàCollegate->isNotEmpty()) {
200
+        throw \Illuminate\Validation\ValidationException::withMessages([
201
+          'offerta_ids' => 'Una o più offerte selezionate sono già collegate a un altro ordine di lavoro.',
202
+        ]);
203
+      }
204
+    }
205
+
206
+    return $validated;
207
+  }
208
+
209
+  private function fillOrdine(OrdineLavoro $ordine, array $validated): void
210
+  {
211
+    $ordine->user_id = $validated['user_id'];
212
+    if (isset($validated['numero'])) {
213
+      $ordine->numero = $validated['numero'];
214
+    }
215
+    $ordine->titolo = $validated['titolo'] ?? null;
216
+    $ordine->stato = $validated['stato'];
217
+    $ordine->percentuale_avanzamento = $validated['percentuale_avanzamento'] ?? 0;
218
+    $ordine->data_apertura = $validated['data_apertura'] ?? null;
219
+    $ordine->data_chiusura = $validated['data_chiusura'] ?? null;
220
+    $ordine->note = $validated['note'] ?? null;
221
+  }
222
+}

+ 215
- 0
app/Http/Controllers/ProdottoController.php 파일 보기

@@ -0,0 +1,215 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use App\DataTables\ProdottoDataTable;
7
+use App\Models\Prodotto;
8
+use App\Models\ProdottoTodo;
9
+use Illuminate\Routing\Controllers\Middleware;
10
+use Illuminate\Routing\Controllers\HasMiddleware;
11
+
12
+class ProdottoController extends Controller implements HasMiddleware
13
+{
14
+    public static $permission_group = "Prodotti";
15
+    public static $permissions = [
16
+        'view-prodotto' => 'Vedi',
17
+        'create-prodotto' => 'Crea',
18
+        'edit-prodotto' => 'Modifica',
19
+        'remove-prodotto' => 'Rimuovi',
20
+    ];
21
+
22
+    public static function middleware(): array
23
+    {
24
+        return [
25
+            new Middleware('permission:view-prodotto', only: ['admin_index', 'admin_list', 'admin_show']),
26
+            new Middleware('permission:create-prodotto|edit-prodotto', only: ['admin_store']),
27
+            new Middleware('permission:remove-prodotto', only: ['admin_delete']),
28
+        ];
29
+    }
30
+
31
+    public function admin_index(ProdottoDataTable $dataTable)
32
+    {
33
+        return $dataTable->render('prodotto.index');
34
+    }
35
+
36
+    public function admin_show($id)
37
+    {
38
+        $prodotto = Prodotto::with(['todos' => fn ($q) => $q->orderBy('ordine')->orderBy('id')])->findOrFail($id);
39
+
40
+        return response()->json([
41
+            'id' => $prodotto->id,
42
+            'codice' => $prodotto->codice,
43
+            'nome' => $prodotto->nome,
44
+            'descrizione' => $prodotto->descrizione,
45
+            'tipo' => $prodotto->tipo,
46
+            'unita_misura' => $prodotto->unita_misura,
47
+            'prezzo' => $prodotto->prezzo,
48
+            'periodo' => $prodotto->periodo,
49
+            'attivo' => (bool) $prodotto->attivo,
50
+            'prenotabile_online' => (bool) $prodotto->prenotabile_online,
51
+            'todos' => $prodotto->todos->map(fn (ProdottoTodo $t) => [
52
+                'titolo' => $t->titolo,
53
+                'descrizione' => $t->descrizione,
54
+                'ordine' => $t->ordine,
55
+                'attivo' => (bool) $t->attivo,
56
+                'obbligatorio' => (bool) $t->obbligatorio,
57
+                'public' => (bool) $t->public,
58
+            ])->values(),
59
+        ]);
60
+    }
61
+
62
+    public function admin_store(Request $request)
63
+    {
64
+        $isUpdate = $request->filled('id');
65
+        if ($isUpdate && ! $request->user()->can('edit-prodotto')) {
66
+            abort(403);
67
+        }
68
+        if (! $isUpdate && ! $request->user()->can('create-prodotto')) {
69
+            abort(403);
70
+        }
71
+
72
+        $validated = $request->validate([
73
+            'id' => 'nullable|exists:prodotto,id',
74
+            'codice' => 'nullable|string|max:255',
75
+            'nome' => 'required|string|max:255',
76
+            'descrizione' => 'nullable|string',
77
+            'tipo' => 'nullable|string|max:50',
78
+            'unita_misura' => 'nullable|string|in:'.implode(',', array_keys(Prodotto::UNITA_MISURA)),
79
+            'prezzo' => 'nullable|numeric|min:0',
80
+            'periodo' => 'nullable|string|max:50',
81
+            'attivo' => 'nullable|boolean',
82
+            'prenotabile_online' => 'nullable|boolean',
83
+            'todos' => 'nullable|array',
84
+            'todos.*.titolo' => 'nullable|string|max:255',
85
+            'todos.*.descrizione' => 'nullable|string',
86
+            'todos.*.ordine' => 'nullable|integer|min:0',
87
+            'todos.*.attivo' => 'nullable',
88
+            'todos.*.obbligatorio' => 'nullable',
89
+            'todos.*.public' => 'nullable',
90
+        ]);
91
+
92
+        $prodotto = $isUpdate
93
+            ? Prodotto::findOrFail($validated['id'])
94
+            : new Prodotto();
95
+
96
+        $prodotto->codice = $validated['codice'] ?: null;
97
+        $prodotto->nome = $validated['nome'];
98
+        $prodotto->descrizione = $validated['descrizione'] ?: null;
99
+        $prodotto->tipo = $validated['tipo'] ?: null;
100
+        $prodotto->unita_misura = $validated['unita_misura'] ?: null;
101
+        $prodotto->prezzo = $validated['prezzo'] !== null && $validated['prezzo'] !== '' ? $validated['prezzo'] : null;
102
+        $prodotto->periodo = $validated['periodo'] ?: null;
103
+        $prodotto->attivo = $request->boolean('attivo', true);
104
+        $prodotto->prenotabile_online = $request->boolean('prenotabile_online', false);
105
+        $prodotto->save();
106
+
107
+        $this->syncTodos($prodotto, $request->input('todos', []));
108
+
109
+        $payload = [
110
+            'id' => $prodotto->id,
111
+            'codice' => $prodotto->codice,
112
+            'nome' => $prodotto->nome,
113
+            'descrizione' => $prodotto->descrizione,
114
+            'tipo' => $prodotto->tipo,
115
+            'tipo_label' => $prodotto->tipo_label,
116
+            'unita_misura' => $prodotto->unita_misura,
117
+            'unita_misura_label' => $prodotto->unita_misura_label,
118
+            'prezzo' => $prodotto->prezzo,
119
+            'prezzo_label' => $prodotto->prezzo !== null ? number_format((float) $prodotto->prezzo, 2, ',', '.') . ' €' : '—',
120
+            'periodo' => $prodotto->periodo,
121
+            'attivo' => (bool) $prodotto->attivo,
122
+            'prenotabile_online' => (bool) $prodotto->prenotabile_online,
123
+            'label' => $prodotto->label,
124
+        ];
125
+
126
+        if ($request->expectsJson() || $request->ajax()) {
127
+            return response()->json([
128
+                'success' => true,
129
+                'message' => $isUpdate ? 'Prodotto aggiornato.' : 'Prodotto creato.',
130
+                'prodotto' => $payload,
131
+            ]);
132
+        }
133
+
134
+        return redirect()->route('admin.prodotto.index')->with('success', $isUpdate ? 'Prodotto aggiornato.' : 'Prodotto creato.');
135
+    }
136
+
137
+    public function admin_list(Request $request)
138
+    {
139
+        $input = $request->all();
140
+        $term = $input['term'] ?? '';
141
+        $page = (int) ($input['page'] ?? 1);
142
+        $resultCount = 25;
143
+        $offset = ($page - 1) * $resultCount;
144
+
145
+        $query = \App\Models\Prodotto::query()
146
+            ->where('attivo', true)
147
+            ->where(function ($q) use ($term) {
148
+                $q->where('nome', 'LIKE', '%'.$term.'%')
149
+                    ->orWhere('codice', 'LIKE', '%'.$term.'%');
150
+            })
151
+            ->orderBy('nome');
152
+
153
+        $list = $query->skip($offset)->take($resultCount)->get()->map(fn ($p) => [
154
+            'id' => $p->id,
155
+            'text' => $p->label,
156
+            'tipo' => $p->tipo,
157
+            'unita_misura' => $p->unita_misura,
158
+            'prezzo' => $p->prezzo,
159
+            'periodo' => $p->periodo,
160
+        ])->values();
161
+
162
+        $count = $query->count();
163
+
164
+        return response()->json([
165
+            'results' => $list,
166
+            'pagination' => ['more' => $count > ($offset + $resultCount)],
167
+        ]);
168
+    }
169
+
170
+    public function admin_delete(Request $request, $id)
171
+    {
172
+        $prodotto = Prodotto::findOrFail($id);
173
+        $prodotto->delete();
174
+
175
+        if ($request->expectsJson() || $request->ajax()) {
176
+            return response()->json([
177
+                'success' => true,
178
+                'message' => 'Prodotto eliminato.',
179
+            ]);
180
+        }
181
+
182
+        return redirect()->route('admin.prodotto.index')->with('success', 'Prodotto eliminato.');
183
+    }
184
+
185
+    private function syncTodos(Prodotto $prodotto, array $todos): void
186
+    {
187
+        $prodotto->prodottoTodos()->delete();
188
+
189
+        $ordine = 0;
190
+        foreach ($todos as $row) {
191
+            $titolo = trim((string) ($row['titolo'] ?? ''));
192
+            if ($titolo === '') {
193
+                continue;
194
+            }
195
+            ProdottoTodo::create([
196
+                'prodotto_id' => $prodotto->id,
197
+                'titolo' => $titolo,
198
+                'descrizione' => isset($row['descrizione']) && trim((string) $row['descrizione']) !== ''
199
+                    ? trim((string) $row['descrizione'])
200
+                    : null,
201
+                'ordine' => isset($row['ordine']) && $row['ordine'] !== '' ? (int) $row['ordine'] : $ordine,
202
+                'attivo' => array_key_exists('attivo', $row)
203
+                    ? filter_var($row['attivo'], FILTER_VALIDATE_BOOLEAN)
204
+                    : true,
205
+                'obbligatorio' => array_key_exists('obbligatorio', $row)
206
+                    ? filter_var($row['obbligatorio'], FILTER_VALIDATE_BOOLEAN)
207
+                    : false,
208
+                'public' => array_key_exists('public', $row)
209
+                    ? filter_var($row['public'], FILTER_VALIDATE_BOOLEAN)
210
+                    : true,
211
+            ]);
212
+            $ordine++;
213
+        }
214
+    }
215
+}

+ 123
- 0
app/Http/Controllers/PromoCodeController.php 파일 보기

@@ -0,0 +1,123 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\DataTables\PromoCodeDataTable;
6
+use App\Models\Prodotto;
7
+use App\Models\PromoCode;
8
+use Illuminate\Http\Request;
9
+use Illuminate\Routing\Controllers\HasMiddleware;
10
+use Illuminate\Routing\Controllers\Middleware;
11
+use Illuminate\Validation\Rule;
12
+
13
+class PromoCodeController extends Controller implements HasMiddleware
14
+{
15
+    public static $permission_group = 'Codici promo';
16
+
17
+    public static $permissions = [
18
+        'view-promo-code' => 'Vedi',
19
+        'create-promo-code' => 'Crea',
20
+        'edit-promo-code' => 'Modifica',
21
+        'delete-promo-code' => 'Elimina',
22
+    ];
23
+
24
+    public static function middleware(): array
25
+    {
26
+        return [
27
+            new Middleware('permission:view-promo-code', only: ['admin_index', 'admin_show']),
28
+            new Middleware('permission:create-promo-code', only: ['admin_create', 'admin_store']),
29
+            new Middleware('permission:edit-promo-code', only: ['admin_edit', 'admin_update']),
30
+            new Middleware('permission:delete-promo-code', only: ['admin_destroy']),
31
+        ];
32
+    }
33
+
34
+    public function admin_index(PromoCodeDataTable $dataTable)
35
+    {
36
+        return $dataTable->render('promo_code.index');
37
+    }
38
+
39
+    public function admin_create()
40
+    {
41
+        return view('promo_code.form', [
42
+            'promo' => new PromoCode(['attivo' => true, 'sconto_percentuale' => 10, 'usi_count' => 0]),
43
+            'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
44
+            'mode' => 'create',
45
+        ]);
46
+    }
47
+
48
+    public function admin_store(Request $request)
49
+    {
50
+        $validated = $this->validatePromo($request);
51
+        $promo = new PromoCode();
52
+        $this->fillPromo($promo, $validated);
53
+        $promo->usi_count = 0;
54
+        $promo->save();
55
+        $promo->prodotti()->sync($validated['prodotto_ids'] ?? []);
56
+
57
+        return redirect()->route('admin.promo_code.index')->with('success', 'Codice promo creato.');
58
+    }
59
+
60
+    public function admin_edit($id)
61
+    {
62
+        $promo = PromoCode::with('prodotti')->findOrFail($id);
63
+
64
+        return view('promo_code.form', [
65
+            'promo' => $promo,
66
+            'prodotti' => Prodotto::where('attivo', true)->orderBy('nome')->get(),
67
+            'mode' => 'edit',
68
+        ]);
69
+    }
70
+
71
+    public function admin_update(Request $request, $id)
72
+    {
73
+        $promo = PromoCode::findOrFail($id);
74
+        $validated = $this->validatePromo($request, $promo->id);
75
+        $this->fillPromo($promo, $validated);
76
+        $promo->save();
77
+        $promo->prodotti()->sync($validated['prodotto_ids'] ?? []);
78
+
79
+        return redirect()->route('admin.promo_code.index')->with('success', 'Codice promo aggiornato.');
80
+    }
81
+
82
+    public function admin_destroy($id)
83
+    {
84
+        $promo = PromoCode::findOrFail($id);
85
+        $promo->prodotti()->detach();
86
+        $promo->delete();
87
+
88
+        if (request()->expectsJson()) {
89
+            return response()->json(['success' => true]);
90
+        }
91
+
92
+        return redirect()->route('admin.promo_code.index')->with('success', 'Codice promo eliminato.');
93
+    }
94
+
95
+    private function validatePromo(Request $request, ?int $ignoreId = null): array
96
+    {
97
+        return $request->validate([
98
+            'codice' => [
99
+                'required', 'string', 'max:50',
100
+                Rule::unique('promo_code', 'codice')->ignore($ignoreId),
101
+            ],
102
+            'sconto_percentuale' => 'required|numeric|min:0|max:100',
103
+            'attivo' => 'nullable|boolean',
104
+            'valido_dal' => 'nullable|date',
105
+            'valido_al' => 'nullable|date|after_or_equal:valido_dal',
106
+            'usi_max' => 'nullable|integer|min:1',
107
+            'note' => 'nullable|string',
108
+            'prodotto_ids' => 'nullable|array',
109
+            'prodotto_ids.*' => 'integer|exists:prodotto,id',
110
+        ]);
111
+    }
112
+
113
+    private function fillPromo(PromoCode $promo, array $validated): void
114
+    {
115
+        $promo->codice = strtoupper(trim($validated['codice']));
116
+        $promo->sconto_percentuale = $validated['sconto_percentuale'];
117
+        $promo->attivo = request()->boolean('attivo', true);
118
+        $promo->valido_dal = $validated['valido_dal'] ?? null;
119
+        $promo->valido_al = $validated['valido_al'] ?? null;
120
+        $promo->usi_max = $validated['usi_max'] ?? null;
121
+        $promo->note = $validated['note'] ?? null;
122
+    }
123
+}

+ 317
- 0
app/Http/Controllers/RolePermissionsController.php 파일 보기

@@ -0,0 +1,317 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\Permission;
6
+use App\Models\Role;
7
+use App\Models\User;
8
+use Illuminate\Http\Request;
9
+use Illuminate\Routing\Controllers\HasMiddleware;
10
+use Illuminate\Routing\Controllers\Middleware;
11
+use Illuminate\Support\Facades\Auth;
12
+use Illuminate\Support\Str;
13
+
14
+class RolePermissionsController extends Controller implements HasMiddleware
15
+{
16
+    public static $permission_group = 'Ruoli e Permessi';
17
+
18
+    public static $permissions = [
19
+        'manage-roles-permissions' => 'Gestisci ruoli e permessi',
20
+    ];
21
+
22
+    public static function middleware(): array
23
+    {
24
+        return [
25
+            new Middleware('permission:manage-roles-permissions'),
26
+        ];
27
+    }
28
+
29
+    public function index()
30
+    {
31
+        $user = Auth::user();
32
+        $rolesQuery = Role::query();
33
+        if (! $user?->hasRole('superadmin')) {
34
+            $rolesQuery->where('name', '!=', 'superadmin');
35
+        }
36
+        $roles = $rolesQuery->orderBy('name')->get();
37
+
38
+        $permissions = Permission::query()
39
+            ->select(['id', 'name', 'display_name', 'group'])
40
+            ->get()
41
+            ->map(function ($p) {
42
+                $p->group = $p->group ?: 'Altro';
43
+                $p->display_name = $p->display_name ?: $p->name;
44
+
45
+                return $p;
46
+            });
47
+
48
+        $permissionsByGroup = $permissions
49
+            ->groupBy('group')
50
+            ->sortKeys()
51
+            ->map(function ($items) {
52
+                return $items->sortBy('name')->values();
53
+            });
54
+
55
+        $roles->load('permissions');
56
+        $rolePermissionIds = [];
57
+        foreach ($roles as $role) {
58
+            $rolePermissionIds[$role->id] = $role->permissions->pluck('id')->flip()->toArray();
59
+        }
60
+
61
+        return view('role.ruoli_permessi', [
62
+            'roles' => $roles,
63
+            'permissionsByGroup' => $permissionsByGroup,
64
+            'rolePermissionIds' => $rolePermissionIds,
65
+        ]);
66
+    }
67
+
68
+    public function store(Request $request)
69
+    {
70
+        $validated = $request->validate([
71
+            'display_name' => ['required', 'string', 'max:255'],
72
+        ]);
73
+
74
+        $slug = Str::slug($validated['display_name']);
75
+        if ($slug === '') {
76
+            return back()->withErrors(['display_name' => 'Nome ruolo non valido.'])->withInput();
77
+        }
78
+
79
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
80
+        if ($slug === 'superadmin' && ! $userIsSuperAdmin) {
81
+            abort(403, 'Operazione non consentita.');
82
+        }
83
+
84
+        if (Role::where('name', $slug)->exists()) {
85
+            return back()->withErrors(['display_name' => 'Esiste già un ruolo con questo nome.'])->withInput();
86
+        }
87
+
88
+        $role = new Role;
89
+        $role->guard_name = 'web';
90
+        $role->display_name = $validated['display_name'];
91
+        $role->name = $slug;
92
+        $role->save();
93
+
94
+        return redirect()
95
+            ->route('admin.ruoli_permessi.index')
96
+            ->with('status', 'Ruolo creato!');
97
+    }
98
+
99
+    public function update(Request $request)
100
+    {
101
+        $validated = $request->validate([
102
+            'role_id' => ['required', 'integer', 'min:1'],
103
+            'display_name' => ['required', 'string', 'max:255'],
104
+        ]);
105
+
106
+        $role = Role::findOrFail($validated['role_id']);
107
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
108
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
109
+            abort(403, 'Operazione non consentita.');
110
+        }
111
+
112
+        $slug = Str::slug($validated['display_name']);
113
+        if ($slug === '') {
114
+            return back()->withErrors(['display_name' => 'Nome ruolo non valido.'])->withInput();
115
+        }
116
+
117
+        if ($slug === 'superadmin' && ! $userIsSuperAdmin) {
118
+            abort(403, 'Operazione non consentita.');
119
+        }
120
+
121
+        $exists = Role::where('name', $slug)
122
+            ->where('id', '!=', $role->id)
123
+            ->exists();
124
+        if ($exists) {
125
+            return back()->withErrors(['display_name' => 'Esiste già un ruolo con questo nome.'])->withInput();
126
+        }
127
+
128
+        $role->display_name = $validated['display_name'];
129
+        $role->name = $slug;
130
+        $role->save();
131
+
132
+        return redirect()
133
+            ->route('admin.ruoli_permessi.index')
134
+            ->with('status', 'Ruolo aggiornato!');
135
+    }
136
+
137
+    public function destroy(Request $request)
138
+    {
139
+        $validated = $request->validate([
140
+            'role_id' => ['required', 'integer', 'min:1'],
141
+        ]);
142
+
143
+        $role = Role::findOrFail($validated['role_id']);
144
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
145
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
146
+            abort(403, 'Operazione non consentita.');
147
+        }
148
+
149
+        $role->delete();
150
+
151
+        return redirect()
152
+            ->route('admin.ruoli_permessi.index')
153
+            ->with('status', 'Ruolo eliminato!');
154
+    }
155
+
156
+    public function togglePermission(Request $request)
157
+    {
158
+        $validated = $request->validate([
159
+            'role_id' => ['required', 'integer', 'min:1'],
160
+            'permission_id' => ['required', 'integer', 'min:1'],
161
+            'enabled' => ['required'],
162
+        ]);
163
+
164
+        $role = Role::findOrFail($validated['role_id']);
165
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
166
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
167
+            abort(403, 'Operazione non consentita.');
168
+        }
169
+
170
+        $permission = Permission::findOrFail($validated['permission_id']);
171
+        $enabled = $request->boolean('enabled');
172
+
173
+        if ($enabled) {
174
+            $role->givePermissionTo($permission->name);
175
+        } else {
176
+            $role->revokePermissionTo($permission->name);
177
+        }
178
+
179
+        return response()->json(['success' => true]);
180
+    }
181
+
182
+    public function syncPermissions(Request $request)
183
+    {
184
+        $validated = $request->validate([
185
+            'role_id' => ['required', 'integer', 'min:1'],
186
+            'permission_ids' => ['sometimes', 'array'],
187
+        ]);
188
+
189
+        $role = Role::findOrFail($validated['role_id']);
190
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
191
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
192
+            abort(403, 'Operazione non consentita.');
193
+        }
194
+
195
+        $permissionIds = array_map('intval', $request->input('permission_ids', []));
196
+
197
+        $permissionNames = Permission::query()
198
+            ->whereIn('id', $permissionIds)
199
+            ->pluck('name')
200
+            ->all();
201
+
202
+        $role->syncPermissions($permissionNames);
203
+
204
+        return response()->json(['success' => true]);
205
+    }
206
+
207
+    public function assignedUsersForRole(Request $request)
208
+    {
209
+        $validated = $request->validate([
210
+            'role_id' => ['required', 'integer', 'min:1'],
211
+        ]);
212
+
213
+        $role = Role::findOrFail($validated['role_id']);
214
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
215
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
216
+            abort(403, 'Operazione non consentita.');
217
+        }
218
+
219
+        $users = $role->users()
220
+            ->orderBy('cognome')
221
+            ->orderBy('nome')
222
+            ->get();
223
+
224
+        return response()->json([
225
+            'success' => true,
226
+            'assigned' => $users->map(function (User $u) {
227
+                return [
228
+                    'id' => $u->id,
229
+                    'text' => $u->getLabel(),
230
+                ];
231
+            })->values(),
232
+        ]);
233
+    }
234
+
235
+    public function searchUsersForRole(Request $request)
236
+    {
237
+        $validated = $request->validate([
238
+            'role_id' => ['required', 'integer', 'min:1'],
239
+            'q' => ['sometimes', 'string', 'max:255'],
240
+        ]);
241
+
242
+        $role = Role::findOrFail($validated['role_id']);
243
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
244
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
245
+            abort(403, 'Operazione non consentita.');
246
+        }
247
+
248
+        $q = trim((string) ($validated['q'] ?? ''));
249
+
250
+        $assignedIds = $role->users()->pluck('id')->all();
251
+
252
+        $query = User::query()->soloUtenti()->whereNotIn('id', $assignedIds);
253
+
254
+        if ($q !== '') {
255
+            $query->where(function ($sub) use ($q) {
256
+                $sub->where('nome', 'like', '%'.$q.'%')
257
+                    ->orWhere('cognome', 'like', '%'.$q.'%')
258
+                    ->orWhere('email', 'like', '%'.$q.'%');
259
+            });
260
+        }
261
+
262
+        $users = $query->orderBy('cognome')->orderBy('nome')->limit(20)->get();
263
+
264
+        $results = $users->map(function (User $u) {
265
+            return [
266
+                'id' => $u->id,
267
+                'text' => $u->getLabel(),
268
+            ];
269
+        })->values();
270
+
271
+        return response()->json([
272
+            'results' => $results,
273
+            'pagination' => ['more' => false],
274
+        ]);
275
+    }
276
+
277
+    public function addUserToRole(Request $request)
278
+    {
279
+        $validated = $request->validate([
280
+            'role_id' => ['required', 'integer', 'min:1'],
281
+            'user_id' => ['required', 'integer', 'min:1'],
282
+        ]);
283
+
284
+        $role = Role::findOrFail($validated['role_id']);
285
+        $user = User::findOrFail($validated['user_id']);
286
+
287
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
288
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
289
+            abort(403, 'Operazione non consentita.');
290
+        }
291
+
292
+        // Non usare syncRoles: deve NON rimuovere eventuali altri ruoli già assegnati.
293
+        $user->assignRole($role);
294
+
295
+        return response()->json(['success' => true]);
296
+    }
297
+
298
+    public function removeUserFromRole(Request $request)
299
+    {
300
+        $validated = $request->validate([
301
+            'role_id' => ['required', 'integer', 'min:1'],
302
+            'user_id' => ['required', 'integer', 'min:1'],
303
+        ]);
304
+
305
+        $role = Role::findOrFail($validated['role_id']);
306
+        $user = User::findOrFail($validated['user_id']);
307
+
308
+        $userIsSuperAdmin = Auth::user()?->hasRole('superadmin') ?? false;
309
+        if ($role->name === 'superadmin' && ! $userIsSuperAdmin) {
310
+            abort(403, 'Operazione non consentita.');
311
+        }
312
+
313
+        $user->removeRole($role->name);
314
+
315
+        return response()->json(['success' => true]);
316
+    }
317
+}

+ 947
- 0
app/Http/Controllers/SegnalazioneController.php 파일 보기

@@ -0,0 +1,947 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use Illuminate\Routing\Controllers\Middleware;
7
+use Illuminate\Routing\Controllers\HasMiddleware;
8
+use App\Models\Segnalazione;
9
+use App\Models\Reparto;
10
+use App\Models\Luogo;
11
+use App\Models\User;
12
+use App\Models\Annotazione;
13
+use App\Models\AllegatoAnnotazione;
14
+use App\Models\Assegnazione;
15
+use App\Models\TempoImpiegato;
16
+use App\Models\Config;
17
+use App\Models\ConfiguraSegnalazione;
18
+use App\Models\SegnalazioneTodo;
19
+use App\DataTables\SegnalazioneDataTable;
20
+use Illuminate\Support\Facades\Auth;
21
+use App\DataTables\SegnalazioneDataTableEditor;
22
+use App\Services\SegnalazioneTicketPdfService;
23
+use Illuminate\Support\Str;
24
+use Illuminate\Support\Facades\Log;
25
+use Illuminate\Support\Facades\File;
26
+use Illuminate\Validation\Rule;
27
+
28
+class SegnalazioneController extends Controller implements HasMiddleware
29
+{
30
+  public static $permission_group = "Segnalazioni";
31
+  public static $permissions = [
32
+    'create-segnalazione' => 'Crea',
33
+    'edit-segnalazione' => 'Modifica',
34
+    'delete-segnalazione' => 'Elimina',
35
+    'view-segnalazione' => 'Vedi tutte le segnalazioni',
36
+    'view-own-segnalazione' => 'Vedi solo le segnalazioni create dall\'utente',
37
+    'view-segnalazione-assegnate' => 'Vedi solo le segnalazioni assegnate all\'utente o ai propri gruppi',
38
+    'create-segnalazione-noauth' => 'Crea (no auth)',
39
+  ];
40
+
41
+  public static function middleware(): array
42
+  {
43
+    return [
44
+      new Middleware('permission:create-segnalazione', only: ['store', 'admin_crea', 'admin_nuova']),
45
+      new Middleware('permission:edit-segnalazione', only: [
46
+        'updateStato',
47
+        'admin_aggiorna',
48
+        'admin_toggle_todo',
49
+        'admin_update_todo_note',
50
+        'admin_store_todo',
51
+        'admin_update_todo',
52
+        'admin_destroy_todo',
53
+      ]),
54
+      new Middleware('permission:delete-segnalazione', only: ['destroy', 'admin_delete']),
55
+      // new Middleware('permission:view-segnalazione', only: ['show']),
56
+      // new Middleware('permission:create-segnalazione-noauth', only: ['nuova']),
57
+    ];
58
+  }
59
+  public function admin_index(SegnalazioneDataTable $dataTable, Request $request, ?string $status = null)
60
+  {
61
+    // Imposta showAll in base al parametro della richiesta
62
+    $dataTable->showAll = $request->get('show_all', 'false') === 'true';
63
+    return $dataTable->with(['status' => $status])->render('segnalazione.admin.index', ['status' => $status]);
64
+  }
65
+
66
+  // ADMIN
67
+  public function admin_nuova(Request $request){
68
+    return view('segnalazione.admin.nuova');
69
+  }
70
+
71
+  public function admin_aggiorna(Request $request, $id)
72
+  {
73
+    $request->validate([
74
+      'titolo' => 'required|string|max:255',
75
+      'azienda_id' => ['nullable', Rule::exists('users', 'id')->where('is_azienda', 1)],
76
+      'user_id' => 'required|exists:users,id', // utente che ha aperto la segnalazione
77
+      'assegnata_a_id' => 'nullable|exists:users,id',
78
+      'ordine_lavoro_id' => 'nullable|exists:ordine_lavoro,id',
79
+      'contratto_servizio_id' => 'nullable|exists:contratto_servizio,id',
80
+      'stato' => 'nullable|string|max:255',
81
+      'priorita' => 'nullable|string|max:255',
82
+      'tipo' => 'nullable|string|max:255',
83
+      'categoria_sicurezza' => 'nullable|string|max:255',
84
+      'annotazione_user_id' => 'required|exists:users,id',
85
+      'annotazione' => 'nullable|string',
86
+      'tempo_ore' => 'nullable|string',
87
+      'tempo_user_id' => 'nullable|exists:users,id',
88
+      'allegato' => 'nullable|array',
89
+      'allegato.*' => 'file|max:10240', // max 10MB per file
90
+    ]);
91
+
92
+    $segnalazione = Segnalazione::find($id);
93
+    if (!$segnalazione) {
94
+      return redirect()->back()->with('error', 'Segnalazione non trovata');
95
+    }
96
+
97
+    $assegnataAId = $request->filled('assegnata_a_id') ? (int) $request->assegnata_a_id : null;
98
+
99
+    // Se non c’è assegnazione, assegna a chi sta rispondendo (utente annotazione).
100
+    if ($assegnataAId === null && ! $segnalazione->assegnata_a_id) {
101
+      $assegnataAId = (int) ($request->annotazione_user_id ?: Auth::id());
102
+    }
103
+
104
+    abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
105
+    abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $assegnataAId, $segnalazione->user_id), 403);
106
+
107
+    $tempoMinuti = null;
108
+    if ($request->filled('tempo_ore')) {
109
+      $tempoMinuti = Config::parseTempoToMinuti($request->input('tempo_ore'));
110
+      if ($tempoMinuti === null) {
111
+        return redirect()->back()->withInput()->with('error', 'Formato tempo non valido');
112
+      }
113
+    }
114
+
115
+    $blockClose = $segnalazione->closingBlockedMessage($request->stato);
116
+    if ($blockClose) {
117
+      return redirect()->back()->withInput()->with('error', $blockClose);
118
+    }
119
+
120
+    $segnalazione->titolo = $request->titolo;
121
+    $segnalazione->azienda_id = $request->filled('azienda_id') ? $request->azienda_id : null;
122
+    $segnalazione->user_id = $request->user_id;
123
+    $segnalazione->assegnata_a_id = $assegnataAId;
124
+    $segnalazione->ordine_lavoro_id = $request->filled('ordine_lavoro_id') ? $request->ordine_lavoro_id : null;
125
+    $segnalazione->contratto_servizio_id = $request->filled('contratto_servizio_id') ? $request->contratto_servizio_id : null;
126
+    $segnalazione->stato = $request->stato;
127
+    $segnalazione->priorita = $request->priorita;
128
+    $segnalazione->tipo = $request->tipo;
129
+    $segnalazione->save();
130
+
131
+    $annotazione = new Annotazione();
132
+    $annotazione->text = $request->text;
133
+    $annotazione->segnalazione_id = $segnalazione->id;
134
+    $annotazione->user_id = $request->annotazione_user_id;
135
+    $annotazione->riservata = $request->boolean('annotazione_riservata');
136
+    $annotazione->save();
137
+
138
+    if ($tempoMinuti !== null) {
139
+      $annotazione->tempo_minuti = $tempoMinuti;
140
+      $annotazione->tempo_data = $request->tempo_data;
141
+    }
142
+
143
+    if($request->costo !== null) {
144
+      $annotazione->costo = $request->costo;
145
+      $annotazione->costo_descrizione = $request->costo_descrizione;
146
+    }
147
+
148
+    $annotazione->save();
149
+
150
+    if ($request->hasFile('allegato')) {
151
+      $directory = storage_path('app/public/allegatiAnnotazioni/' . $segnalazione->id);
152
+      if (!File::exists($directory)) {
153
+        File::makeDirectory($directory, 0755, true);
154
+      }
155
+
156
+      foreach ($request->file('allegato') as $file) {
157
+        if (!$file) {
158
+          continue;
159
+        }
160
+        $filename = time() . '_' . $file->getClientOriginalName();
161
+        $path = $file->storeAs($segnalazione->id, $filename, 'allegatiAnnotazioni');
162
+
163
+        $allegato = new AllegatoAnnotazione();
164
+          $allegato->annotazione()->associate($annotazione);
165
+          $allegato->nome = $file->getClientOriginalName();
166
+          $allegato->path_file = $path;
167
+          $allegato->save();
168
+      }
169
+    }
170
+
171
+    return redirect()->route('admin.segnalazione.show', ['id' => $segnalazione->id])
172
+    ->with('success', 'Segnalazione aggiornata con successo');
173
+  }
174
+
175
+  public function admin_crea(Request $request)
176
+  {
177
+    // Validazione base
178
+    $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
179
+      'azienda_id' => ['nullable', Rule::exists('users', 'id')->where('is_azienda', 1)],
180
+      'user_id' => 'required|exists:users,id',
181
+      'assegnata_a_id' => 'nullable|exists:users,id',
182
+      'ordine_lavoro_id' => 'nullable|exists:ordine_lavoro,id',
183
+      'contratto_servizio_id' => 'nullable|exists:contratto_servizio,id',
184
+      'titolo' => 'required|string|max:255',
185
+      'descrizione' => 'required|string',
186
+      'allegato' => 'nullable|array',
187
+      'allegato.*' => 'file|max:10240', // max 10MB per file
188
+    ]);
189
+
190
+    if ($validator->fails()) {
191
+      return redirect()->back()
192
+      ->withErrors($validator)
193
+      ->withInput();
194
+    }
195
+
196
+    // Crea la segnalazione
197
+      $segnalazione = new Segnalazione();
198
+      $segnalazione->fill($request->all());
199
+      $segnalazione->user_id = $request->user_id;
200
+      $segnalazione->assegnata_a_id = $request->filled('assegnata_a_id') ? $request->assegnata_a_id : null;
201
+      $segnalazione->azienda_id = $request->filled('azienda_id') ? $request->azienda_id : null;
202
+      $segnalazione->ordine_lavoro_id = $request->filled('ordine_lavoro_id') ? $request->ordine_lavoro_id : null;
203
+      $segnalazione->contratto_servizio_id = $request->filled('contratto_servizio_id') ? $request->contratto_servizio_id : null;
204
+      if (! $segnalazione->origine) {
205
+        $segnalazione->origine = 'manuale';
206
+      }
207
+      $segnalazione->notificaApertaDa = $request->boolean('notifica_aperta_da');
208
+      $categoriaSicurezzaValue = $request->input('categoria_sicurezza');
209
+      if ($categoriaSicurezzaValue !== null && $categoriaSicurezzaValue !== '') {
210
+        $tipoConfig = ConfiguraSegnalazione::where('gruppo', 'Tipo')
211
+          ->where('value', $segnalazione->tipo)
212
+          ->first();
213
+        $segnalazione->categoria_sicurezza = $tipoConfig
214
+          ? [$tipoConfig->id => $categoriaSicurezzaValue]
215
+          : null;
216
+      }
217
+      $segnalazione->save();
218
+
219
+      // creo la prima annotazione con il testo della segnalazione
220
+      $annotazione = new Annotazione();
221
+      $annotazione->text = $request->descrizione;
222
+      $annotazione->segnalazione_id = $segnalazione->id;
223
+      $annotazione->riservata = false;
224
+      $annotazione->user_id = Auth::user()->id;
225
+      $annotazione->riferimento = null;
226
+      $annotazione->email = null;
227
+      $annotazione->telefono = null;
228
+      $annotazione->save();
229
+
230
+
231
+      // Se è stato caricato un file, crea l'allegato usando il sistema esistente
232
+      if ($request->hasFile('allegato')) {
233
+        // Assicurati che la directory esista
234
+        $directory = storage_path('app/public/allegatiAnnotazioni/' . $segnalazione->id);
235
+        if (!\Illuminate\Support\Facades\File::exists($directory)) {
236
+          \Illuminate\Support\Facades\File::makeDirectory($directory, 0755, true);
237
+        }
238
+
239
+        foreach ($request->file('allegato') as $file) {
240
+          if (!$file) {
241
+            continue;
242
+          }
243
+          $filename = time() . '_' . $file->getClientOriginalName();
244
+          $path = $file->storeAs($segnalazione->id, $filename, 'allegatiAnnotazioni');
245
+
246
+          $allegato = new AllegatoAnnotazione();
247
+          $allegato->annotazione()->associate($annotazione);
248
+          $allegato->nome = $file->getClientOriginalName();
249
+          $allegato->path_file = $path;
250
+          $allegato->save();
251
+        }
252
+      }
253
+
254
+    return redirect()->route('admin.segnalazione.show', ['id' => $segnalazione->id])->with('success', 'Segnalazione creata con successo');
255
+  }
256
+
257
+  public function admin_show(Request $request, $id)
258
+  {
259
+    $segnalazione = Segnalazione::with([
260
+      'azienda',
261
+      'user',
262
+      'assegnataA',
263
+      'ordineLavoro',
264
+      'contrattoServizio.contrattoTemplate',
265
+      'contrattoServizioTodo',
266
+      'prodotto',
267
+      'acquistoServizio.prodotto',
268
+      'acquistoServizios.prodotto',
269
+      'todos.completatoDa',
270
+      'annotazioni.allegatiAnnotazione',
271
+      'annotazioni.user',
272
+      ])->find($id);
273
+      if(!$segnalazione) return redirect()->route('admin.segnalazione.index')->with('error', 'Segnalazione non trovata');
274
+
275
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
276
+
277
+      // Recupera gli stati disponibili per il dropdown (solo se l'utente ha i permessi)
278
+      $statiDisponibili = [];
279
+      if(Auth::user()->can('edit-segnalazione')){
280
+        $statiDisponibili = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
281
+        ->orderBy('order')
282
+        ->get();
283
+      }
284
+
285
+      // $costi = \App\Models\Costi::where('segnalazione_id', $segnalazione->id)->get();
286
+      // $tempoImpiegato = \App\Models\TempoImpiegato::where('segnalazione_id', $segnalazione->id)->get();
287
+      return view('segnalazione.admin.show', [
288
+        'segnalazione' => $segnalazione,
289
+        'statiDisponibili' => $statiDisponibili,
290
+        // 'costi' => $costi,
291
+        // 'tempoImpiegato' => $tempoImpiegato,
292
+      ]);
293
+    }
294
+
295
+    public function admin_toggle_todo(Request $request, $id, $todoId)
296
+    {
297
+      $segnalazione = Segnalazione::findOrFail($id);
298
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
299
+
300
+      $todo = SegnalazioneTodo::where('segnalazione_id', $segnalazione->id)->findOrFail($todoId);
301
+
302
+      if ($todo->is_completato) {
303
+        $todo->markNonCompletato();
304
+      } else {
305
+        $todo->markCompletato(Auth::id());
306
+      }
307
+
308
+      $todo->load('completatoDa');
309
+      $done = $segnalazione->todos()->whereNotNull('completato_at')->count();
310
+      $total = $segnalazione->todos()->count();
311
+
312
+      if ($request->expectsJson() || $request->ajax()) {
313
+        return response()->json([
314
+          'success' => true,
315
+          'completato' => $todo->is_completato,
316
+          'completato_at' => $todo->completato_at?->format('d/m/Y H:i'),
317
+          'completato_da' => $todo->completatoDa?->full_name,
318
+          'todos_done' => $done,
319
+          'todos_total' => $total,
320
+        ]);
321
+      }
322
+
323
+      return redirect()->route('admin.segnalazione.show', $segnalazione->id)
324
+        ->with('success', 'Checklist aggiornata.');
325
+    }
326
+
327
+    public function admin_update_todo_note(Request $request, $id, $todoId)
328
+    {
329
+      $segnalazione = Segnalazione::findOrFail($id);
330
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
331
+
332
+      $validated = $request->validate([
333
+        'note' => 'nullable|string|max:5000',
334
+      ]);
335
+
336
+      $todo = SegnalazioneTodo::where('segnalazione_id', $segnalazione->id)->findOrFail($todoId);
337
+      $todo->setNote((string) ($validated['note'] ?? ''));
338
+
339
+      if ($request->expectsJson() || $request->ajax()) {
340
+        return response()->json([
341
+          'success' => true,
342
+          'note' => $todo->note,
343
+        ]);
344
+      }
345
+
346
+      return redirect()->route('admin.segnalazione.show', $segnalazione->id)
347
+        ->with('success', 'Nota task aggiornata.');
348
+    }
349
+
350
+    public function admin_store_todo(Request $request, $id)
351
+    {
352
+      $segnalazione = Segnalazione::findOrFail($id);
353
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
354
+
355
+      $validated = $request->validate([
356
+        'titolo' => 'required|string|max:255',
357
+        'descrizione' => 'nullable|string|max:2000',
358
+        'obbligatorio' => 'sometimes|boolean',
359
+        'public' => 'sometimes|boolean',
360
+      ]);
361
+
362
+      $maxOrdine = (int) $segnalazione->todos()->max('ordine');
363
+      $todo = new SegnalazioneTodo();
364
+      $todo->segnalazione_id = $segnalazione->id;
365
+      $todo->titolo = trim($validated['titolo']);
366
+      $todo->descrizione = isset($validated['descrizione']) && trim((string) $validated['descrizione']) !== ''
367
+        ? trim((string) $validated['descrizione'])
368
+        : null;
369
+      $todo->obbligatorio = $request->has('obbligatorio')
370
+        ? $request->boolean('obbligatorio')
371
+        : false;
372
+      $todo->public = $request->has('public')
373
+        ? $request->boolean('public')
374
+        : true;
375
+      $todo->ordine = $maxOrdine + 1;
376
+      $todo->save();
377
+
378
+      return $this->todoJsonResponse($segnalazione, $todo, created: true);
379
+    }
380
+
381
+    public function admin_update_todo(Request $request, $id, $todoId)
382
+    {
383
+      $segnalazione = Segnalazione::findOrFail($id);
384
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
385
+
386
+      $validated = $request->validate([
387
+        'titolo' => 'required|string|max:255',
388
+        'descrizione' => 'nullable|string|max:2000',
389
+        'obbligatorio' => 'sometimes|boolean',
390
+        'public' => 'sometimes|boolean',
391
+      ]);
392
+
393
+      $todo = SegnalazioneTodo::where('segnalazione_id', $segnalazione->id)->findOrFail($todoId);
394
+      $todo->titolo = trim($validated['titolo']);
395
+      $todo->descrizione = isset($validated['descrizione']) && trim((string) $validated['descrizione']) !== ''
396
+        ? trim((string) $validated['descrizione'])
397
+        : null;
398
+      if ($request->has('obbligatorio')) {
399
+        $todo->obbligatorio = $request->boolean('obbligatorio');
400
+      }
401
+      if ($request->has('public')) {
402
+        $todo->public = $request->boolean('public');
403
+      }
404
+      $todo->save();
405
+
406
+      return $this->todoJsonResponse($segnalazione, $todo);
407
+    }
408
+
409
+    public function admin_destroy_todo(Request $request, $id, $todoId)
410
+    {
411
+      $segnalazione = Segnalazione::findOrFail($id);
412
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
413
+
414
+      $todo = SegnalazioneTodo::where('segnalazione_id', $segnalazione->id)->findOrFail($todoId);
415
+      $todo->delete();
416
+
417
+      $done = $segnalazione->todos()->whereNotNull('completato_at')->count();
418
+      $total = $segnalazione->todos()->count();
419
+
420
+      if ($request->expectsJson() || $request->ajax()) {
421
+        return response()->json([
422
+          'success' => true,
423
+          'todos_done' => $done,
424
+          'todos_total' => $total,
425
+        ]);
426
+      }
427
+
428
+      return redirect()->route('admin.segnalazione.show', $segnalazione->id)
429
+        ->with('success', 'Task eliminato.');
430
+    }
431
+
432
+    private function todoJsonResponse(Segnalazione $segnalazione, SegnalazioneTodo $todo, bool $created = false)
433
+    {
434
+      $done = $segnalazione->todos()->whereNotNull('completato_at')->count();
435
+      $total = $segnalazione->todos()->count();
436
+
437
+      return response()->json([
438
+        'success' => true,
439
+        'created' => $created,
440
+        'todo' => [
441
+          'id' => $todo->id,
442
+          'titolo' => $todo->titolo,
443
+          'descrizione' => $todo->descrizione,
444
+          'obbligatorio' => (bool) $todo->obbligatorio,
445
+          'public' => (bool) $todo->public,
446
+          'note' => $todo->note,
447
+          'completato' => $todo->is_completato,
448
+          'completato_at' => $todo->completato_at?->format('d/m/Y H:i'),
449
+          'completato_da' => $todo->completatoDa?->full_name,
450
+          'toggle_url' => route('admin.segnalazione.todo.toggle', [$segnalazione->id, $todo->id]),
451
+          'note_url' => route('admin.segnalazione.todo.note', [$segnalazione->id, $todo->id]),
452
+          'update_url' => route('admin.segnalazione.todo.update', [$segnalazione->id, $todo->id]),
453
+          'delete_url' => route('admin.segnalazione.todo.destroy', [$segnalazione->id, $todo->id]),
454
+        ],
455
+        'todos_done' => $done,
456
+        'todos_total' => $total,
457
+      ]);
458
+    }
459
+
460
+    public function admin_delete(Request $request, $id)
461
+    {
462
+      $segnalazione = Segnalazione::find($id);
463
+      if (!$segnalazione) {
464
+        return redirect()->route('admin.segnalazione.index')->with('error', 'Segnalazione non trovata');
465
+      }
466
+
467
+      $segnalazione->delete();
468
+
469
+      return redirect()->route('admin.segnalazione.index')->with('success', 'Segnalazione eliminata con successo');
470
+    }
471
+
472
+    public function admin_pdf(Request $request, $id, SegnalazioneTicketPdfService $pdfService)
473
+    {
474
+      $segnalazione = Segnalazione::find($id);
475
+
476
+      if (!$segnalazione) {
477
+        return redirect()->route('admin.segnalazione.index')->with('error', 'Segnalazione non trovata');
478
+      }
479
+
480
+      abort_unless(Segnalazione::authUserMayViewSegnalazione(Auth::user(), $segnalazione->assegnata_a_id, $segnalazione->user_id), 403);
481
+
482
+      $destinatario = $pdfService->normalizeDestinatario($request->query('destinatario'));
483
+      // Vista “cliente” (solo view-own): solo PDF pubblico, senza note interne
484
+      $authUser = Auth::user();
485
+      if (
486
+        $authUser
487
+        && $authUser->can('view-own-segnalazione')
488
+        && ! $authUser->can('view-segnalazione')
489
+        && ! $authUser->can('view-segnalazione-assegnate')
490
+        && ! $authUser->can('edit-segnalazione')
491
+      ) {
492
+        $destinatario = SegnalazioneTicketPdfService::DEST_CLIENTE;
493
+      }
494
+
495
+      $filename = $pdfService->filename($segnalazione, $destinatario);
496
+
497
+      return response()->stream(function () use ($pdfService, $segnalazione, $destinatario) {
498
+        echo $pdfService->generateBinary($segnalazione, $destinatario);
499
+      }, 200, [
500
+        'Content-Type' => 'application/pdf',
501
+        'Content-Disposition' => 'inline; filename="' . $filename . '"',
502
+      ]);
503
+    }
504
+
505
+    public function admin_list(Request $request){
506
+          $input = $request->all();
507
+          $list = array();
508
+
509
+          if($request->has('initialValue') && $input['initialValue'] == "true"){
510
+            $val = trim(str_replace('"', '', $input['value']));
511
+            $entity = Segnalazione::find($val);
512
+            return response()->json(['id' => $entity->id, 'text' => $entity->getLabel()]);
513
+
514
+          }else{
515
+            $page = $input['page'];
516
+            $resultCount = 25;
517
+            $offset = ($page - 1) * $resultCount;
518
+              $entitys = Segnalazione::where(function($q)use($input){
519
+                $q->where('titolo', 'LIKE', '%'.$input['term'].'%')
520
+                ->orWhere('id', 'LIKE', $input['term'].'%');
521
+              })
522
+              ->orderBy('titolo', 'ASC');
523
+
524
+
525
+            foreach ($entitys->skip($offset)->take($resultCount)->get() as $e) {
526
+              array_push($list, array('id' => $e->id, 'text' => $e->getLabel()));
527
+            }
528
+
529
+            $count = $entitys->count();
530
+            $endCount = $offset + $resultCount;
531
+            $morePages = $count > $endCount;
532
+
533
+            $results = array(
534
+              "results" => $list,
535
+              "pagination" => array(
536
+                "more" => $morePages
537
+                )
538
+              );
539
+
540
+            }
541
+            return response()->json($results);
542
+          }
543
+
544
+    // END ADMIN
545
+
546
+    public function nuova($token, Request $request){
547
+      $reparto = Reparto::where('token', $token)->first();
548
+      if(!$reparto){
549
+        return redirect()->route('reparto.dashboard')->with('error', 'Reparto non trovato');
550
+      }
551
+      return view('segnalazione.nuova', ['reparto' => $reparto]);
552
+    }
553
+
554
+    public function reparto($token, Request $request)
555
+    {
556
+      $reparto = Reparto::where('token', $token)->first();
557
+
558
+      if (!$reparto) {
559
+        return redirect()->route('reparto.dashboard')->with('error', 'Reparto non trovato');
560
+      }
561
+
562
+      $segnalazioni = Segnalazione::with(['reparto', 'luogo'])
563
+      ->where('reparto_id', $reparto->id)
564
+      ->orderBy('created_at', 'desc')
565
+      ->get();
566
+
567
+      // Prepara i dati per i filtri
568
+      $luoghiUnici = $segnalazioni->pluck('luogo')->filter(function($luogo) {
569
+        return $luogo !== null;
570
+      })->unique('id')->sortBy('label')->values();
571
+      $statiUnici = $segnalazioni->pluck('stato')->filter()->unique()->sort()->values();
572
+      $prioritaUniche = $segnalazioni->pluck('priorita')->filter()->unique()->sort()->values();
573
+
574
+      // Recupera le configurazioni per stati e priorità
575
+      $configStati = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
576
+      ->whereIn('value', $statiUnici->toArray())
577
+      ->orderBy('order')
578
+      ->get()
579
+      ->keyBy('value');
580
+
581
+      $configPriorita = \App\Models\ConfiguraSegnalazione::where('gruppo', 'Priorità')
582
+      ->whereIn('value', $prioritaUniche->toArray())
583
+      ->orderBy('order')
584
+      ->get()
585
+      ->keyBy('value');
586
+
587
+      return view('segnalazione.reparto', [
588
+        'reparto' => $reparto,
589
+        'segnalazioni' => $segnalazioni,
590
+        'luoghiUnici' => $luoghiUnici,
591
+        'statiUnici' => $statiUnici,
592
+        'prioritaUniche' => $prioritaUniche,
593
+        'configStati' => $configStati,
594
+        'configPriorita' => $configPriorita
595
+      ]);
596
+    }
597
+
598
+    public function crea(Request $request)
599
+    {
600
+      // Validazione base
601
+      $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
602
+        'token' => 'required|exists:reparto,token',
603
+        'luogo_id' => 'required|exists:luogo,id',
604
+        'titolo' => 'required|string|max:255',
605
+        'descrizione' => 'required|string',
606
+        'allegato' => 'nullable|array',
607
+        'allegato.*' => 'file|max:10240', // max 10MB per file
608
+      ]);
609
+
610
+      if ($validator->fails()) {
611
+        return redirect()->back()
612
+        ->withErrors($validator)
613
+        ->withInput();
614
+      }
615
+
616
+      // Crea la segnalazione
617
+      $segnalazione = new Segnalazione();
618
+      $segnalazione->fill($request->all());
619
+
620
+      $reparto = Reparto::where('token', $request->token)->first();
621
+      if($reparto) {
622
+        $segnalazione->reparto_id = $reparto->id;
623
+      }
624
+
625
+      $segnalazione->save();
626
+
627
+      // creo la prima annotazione con il testo della segnalazione
628
+      $annotazione = new Annotazione();
629
+      $annotazione->text = $segnalazione->descrizione;
630
+      $annotazione->segnalazione_id = $segnalazione->id;
631
+      $annotazione->user_id = null;
632
+      $annotazione->riservata = false;
633
+      $annotazione->riferimento = $segnalazione->riferimento;
634
+      $annotazione->email = $segnalazione->email;
635
+      $annotazione->telefono = $segnalazione->telefono;
636
+      $annotazione->save();
637
+
638
+
639
+      // Se è stato caricato un file, crea l'allegato usando il sistema esistente
640
+      if ($request->hasFile('allegato')){
641
+        // Assicurati che la directory esista
642
+        $directory = storage_path('app/public/allegatiAnnotazioni/' . $segnalazione->id);
643
+        if (!\Illuminate\Support\Facades\File::exists($directory)) {
644
+          \Illuminate\Support\Facades\File::makeDirectory($directory, 0755, true);
645
+        }
646
+
647
+        foreach ($request->file('allegato') as $file) {
648
+          if (!$file) {
649
+            continue;
650
+          }
651
+          $filename = time() . '_' . $file->getClientOriginalName();
652
+          $path = $file->storeAs($segnalazione->id, $filename, 'allegatiAnnotazioni');
653
+
654
+          $allegato = new AllegatoAnnotazione();
655
+          $allegato->annotazione()->associate($annotazione);
656
+          $allegato->nome = $file->getClientOriginalName();
657
+          $allegato->path_file = $path;
658
+          $allegato->save();
659
+        }
660
+      }
661
+
662
+      return redirect()->route('segnalazione.reparto', ['token' => $segnalazione->reparto->token])->with('success', 'Segnalazione creata con successo');
663
+    }
664
+
665
+    public function show(Request $request)
666
+    {
667
+      $segnalazione = Segnalazione::with([
668
+        'reparto',
669
+        'luogo',
670
+        'annotazioni.allegatiAnnotazione',
671
+        'annotazioni.user',
672
+        ])->where('token', $request->token)->first();
673
+        if(!$segnalazione) return redirect()->route('welcome')->with('error', 'Segnalazione non trovata');
674
+
675
+        // Recupera gli stati disponibili per il dropdown (solo se l'utente ha i permessi)
676
+        $statiDisponibili = [];
677
+        if(Auth::check()){
678
+          if(Auth::user()->can('edit-segnalazione')){
679
+            $statiDisponibili = \App\Models\ConfiguraSegnalazione::where('gruppo', 'stato')
680
+            ->orderBy('order')
681
+            ->get();
682
+          }
683
+        }
684
+
685
+        return view('segnalazione.show', [
686
+          'segnalazione' => $segnalazione,
687
+          'statiDisponibili' => $statiDisponibili,
688
+        ]);
689
+      }
690
+
691
+
692
+
693
+
694
+
695
+
696
+
697
+
698
+
699
+      // public function esportaPdf(Request $request, $id)
700
+      // {
701
+      //   $segnalazione = Segnalazione::with([
702
+      //     'reparto',
703
+      //     'luogo',
704
+      //     'annotazioni.allegatiAnnotazione',
705
+      //     ])->find($id);
706
+
707
+      //     if(!$segnalazione) {
708
+      //       return redirect()->route('segnalazione.index')->with('error', 'Segnalazione non trovata');
709
+      //     }
710
+
711
+      //     // Recupera le configurazioni
712
+      //     $configStato = $segnalazione->getConfiguraStato();
713
+      //     $configPriorita = $segnalazione->getConfiguraPriorita();
714
+      //     $configTipo = $segnalazione->getConfiguraTipo();
715
+
716
+      //     // Crea timeline eventi (escludendo tempo e costi)
717
+      //     $eventi = collect();
718
+
719
+      //     // Evento: Creazione segnalazione
720
+      //     $eventi->push([
721
+      //       'tipo' => 'creazione',
722
+      //       'data' => $segnalazione->created_at,
723
+      //       'titolo' => 'Segnalazione creata',
724
+      //       'descrizione' => $segnalazione->descrizione,
725
+      //       'dati' => [
726
+      //         'titolo' => $segnalazione->titolo,
727
+      //         'priorita' => $configPriorita ? $configPriorita->label : null,
728
+      //         'tipo' => $configTipo ? $configTipo->label : null,
729
+      //         'luogo' => $segnalazione->luogo ? $segnalazione->luogo->label : null,
730
+      //         'reparto' => $segnalazione->reparto ? $segnalazione->reparto->label : null,
731
+      //         'segnalatore' => $segnalazione->segnalatore ? $segnalazione->segnalatore->fullName() : null,
732
+      //         ]
733
+      //       ]);
734
+
735
+      //       // Eventi: Annotazioni
736
+      //       foreach($segnalazione->annotazioni as $annotazione) {
737
+      //         $allegatiAnnotazione = [];
738
+      //         foreach($annotazione->allegatiAnnotazione as $allegato) {
739
+      //           $allegatiAnnotazione[] = [
740
+      //             'id' => $allegato->id,
741
+      //             'nome' => $allegato->nome,
742
+      //             'descrizione' => $allegato->descrizione,
743
+      //             'path_file' => $allegato->path_file,
744
+      //             'isImage' => $allegato->isImage(),
745
+      //             'url' => $allegato->isImage() ? $allegato->getUrl() : null,
746
+      //           ];
747
+      //         }
748
+
749
+      //         $eventi->push([
750
+      //           'tipo' => 'annotazione',
751
+      //           'data' => $annotazione->created_at,
752
+      //           'titolo' => 'Annotazione',
753
+      //           'descrizione' => $annotazione->annotazione,
754
+      //           'dati' => [
755
+      //             'autore' => $annotazione->autore ? $annotazione->autore->fullName() : null,
756
+      //             'allegati' => $allegatiAnnotazione,
757
+      //             ]
758
+      //           ]);
759
+      //         }
760
+
761
+      //         // Eventi: Allegati
762
+      //         foreach($segnalazione->allegatiSegnalazione as $allegato) {
763
+      //           $eventi->push([
764
+      //             'tipo' => 'allegato',
765
+      //             'data' => $allegato->created_at,
766
+      //             'titolo' => 'Allegato caricato',
767
+      //             'descrizione' => $allegato->nome ?? 'Allegato',
768
+      //             'dati' => [
769
+      //               'nome' => $allegato->nome,
770
+      //               'descrizione' => $allegato->descrizione,
771
+      //               'path_file' => $allegato->path_file,
772
+      //               'isImage' => $allegato->isImage(),
773
+      //               'url' => $allegato->isImage() ? $allegato->getUrl() : null,
774
+      //               ]
775
+      //             ]);
776
+      //           }
777
+
778
+      //           // Ordina per data (dal più vecchio al più recente per il PDF)
779
+      //           $eventi = $eventi->sortBy('data')->values();
780
+
781
+      //           // Renderizza la view in HTML
782
+      //           $html = view('segnalazione.pdf', [
783
+      //             'segnalazione' => $segnalazione,
784
+      //             'configStato' => $configStato,
785
+      //             'configPriorita' => $configPriorita,
786
+      //             'configTipo' => $configTipo,
787
+      //             'eventi' => $eventi,
788
+      //             ])->render();
789
+
790
+      //             // DomPDF: mantieni le immagini come base64 inline (più affidabile)
791
+      //             // Le immagini sono già ottimizzate nel template Blade
792
+      //             // Non serve convertire in file temporanei - DomPDF dovrebbe gestire il base64
793
+      //             $tempImageFiles = []; // Vuoto - non usiamo file temporanei
794
+
795
+      //             // Pulisci l'HTML da elementi problematici
796
+      //             // Rimuovi il CSS di testata_pdf che contiene "Encode Sans Condensed"
797
+      //             $html = preg_replace('/<style[^>]*>.*?Encode Sans Condensed.*?<\/style>/is', '', $html);
798
+
799
+      //             // Rimuovi link a font esterni
800
+      //             $html = preg_replace('/<link[^>]*fonts\.googleapis[^>]*>/i', '', $html);
801
+
802
+      //             // Rimuovi margin: 1cm dal CSS rimanente
803
+      //             $html = preg_replace('/(margin:\s*)1cm/i', '$10', $html);
804
+
805
+      //             // Pulisci l'HTML da caratteri problematici
806
+      //             $html = mb_convert_encoding($html, 'UTF-8', 'UTF-8');
807
+      //             $html = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $html);
808
+
809
+      //             // Rimuovi l'attributo data-mpdf-image-path che non serve più
810
+      //             $html = preg_replace('/\s+data-mpdf-image-path="[^"]+"/i', '', $html);
811
+
812
+      //             // Assicurati che l'HTML abbia una struttura valida
813
+      //             if (!preg_match('/<html/i', $html)) {
814
+      //               $html = '<html><head></head><body>' . $html . '</body></html>';
815
+      //             }
816
+
817
+      //             $filename = 'segnalazione_' . Str::slug($segnalazione->titolo) . '_' . $segnalazione->id . '.pdf';
818
+
819
+      //             // Genera il PDF con DomPDF
820
+      //             // Usa base64 inline - DomPDF dovrebbe gestirlo nativamente
821
+      //             try {
822
+      //               // Conta le immagini base64 nell'HTML per debug
823
+      //               $imageCount = preg_match_all('/data:image\/[^;]+;base64,[^"]+/i', $html);
824
+      //               Log::info('Generazione PDF - Immagini base64 trovate nell\'HTML: ' . $imageCount);
825
+
826
+      //               $pdf = Pdf::loadHTML($html);
827
+      //               $pdf->setPaper('a4', 'portrait');
828
+
829
+      //               // Configurazione DomPDF per gestire meglio le immagini base64
830
+      //               $pdf->setOption('isRemoteEnabled', false); // Non serve per base64 inline
831
+      //               $pdf->setOption('isHtml5ParserEnabled', true);
832
+      //               $pdf->setOption('isPhpEnabled', true); // Abilitato per numerazione pagine
833
+      //               $pdf->setOption('isFontSubsettingEnabled', true); // Ottimizza font
834
+      //               $pdf->setOption('defaultFont', 'DejaVu Sans'); // Usa font di sistema
835
+
836
+      //               // Abilita logging errori DomPDF
837
+      //               $pdf->setOption('logOutputFile', storage_path('logs/dompdf.log'));
838
+      //               $pdf->setOption('enableFontSubsetting', true);
839
+
840
+      //               return response()->streamDownload(function() use ($pdf) {
841
+      //                 echo $pdf->output();
842
+      //               }, $filename, [
843
+      //                 'Content-Type' => 'application/pdf',
844
+      //               ]);
845
+      //             } catch (\Exception $e) {
846
+      //               // Log errore per debug
847
+      //               Log::error('Errore generazione PDF DomPDF: ' . $e->getMessage());
848
+      //               Log::error('Stack trace: ' . $e->getTraceAsString());
849
+
850
+      //               throw new \Exception('Errore nella generazione del PDF: ' . $e->getMessage());
851
+      //             }
852
+      //           }
853
+
854
+      //           public function previewPdf(Request $request, $id)
855
+      //           {
856
+      //             $segnalazione = Segnalazione::with([
857
+      //               'reparto',
858
+      //               'luogo',
859
+      //               'segnalatore',
860
+      //               'allegatiSegnalazione',
861
+      //               'annotazioni.allegatiAnnotazione',
862
+      //               'annotazioni.autore'
863
+      //               ])->find($id);
864
+
865
+      //               if(!$segnalazione) {
866
+      //                 return redirect()->route('segnalazione.index')->with('error', 'Segnalazione non trovata');
867
+      //               }
868
+
869
+      //               // Recupera le configurazioni
870
+      //               $configStato = $segnalazione->getConfiguraStato();
871
+      //               $configPriorita = $segnalazione->getConfiguraPriorita();
872
+      //               $configTipo = $segnalazione->getConfiguraTipo();
873
+
874
+      //               // Crea timeline eventi (escludendo tempo e costi)
875
+      //               $eventi = collect();
876
+
877
+      //               // Evento: Creazione segnalazione
878
+      //               $eventi->push([
879
+      //                 'tipo' => 'creazione',
880
+      //                 'data' => $segnalazione->created_at,
881
+      //                 'titolo' => 'Segnalazione creata',
882
+      //                 'descrizione' => $segnalazione->descrizione,
883
+      //                 'dati' => [
884
+      //                   'titolo' => $segnalazione->titolo,
885
+      //                   'priorita' => $configPriorita ? $configPriorita->label : null,
886
+      //                   'tipo' => $configTipo ? $configTipo->label : null,
887
+      //                   'luogo' => $segnalazione->luogo ? $segnalazione->luogo->label : null,
888
+      //                   'reparto' => $segnalazione->reparto ? $segnalazione->reparto->label : null,
889
+      //                   'segnalatore' => $segnalazione->segnalatore ? $segnalazione->segnalatore->fullName() : null,
890
+      //                   ]
891
+      //                 ]);
892
+
893
+      //                 // Eventi: Annotazioni
894
+      //                 foreach($segnalazione->annotazioni as $annotazione) {
895
+      //                   $allegatiAnnotazione = [];
896
+      //                   foreach($annotazione->allegatiAnnotazione as $allegato) {
897
+      //                     $allegatiAnnotazione[] = [
898
+      //                       'id' => $allegato->id,
899
+      //                       'nome' => $allegato->nome,
900
+      //                       'descrizione' => $allegato->descrizione,
901
+      //                       'path_file' => $allegato->path_file,
902
+      //                       'isImage' => $allegato->isImage(),
903
+      //                       'url' => $allegato->isImage() ? $allegato->getUrl() : null,
904
+      //                     ];
905
+      //                   }
906
+
907
+      //                   $eventi->push([
908
+      //                     'tipo' => 'annotazione',
909
+      //                     'data' => $annotazione->created_at,
910
+      //                     'titolo' => 'Annotazione',
911
+      //                     'descrizione' => $annotazione->annotazione,
912
+      //                     'dati' => [
913
+      //                       'autore' => $annotazione->autore ? $annotazione->autore->fullName() : null,
914
+      //                       'allegati' => $allegatiAnnotazione,
915
+      //                       ]
916
+      //                     ]);
917
+      //                   }
918
+
919
+      //                   // Eventi: Allegati
920
+      //                   foreach($segnalazione->allegatiSegnalazione as $allegato) {
921
+      //                     $eventi->push([
922
+      //                       'tipo' => 'allegato',
923
+      //                       'data' => $allegato->created_at,
924
+      //                       'titolo' => 'Allegato caricato',
925
+      //                       'descrizione' => $allegato->nome ?? 'Allegato',
926
+      //                       'dati' => [
927
+      //                         'nome' => $allegato->nome,
928
+      //                         'descrizione' => $allegato->descrizione,
929
+      //                         'path_file' => $allegato->path_file,
930
+      //                         'isImage' => $allegato->isImage(),
931
+      //                         'url' => $allegato->isImage() ? $allegato->getUrl() : null,
932
+      //                         ]
933
+      //                       ]);
934
+      //                     }
935
+
936
+      //                     // Ordina per data (dal più vecchio al più recente per il PDF)
937
+      //                     $eventi = $eventi->sortBy('data')->values();
938
+
939
+      //                     return view('segnalazione.pdf', [
940
+      //                       'segnalazione' => $segnalazione,
941
+      //                       'configStato' => $configStato,
942
+      //                       'configPriorita' => $configPriorita,
943
+      //                       'configTipo' => $configTipo,
944
+      //                       'eventi' => $eventi,
945
+      //                     ]);
946
+      //                   }
947
+                      }

+ 41
- 0
app/Http/Controllers/ShortUrlController.php 파일 보기

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use App\Models\AcquistoServizio;
6
+use Illuminate\Http\RedirectResponse;
7
+
8
+class ShortUrlController extends Controller
9
+{
10
+    public function redirect(string $code): RedirectResponse
11
+    {
12
+        $code = trim($code);
13
+        abort_if($code === '' || strlen($code) > 32, 404);
14
+
15
+        $acquisto = AcquistoServizio::query()
16
+            ->where('meta->firma_consegna_short', $code)
17
+            ->first();
18
+
19
+        if ($acquisto) {
20
+            $acquisto->ensureFirmaConsegnaToken();
21
+            $url = $acquisto->firma_consegna_url;
22
+            abort_unless($url, 404);
23
+
24
+            return redirect()->away($url);
25
+        }
26
+
27
+        $acquisto = AcquistoServizio::query()
28
+            ->where('meta->firma_riconsegna_short', $code)
29
+            ->first();
30
+
31
+        if ($acquisto) {
32
+            $acquisto->ensureFirmaRiconsegnaToken();
33
+            $url = $acquisto->firma_riconsegna_url;
34
+            abort_unless($url, 404);
35
+
36
+            return redirect()->away($url);
37
+        }
38
+
39
+        abort(404);
40
+    }
41
+}

+ 61
- 0
app/Http/Controllers/TempoImpiegatoController.php 파일 보기

@@ -0,0 +1,61 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Http\Request;
6
+use Auth;
7
+use Session;
8
+use App\DataTables\TempoImpiegatoDataTable;
9
+use App\DataTables\TempoImpiegatoDataTableEditor;
10
+use Illuminate\Routing\Controllers\Middleware;
11
+use Illuminate\Routing\Controllers\HasMiddleware;
12
+use App\Models\Segnalazione;
13
+
14
+class TempoImpiegatoController extends Controller implements HasMiddleware
15
+{
16
+    public static $permission_group = "Tempo Impiegato";
17
+    public static $permissions = [
18
+        'view-tempo-impiegato' => 'Vedi',
19
+        'create-tempo-impiegato' => 'Crea',
20
+        'edit-tempo-impiegato' => 'Modifica',
21
+        'remove-tempo-impiegato' => 'Rimuovi'
22
+    ];
23
+
24
+    public static function middleware(): array
25
+    {
26
+        return [
27
+            new Middleware('permission:view-tempo-impiegato', only: ['admin_index']),
28
+            new Middleware('permission:create-tempo-impiegato|edit-tempo-impiegato|remove-tempo-impiegato', only: ['admin_store']),
29
+        ];
30
+    }
31
+    public function admin_index(TempoImpiegatoDataTable $dataTable, Request $request)
32
+    {
33
+      if($request->has('segnalazione_id')){
34
+        $segnalazione = Segnalazione::find($request->segnalazione_id);
35
+        $params = [
36
+          'segnalazione' => $segnalazione
37
+        ];
38
+        return $dataTable->with($params)->render('tempo_impiegato.segnalazione', $params);
39
+      }
40
+        return $dataTable->render('tempo_impiegato.index');
41
+    }
42
+
43
+    public function admin_store(TempoImpiegatoDataTableEditor $editor)
44
+    {
45
+        $request = request();
46
+        $input = $request->all();
47
+        // dd($input);
48
+        if($request->has('action')){
49
+            switch($input['action']){
50
+                case 'create':
51
+                break;
52
+                case 'edit':
53
+                break;
54
+                case 'remove':
55
+                break;
56
+            }
57
+        }
58
+        return $editor->process(request());
59
+    }
60
+
61
+}

+ 453
- 0
app/Http/Controllers/UserController.php 파일 보기

@@ -0,0 +1,453 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Routing\Controllers\Middleware;
6
+use Illuminate\Routing\Controllers\HasMiddleware;
7
+
8
+use Illuminate\Http\Request;
9
+use App\Models\User;
10
+use App\DataTables\UserDataTableEditor;
11
+use App\DataTables\UserDataTable;
12
+use Illuminate\Support\Facades\Auth;
13
+use Notification;
14
+use Session;
15
+use PDF;
16
+use Illuminate\Support\Facades\Storage;
17
+use Illuminate\Support\Str;
18
+use Carbon\Carbon;
19
+use Illuminate\Support\Facades\Hash;
20
+use Illuminate\Support\Facades\Mail;
21
+use App\Mail\InvitoFamiglia;
22
+use Intervention\Image\ImageManager;
23
+use Intervention\Image\Drivers\Gd\Driver;
24
+use Illuminate\Support\Facades\Validator;
25
+use App\Models\Role;
26
+
27
+class UserController extends Controller implements HasMiddleware
28
+{
29
+  public static $permission_group = "Utenti";
30
+  public static $permissions = [
31
+    'view-user' => 'Vedi',
32
+    'create-user' => 'Crea',
33
+    'edit-user' => 'Modifica',
34
+    'remove-user' => 'Rimuovi'
35
+  ];
36
+
37
+  public $messages = [
38
+    'nome.required' => 'Devi indicare il nome',
39
+    'cognome.required' => 'Devi indicare il cognome',
40
+    // 'telefono.required' => 'Devi indicare il telefono',
41
+    'email.required' => 'Devi indicare l\'indirizzo email',
42
+    'email.email' => 'L\'indirizzo email indicato non è valido',
43
+    'email.unique' => 'L\'indirizzo email indicato non è valido',
44
+    'password.required' => 'Devi indicare la password',
45
+    'password.confirmed' => 'La password non corriponde a quella di conferma',
46
+    'password.min' => 'La password deve essere composta da almeno 8 caratteri',
47
+  ];
48
+
49
+  public static function middleware(): array
50
+  {
51
+    return [
52
+      new Middleware('permission:view-user', only: ['admin_index', 'admin_scheda']),
53
+      new Middleware('permission:create-user|edit-user|remove-user', only: ['admin_store', 'admin_aggiorna', 'admin_aggiorna_scheda', 'admin_quick_store']),
54
+    ];
55
+  }
56
+
57
+  public function admin_index(UserDataTable $dataTable)
58
+  {
59
+    return $dataTable->render('user.index');
60
+  }
61
+
62
+  public function admin_scheda(\App\DataTables\ContrattoServizioDataTable $dataTable, $id)
63
+  {
64
+    $user = User::with(['comune', 'azienda'])->findOrFail($id);
65
+
66
+    return $dataTable->with(['user' => $user])->render('user.scheda', compact('user'));
67
+  }
68
+
69
+  public function admin_aggiorna_scheda(Request $request, $id)
70
+  {
71
+    if (! Auth::user()->can('edit-user')) {
72
+      abort(403);
73
+    }
74
+
75
+    $user = User::findOrFail($id);
76
+    $validated = $request->validate([
77
+      'nome' => 'nullable|string|max:255',
78
+      'cognome' => 'nullable|string|max:255',
79
+      'email' => 'nullable|email|max:255',
80
+      'telefono' => 'nullable|string|max:255',
81
+      'ragione_sociale' => 'nullable|string|max:255',
82
+      'piva' => 'nullable|string|max:50',
83
+      'cod_fiscale' => 'nullable|string|max:50',
84
+      'indirizzo' => 'nullable|string|max:255',
85
+      'comune_id' => 'nullable|exists:comune,id',
86
+      'note' => 'nullable|string',
87
+      'azienda_id' => 'nullable|exists:users,id',
88
+    ]);
89
+
90
+    foreach ($validated as $key => $value) {
91
+      if ($value === '') {
92
+        $validated[$key] = null;
93
+      }
94
+    }
95
+
96
+    $user->fill($validated);
97
+    $user->save();
98
+
99
+    return redirect()->route('admin.user.scheda', $user->id)->with('success', 'Scheda aggiornata.');
100
+  }
101
+
102
+  public function admin_store(UserDataTableEditor $editor)
103
+  {
104
+    $request = request();
105
+    $input = $request->all();
106
+
107
+    if($request->has('action')){
108
+      switch($input['action']){
109
+        case 'create':
110
+          if(!Auth::user()->can('create-user')) return;
111
+          break;
112
+
113
+          case 'edit':
114
+            if(!Auth::user()->can('edit-user')) return;
115
+            break;
116
+
117
+            case 'remove':
118
+              if(!Auth::user()->can('remove-user')) return;
119
+              break;
120
+            }
121
+          }
122
+          return $editor->process($request);
123
+        }
124
+
125
+
126
+        //////
127
+
128
+
129
+        public function edit_profilo(Request $request, $user_id = null)
130
+        {
131
+          $user = Auth::user();
132
+          if ($user_id !== null && (int) $user_id !== (int) $user->id) {
133
+            abort(403);
134
+          }
135
+
136
+          $user->loadMissing(['comune.provincia', 'azienda']);
137
+
138
+          // Setup 2FA incompleto (secret senza conferma): mostra QR
139
+          $twoFactorSetup = (bool) session('two_factor_setup')
140
+            || (filled($user->two_factor_secret) && blank($user->two_factor_confirmed_at));
141
+
142
+          $showRecoveryCodes = (bool) session('show_recovery_codes');
143
+          $recoveryCodes = [];
144
+          if ($showRecoveryCodes && $user->two_factor_secret) {
145
+            try {
146
+              $recoveryCodes = $user->recoveryCodes();
147
+            } catch (\Throwable $e) {
148
+              $recoveryCodes = [];
149
+            }
150
+          }
151
+
152
+          return view('profile.edit_profile', compact('user', 'twoFactorSetup', 'showRecoveryCodes', 'recoveryCodes'));
153
+        }
154
+
155
+        public function updateProfile(Request $request)
156
+        {
157
+          $user = Auth::user();
158
+
159
+          $validated = $request->validate([
160
+            'nome' => 'required|string|max:255',
161
+            'cognome' => 'nullable|string|max:255',
162
+            'email' => 'required|email|max:255|unique:users,email,'.$user->id,
163
+            'telefono' => 'nullable|string|max:255',
164
+            'cod_fiscale' => 'nullable|string|max:50',
165
+            'indirizzo' => 'nullable|string|max:255',
166
+            'comune_id' => 'nullable|exists:comune,id',
167
+            'note' => 'nullable|string',
168
+          ], $this->messages);
169
+
170
+          foreach ($validated as $key => $value) {
171
+            if ($value === '') {
172
+              $validated[$key] = null;
173
+            }
174
+          }
175
+
176
+          $user->fill($validated);
177
+          $user->save();
178
+
179
+          return redirect()
180
+            ->route('user.edit_profilo', $user->id)
181
+            ->with('success', 'Profilo aggiornato correttamente.');
182
+        }
183
+
184
+        public function updatePassword(Request $request)
185
+        {
186
+          $user = Auth::user();
187
+
188
+          $validated = $request->validate([
189
+            'current_password' => ['required', 'string', 'current_password:web'],
190
+            'password' => ['required', 'string', 'confirmed', \Illuminate\Validation\Rules\Password::default()],
191
+          ], [
192
+            'current_password.current_password' => 'La password attuale non è corretta.',
193
+            'password.confirmed' => 'La conferma password non coincide.',
194
+          ]);
195
+
196
+          $user->forceFill([
197
+            'password' => Hash::make($validated['password']),
198
+          ])->save();
199
+
200
+          return redirect()
201
+            ->route('user.edit_profilo', $user->id)
202
+            ->with('success', 'Password aggiornata correttamente.');
203
+        }
204
+
205
+        public function enableTwoFactor(Request $request, \Laravel\Fortify\Actions\EnableTwoFactorAuthentication $enable)
206
+        {
207
+          $this->assertCurrentPassword($request);
208
+
209
+          $enable($request->user(), force: true);
210
+
211
+          return redirect()
212
+            ->route('user.profilo')
213
+            ->with('two_factor_setup', true)
214
+            ->with('success', 'Scansiona il QR code e conferma con il codice dell’app autenticatore.');
215
+        }
216
+
217
+        public function confirmTwoFactor(Request $request, \Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication $confirm)
218
+        {
219
+          $request->validate([
220
+            'code' => 'required|string',
221
+          ], [
222
+            'code.required' => 'Inserisci il codice generato dall’app autenticatore.',
223
+          ]);
224
+
225
+          try {
226
+            $confirm($request->user(), $request->input('code'));
227
+          } catch (\Illuminate\Validation\ValidationException $e) {
228
+            return redirect()
229
+              ->route('user.profilo')
230
+              ->with('two_factor_setup', true)
231
+              ->withErrors($e->errors());
232
+          }
233
+
234
+          return redirect()
235
+            ->route('user.profilo')
236
+            ->with('show_recovery_codes', true)
237
+            ->with('success', 'Autenticazione a due fattori attivata. Conserva i codici di recupero.');
238
+        }
239
+
240
+        public function disableTwoFactor(Request $request, \Laravel\Fortify\Actions\DisableTwoFactorAuthentication $disable)
241
+        {
242
+          $this->assertCurrentPassword($request);
243
+          $disable($request->user());
244
+
245
+          return redirect()
246
+            ->route('user.profilo')
247
+            ->with('success', 'Autenticazione a due fattori disattivata.');
248
+        }
249
+
250
+        public function regenerateTwoFactorRecoveryCodes(Request $request, \Laravel\Fortify\Actions\GenerateNewRecoveryCodes $generate)
251
+        {
252
+          $this->assertCurrentPassword($request);
253
+          $generate($request->user());
254
+
255
+          return redirect()
256
+            ->route('user.profilo')
257
+            ->with('show_recovery_codes', true)
258
+            ->with('success', 'Nuovi codici di recupero generati.');
259
+        }
260
+
261
+        public function showTwoFactorRecoveryCodes(Request $request)
262
+        {
263
+          $this->assertCurrentPassword($request);
264
+
265
+          return redirect()
266
+            ->route('user.profilo')
267
+            ->with('show_recovery_codes', true);
268
+        }
269
+
270
+        private function assertCurrentPassword(Request $request): void
271
+        {
272
+          $request->validate([
273
+            'password' => 'required|string',
274
+          ], [
275
+            'password.required' => 'Inserisci la password per confermare.',
276
+          ]);
277
+
278
+          if (! Hash::check($request->input('password'), $request->user()->getAuthPassword())) {
279
+            throw \Illuminate\Validation\ValidationException::withMessages([
280
+              'password' => 'La password non è corretta.',
281
+            ]);
282
+          }
283
+
284
+          $request->session()->put('auth.password_confirmed_at', now()->unix());
285
+        }
286
+
287
+        public function upload_foto_profilo(Request $request){
288
+          $original_name = $request->file->getClientOriginalName();
289
+          $extension = pathinfo($original_name, PATHINFO_EXTENSION);
290
+
291
+          $filename = pathinfo(Str::camel($original_name), PATHINFO_FILENAME)."_".time().".".$extension;
292
+
293
+          //store file into temp folder
294
+          $file = $request->file->storeAs('', $filename , 'temp');
295
+          // $evento->path_img = $file;
296
+          // $evento->save();
297
+
298
+          return response()->json(['url' => Storage::disk('temp')->url($file), 'filename' => $file]);
299
+        }
300
+
301
+        public function showCropImage(Request $request){
302
+          return view('profile.cropImage', ['filename' => $request->filename]);
303
+        }
304
+
305
+        public function cropImage(Request $request){
306
+          // $evento = Evento::find($request->evento_id);
307
+          $fileName = explode(".", $request->filename);
308
+          $extension = array_pop($fileName);
309
+          $filename = implode("_", $fileName);
310
+
311
+          // create new image instance
312
+          $manager = new ImageManager(new Driver());
313
+          $image = $manager->read(Storage::disk('temp')->path($request->filename));
314
+          $image->crop($request->cropBoxData['width'], $request->cropBoxData['height'], $request->cropBoxData['x'], $request->cropBoxData['y']);
315
+          $newFileName = $filename.'_'.time().'_cropped.'.$extension;
316
+          Storage::disk('temp')->put($filename.'_'.time().'_cropped.'.$extension, $image->encodeByExtension($extension, quality: 100));
317
+          // $evento->path_img = $newFileName;
318
+          // $evento->save();
319
+
320
+          return response()->json(['url' => Storage::disk('temp')->url($newFileName), 'filename' => $newFileName]);
321
+        }
322
+
323
+        public function admin_list(Request $request){
324
+          $input = $request->all();
325
+          $list = array();
326
+
327
+          if($request->has('initialValue') && $input['initialValue'] == "true"){
328
+            $raw = $input['value'] ?? null;
329
+            if (is_array($raw)) {
330
+              $ids = array_values(array_filter(array_map('intval', $raw)));
331
+            } else {
332
+              $raw = trim(str_replace('"', '', (string) $raw));
333
+              $ids = array_values(array_filter(array_map('intval', preg_split('/[,\s]+/', $raw) ?: [])));
334
+            }
335
+
336
+            if (count($ids) === 0) {
337
+              return response()->json([]);
338
+            }
339
+
340
+            if (count($ids) === 1) {
341
+              $entity = User::find($ids[0]);
342
+              if (! $entity) {
343
+                return response()->json([]);
344
+              }
345
+
346
+              return response()->json(['id' => $entity->id, 'text' => $entity->full_name]);
347
+            }
348
+
349
+            $users = User::whereIn('id', $ids)
350
+              ->soloUtenti()
351
+              ->orderBy('cognome')
352
+              ->orderBy('nome')
353
+              ->get()
354
+              ->map(fn (User $user) => ['id' => $user->id, 'text' => $user->full_name])
355
+              ->values();
356
+
357
+            return response()->json($users);
358
+          }else{
359
+            $page = $input['page'];
360
+            $resultCount = 25;
361
+            $offset = ($page - 1) * $resultCount;
362
+            $entitys = User::where(function($q)use($input){
363
+              $q->where('nome', 'LIKE', '%'.$input['term'].'%')
364
+              ->orWhere('cognome', 'LIKE', '%'.$input['term'].'%')
365
+              ->orWhere('label_gruppo', 'LIKE', '%'.$input['term'].'%');
366
+            })
367
+            ->when($request->boolean('solo_utenti'), fn ($q) => $q->soloUtenti())
368
+            ->when($request->boolean('solo_aziende'), fn ($q) => $q->soloAziende())
369
+            ->orderBy('cognome', 'ASC');
370
+
371
+
372
+            foreach ($entitys->skip($offset)->take($resultCount)->get() as $e) {
373
+              if(!$e->hasRole('superadmin') || ($e->hasRole('superadmin') && Auth::user()->hasRole('superadmin'))){
374
+                array_push($list, array('id' => $e->id, 'text' => $e->full_name));
375
+              }
376
+            }
377
+
378
+            $count = $entitys->count();
379
+            $endCount = $offset + $resultCount;
380
+            $morePages = $count > $endCount;
381
+
382
+            $results = array(
383
+              "results" => $list,
384
+              "pagination" => array(
385
+                "more" => $morePages
386
+                )
387
+              );
388
+            return response()->json($results);
389
+          }
390
+
391
+
392
+        }
393
+
394
+        public function admin_quick_store(Request $request)
395
+        {
396
+          if (!Auth::user()->can('create-user') && !Auth::user()->can('create-azienda')) {
397
+            abort(403);
398
+          }
399
+
400
+          $validated = $request->validate([
401
+            'is_azienda' => 'nullable|boolean',
402
+            'nome' => 'nullable|string|max:255',
403
+            'cognome' => 'nullable|string|max:255',
404
+            'ragione_sociale' => 'nullable|string|max:255',
405
+            'email' => 'nullable|email|max:255',
406
+            'telefono' => 'nullable|string|max:255',
407
+            'piva' => 'nullable|string|max:50',
408
+          ]);
409
+
410
+          $isAzienda = $request->boolean('is_azienda');
411
+          if ($isAzienda) {
412
+            if (blank($validated['ragione_sociale'] ?? null) && blank($validated['nome'] ?? null)) {
413
+              return response()->json([
414
+                'message' => 'Indicare almeno ragione sociale o nome azienda.',
415
+                'errors' => ['ragione_sociale' => ['Indicare almeno ragione sociale o nome azienda.']],
416
+              ], 422);
417
+            }
418
+          } else {
419
+            if (blank($validated['nome'] ?? null) && blank($validated['cognome'] ?? null)) {
420
+              return response()->json([
421
+                'message' => 'Indicare almeno nome o cognome.',
422
+                'errors' => ['nome' => ['Indicare almeno nome o cognome.']],
423
+              ], 422);
424
+            }
425
+          }
426
+
427
+          $user = new User();
428
+          $user->is_azienda = $isAzienda;
429
+          $user->is_gruppo = false;
430
+          $user->nome = $validated['nome'] ?? ($validated['ragione_sociale'] ?? null);
431
+          $user->cognome = $isAzienda ? null : ($validated['cognome'] ?? null);
432
+          $user->ragione_sociale = $validated['ragione_sociale'] ?? null;
433
+          $user->email = $validated['email'] ?? null;
434
+          $user->telefono = $validated['telefono'] ?? null;
435
+          $user->piva = $validated['piva'] ?? null;
436
+          $user->save();
437
+
438
+          $role = Role::where('name', 'user')->first();
439
+          if ($role) {
440
+            $user->syncRoles([$role->name]);
441
+          }
442
+
443
+          return response()->json([
444
+            'success' => true,
445
+            'message' => $isAzienda ? 'Azienda creata.' : 'Cliente creato.',
446
+            'user' => [
447
+              'id' => $user->id,
448
+              'text' => $user->full_name,
449
+              'is_azienda' => (bool) $user->is_azienda,
450
+            ],
451
+          ]);
452
+        }
453
+}

+ 25
- 0
app/Http/Middleware/LocaleMiddleware.php 파일 보기

@@ -0,0 +1,25 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Closure;
6
+use Illuminate\Http\Request;
7
+use Symfony\Component\HttpFoundation\Response;
8
+
9
+class LocaleMiddleware
10
+{
11
+  /**
12
+   * Handle an incoming request.
13
+   *
14
+   * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
15
+   */
16
+  public function handle(Request $request, Closure $next): Response
17
+  {
18
+    // Locale is enabled and allowed to be change
19
+    if (session()->has('locale') && in_array(session()->get('locale'), ['en', 'fr', 'ar', 'de'])) {
20
+      app()->setLocale(session()->get('locale'));
21
+    }
22
+
23
+    return $next($request);
24
+  }
25
+}

+ 43
- 0
app/Listeners/InviaNotificaSegnalazione.php 파일 보기

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Listeners;
4
+
5
+use App\Events\NuovaSegnalazione;
6
+use App\Models\ConfigNotifiche;
7
+use App\Models\User;
8
+use App\Notifications\NuovaSegnalazioneAdmin;
9
+use App\Notifications\NuovaSegnalazioneUser;
10
+use Illuminate\Support\Facades\Notification;
11
+
12
+class InviaNotificaSegnalazione
13
+{
14
+    /**
15
+     * Handle the event.
16
+     */
17
+    public function handle(NuovaSegnalazione $event): void
18
+    {
19
+        $segnalazione = $event->segnalazione->loadMissing(['user', 'assegnataA', 'azienda']);
20
+
21
+        $configNotifiche = ConfigNotifiche::where('event', ConfigNotifiche::NUOVA_SEGNALAZIONE_ADMIN)->first();
22
+        if ($configNotifiche != null && $configNotifiche->is_attivo == true && count($configNotifiche->utenti) > 0) {
23
+            Notification::send($configNotifiche->utenti, new NuovaSegnalazioneAdmin($segnalazione));
24
+        }
25
+
26
+        if ($segnalazione->assegnata_a_id != null) {
27
+            $configNotifiche = ConfigNotifiche::where('event', ConfigNotifiche::NUOVA_SEGNALAZIONE_ASSEGNATA)->first();
28
+            if ($configNotifiche != null && $configNotifiche->is_attivo == true) {
29
+                $targets = User::notifiableMembersForAssigneeId((int) $segnalazione->assegnata_a_id);
30
+                if ($targets->isNotEmpty()) {
31
+                    Notification::send($targets, new NuovaSegnalazioneUser($segnalazione, false));
32
+                }
33
+            }
34
+        }
35
+
36
+        if ($event->notificaApertaDa && $segnalazione->user_id != null) {
37
+            $targets = User::notifiableMembersForAssigneeId((int) $segnalazione->user_id);
38
+            if ($targets->isNotEmpty()) {
39
+                Notification::send($targets, new NuovaSegnalazioneUser($segnalazione, false, true));
40
+            }
41
+        }
42
+    }
43
+}

+ 54
- 0
app/Livewire/Profile/LogoutOtherBrowserSessionsForm.php 파일 보기

@@ -0,0 +1,54 @@
1
+<?php
2
+
3
+namespace App\Livewire\Profile;
4
+
5
+use Illuminate\Contracts\Auth\StatefulGuard;
6
+use Illuminate\Support\Facades\Auth;
7
+use Illuminate\Support\Facades\Hash;
8
+use Illuminate\Validation\ValidationException;
9
+use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm as JetstreamForm;
10
+
11
+class LogoutOtherBrowserSessionsForm extends JetstreamForm
12
+{
13
+    public $feedback = '';
14
+
15
+    public function confirmLogout(): void
16
+    {
17
+        $this->feedback = '';
18
+        parent::confirmLogout();
19
+    }
20
+
21
+    /**
22
+     * Jetstream esce subito se il driver non è "database", quindi non viene mai
23
+     * chiamato logoutOtherDevices e la UI sembra non fare nulla.
24
+     */
25
+    public function logoutOtherBrowserSessions(StatefulGuard $guard): void
26
+    {
27
+        $this->resetErrorBag();
28
+
29
+        if (! Hash::check($this->password, Auth::user()->getAuthPassword())) {
30
+            throw ValidationException::withMessages([
31
+                'password' => [__('This password does not match our records.')],
32
+            ]);
33
+        }
34
+
35
+        $guard->logoutOtherDevices($this->password);
36
+
37
+        if (config('session.driver') === 'database') {
38
+            $this->deleteOtherSessionRecords();
39
+        }
40
+
41
+        request()->session()->put([
42
+            'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
43
+        ]);
44
+
45
+        $this->confirmingLogout = false;
46
+        $this->password = '';
47
+
48
+        $this->feedback = config('session.driver') === 'database'
49
+            ? __('Le altre sessioni sono state chiuse.')
50
+            : __('Le altre sessioni sono state invalidate. Imposta SESSION_DRIVER=database per elencare le sessioni nel database.');
51
+
52
+        $this->dispatch('loggedOut');
53
+    }
54
+}

+ 108
- 0
app/Models/AbstractModels/AbstractAcquistoServizio.php 파일 보기

@@ -0,0 +1,108 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractAcquistoServizio extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'acquisto_servizio';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'prodotto_id' => 'integer',
35
+        'promo_code_id' => 'integer',
36
+        'segnalazione_id' => 'integer',
37
+        'nome' => 'string',
38
+        'email' => 'string',
39
+        'telefono' => 'string',
40
+        'modalita' => 'string',
41
+        'prezzo_servizio' => 'decimal:2',
42
+        'costo_domicilio' => 'decimal:2',
43
+        'sconto' => 'decimal:2',
44
+        'totale' => 'decimal:2',
45
+        'provider' => 'string',
46
+        'payment_status' => 'string',
47
+        'condizioni_accettate_at' => 'datetime',
48
+        'token' => 'string',
49
+        'meta' => 'array',
50
+        'codice_fiscale' => 'string',
51
+        'indirizzo' => 'string',
52
+        'note_cliente' => 'string',
53
+        'note_operatore' => 'string',
54
+        'created_at' => 'datetime',
55
+        'updated_at' => 'datetime'
56
+    ];
57
+    
58
+    /**  
59
+     * The attributes that are mass assignable.
60
+     * 
61
+     * @var array
62
+     */
63
+    protected $fillable = [
64
+        'id',
65
+        'prodotto_id',
66
+        'promo_code_id',
67
+        'segnalazione_id',
68
+        'nome',
69
+        'email',
70
+        'telefono',
71
+        'modalita',
72
+        'prezzo_servizio',
73
+        'costo_domicilio',
74
+        'sconto',
75
+        'totale',
76
+        'provider',
77
+        'payment_status',
78
+        'condizioni_accettate_at',
79
+        'token',
80
+        'meta',
81
+        'codice_fiscale',
82
+        'indirizzo',
83
+        'note_cliente',
84
+        'note_operatore',
85
+        'created_at',
86
+        'updated_at'
87
+    ];
88
+    
89
+    public function prodotto()
90
+    {
91
+        return $this->belongsTo('\App\Models\Prodotto', 'prodotto_id', 'id');
92
+    }
93
+    
94
+    public function promoCode()
95
+    {
96
+        return $this->belongsTo('\App\Models\PromoCode', 'promo_code_id', 'id');
97
+    }
98
+    
99
+    public function segnalazione()
100
+    {
101
+        return $this->belongsTo('\App\Models\Segnalazione', 'segnalazione_id', 'id');
102
+    }
103
+    
104
+    public function segnalazioni()
105
+    {
106
+        return $this->hasMany('\App\Models\Segnalazione', 'acquisto_servizio_id', 'id');
107
+    }
108
+}

+ 61
- 0
app/Models/AbstractModels/AbstractAllegatoAnnotazione.php 파일 보기

@@ -0,0 +1,61 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractAllegatoAnnotazione extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'allegato_annotazione';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'annotazione_id' => 'integer',
35
+        'nome' => 'string',
36
+        'descrizione' => 'string',
37
+        'path_file' => 'string',
38
+        'created_at' => 'datetime',
39
+        'updated_at' => 'datetime'
40
+    ];
41
+    
42
+    /**  
43
+     * The attributes that are mass assignable.
44
+     * 
45
+     * @var array
46
+     */
47
+    protected $fillable = [
48
+        'id',
49
+        'annotazione_id',
50
+        'nome',
51
+        'descrizione',
52
+        'path_file',
53
+        'created_at',
54
+        'updated_at'
55
+    ];
56
+    
57
+    public function annotazione()
58
+    {
59
+        return $this->belongsTo('\App\Models\Annotazione', 'annotazione_id', 'id');
60
+    }
61
+}

+ 78
- 0
app/Models/AbstractModels/AbstractAnagrafica.php 파일 보기

@@ -0,0 +1,78 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractAnagrafica extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'anagrafica';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'ragione_sociale' => 'string',
35
+        'nome' => 'string',
36
+        'cognome' => 'string',
37
+        'piva' => 'string',
38
+        'cod_fiscale' => 'string',
39
+        'indirizzo' => 'string',
40
+        'email' => 'string',
41
+        'telefono' => 'string',
42
+        'comune_id' => 'integer',
43
+        'note' => 'string',
44
+        'created_at' => 'datetime',
45
+        'updated_at' => 'datetime'
46
+    ];
47
+    
48
+    /**  
49
+     * The attributes that are mass assignable.
50
+     * 
51
+     * @var array
52
+     */
53
+    protected $fillable = [
54
+        'id',
55
+        'ragione_sociale',
56
+        'nome',
57
+        'cognome',
58
+        'piva',
59
+        'cod_fiscale',
60
+        'indirizzo',
61
+        'email',
62
+        'telefono',
63
+        'comune_id',
64
+        'note',
65
+        'created_at',
66
+        'updated_at'
67
+    ];
68
+    
69
+    public function comune()
70
+    {
71
+        return $this->belongsTo('\App\Models\Comune', 'comune_id', 'id');
72
+    }
73
+    
74
+    public function preventivi()
75
+    {
76
+        return $this->hasMany('\App\Models\Preventivo', 'anagrafica_id', 'id');
77
+    }
78
+}

+ 95
- 0
app/Models/AbstractModels/AbstractAnnotazione.php 파일 보기

@@ -0,0 +1,95 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractAnnotazione extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'annotazione';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = [
33
+        'tempo_minuti' => 0,
34
+        'riservata' => 1
35
+    ];
36
+    
37
+    /**  
38
+     * The attributes that should be cast to native types.
39
+     * 
40
+     * @var array
41
+     */
42
+    protected $casts = [
43
+        'id' => 'integer',
44
+        'segnalazione_id' => 'integer',
45
+        'user_id' => 'integer',
46
+        'text' => 'string',
47
+        'tempo_minuti' => 'integer',
48
+        'tempo_data' => 'datetime:d/m/Y H:i',
49
+        'costo' => 'decimal:2',
50
+        'costo_descrizione' => 'string',
51
+        'riservata' => 'boolean',
52
+        'riferimento' => 'string',
53
+        'email' => 'string',
54
+        'telefono' => 'string',
55
+        'created_at' => 'datetime',
56
+        'updated_at' => 'datetime'
57
+    ];
58
+    
59
+    /**  
60
+     * The attributes that are mass assignable.
61
+     * 
62
+     * @var array
63
+     */
64
+    protected $fillable = [
65
+        'id',
66
+        'segnalazione_id',
67
+        'user_id',
68
+        'text',
69
+        'tempo_minuti',
70
+        'tempo_data',
71
+        'costo',
72
+        'costo_descrizione',
73
+        'riservata',
74
+        'riferimento',
75
+        'email',
76
+        'telefono',
77
+        'created_at',
78
+        'updated_at'
79
+    ];
80
+    
81
+    public function segnalazione()
82
+    {
83
+        return $this->belongsTo('\App\Models\Segnalazione', 'segnalazione_id', 'id');
84
+    }
85
+    
86
+    public function user()
87
+    {
88
+        return $this->belongsTo('\App\Models\User', 'user_id', 'id');
89
+    }
90
+    
91
+    public function allegatiAnnotazione()
92
+    {
93
+        return $this->hasMany('\App\Models\AllegatoAnnotazione', 'annotazione_id', 'id');
94
+    }
95
+}

+ 71
- 0
app/Models/AbstractModels/AbstractComune.php 파일 보기

@@ -0,0 +1,71 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractComune extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'comune';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'provincia_id' => 'integer',
35
+        'nazione_id' => 'integer',
36
+        'nome' => 'string',
37
+        'codice_catastale' => 'string',
38
+        'created_at' => 'datetime',
39
+        'updated_at' => 'datetime'
40
+    ];
41
+    
42
+    /**  
43
+     * The attributes that are mass assignable.
44
+     * 
45
+     * @var array
46
+     */
47
+    protected $fillable = [
48
+        'id',
49
+        'provincia_id',
50
+        'nazione_id',
51
+        'nome',
52
+        'codice_catastale',
53
+        'created_at',
54
+        'updated_at'
55
+    ];
56
+    
57
+    public function provincia()
58
+    {
59
+        return $this->belongsTo('\App\Models\Provincia', 'provincia_id', 'id');
60
+    }
61
+    
62
+    public function nazione()
63
+    {
64
+        return $this->belongsTo('\App\Models\Nazione', 'nazione_id', 'id');
65
+    }
66
+    
67
+    public function users()
68
+    {
69
+        return $this->hasMany('\App\Models\User', 'comune_id', 'id');
70
+    }
71
+}

+ 72
- 0
app/Models/AbstractModels/AbstractConfig.php 파일 보기

@@ -0,0 +1,72 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractConfig extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'config';
19
+    
20
+    /**  
21
+     * Primary key name.
22
+     * 
23
+     * @var string
24
+     */
25
+    public $primaryKey = 'key';
26
+    
27
+    /**  
28
+     * Primary key type.
29
+     * 
30
+     * @var string
31
+     */
32
+    protected $keyType = 'string';
33
+    
34
+    /**  
35
+     * Primary key is non-autoincrementing.
36
+     * 
37
+     * @var bool
38
+     */
39
+    public $incrementing = false;
40
+    
41
+    /**  
42
+     * The attributes that should be cast to native types.
43
+     * 
44
+     * @var array
45
+     */
46
+    protected $casts = [
47
+        'key' => 'string',
48
+        'group' => 'string',
49
+        'display_name' => 'string',
50
+        'value' => 'string',
51
+        'order' => 'integer',
52
+        'type' => 'string',
53
+        'created_at' => 'datetime',
54
+        'updated_at' => 'datetime'
55
+    ];
56
+    
57
+    /**  
58
+     * The attributes that are mass assignable.
59
+     * 
60
+     * @var array
61
+     */
62
+    protected $fillable = [
63
+        'key',
64
+        'group',
65
+        'display_name',
66
+        'value',
67
+        'order',
68
+        'type',
69
+        'created_at',
70
+        'updated_at'
71
+    ];
72
+}

+ 61
- 0
app/Models/AbstractModels/AbstractConfigNotifiche.php 파일 보기

@@ -0,0 +1,61 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractConfigNotifiche extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'config_notifiche';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['is_attivo' => 1];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'event' => 'string',
42
+        'users' => 'array',
43
+        'is_attivo' => 'boolean',
44
+        'created_at' => 'datetime',
45
+        'updated_at' => 'datetime'
46
+    ];
47
+    
48
+    /**  
49
+     * The attributes that are mass assignable.
50
+     * 
51
+     * @var array
52
+     */
53
+    protected $fillable = [
54
+        'id',
55
+        'event',
56
+        'users',
57
+        'is_attivo',
58
+        'created_at',
59
+        'updated_at'
60
+    ];
61
+}

+ 68
- 0
app/Models/AbstractModels/AbstractConfiguraSegnalazione.php 파일 보기

@@ -0,0 +1,68 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractConfiguraSegnalazione extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'configura_segnalazione';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'label' => 'string',
35
+        'descrizione' => 'string',
36
+        'gruppo' => 'string',
37
+        'key' => 'string',
38
+        'value' => 'string',
39
+        'type' => 'string',
40
+        'order' => 'integer',
41
+        'icona' => 'string',
42
+        'colore' => 'string',
43
+        'opzioni' => 'array',
44
+        'created_at' => 'datetime',
45
+        'updated_at' => 'datetime'
46
+    ];
47
+    
48
+    /**  
49
+     * The attributes that are mass assignable.
50
+     * 
51
+     * @var array
52
+     */
53
+    protected $fillable = [
54
+        'id',
55
+        'label',
56
+        'descrizione',
57
+        'gruppo',
58
+        'key',
59
+        'value',
60
+        'type',
61
+        'order',
62
+        'icona',
63
+        'colore',
64
+        'opzioni',
65
+        'created_at',
66
+        'updated_at'
67
+    ];
68
+}

+ 105
- 0
app/Models/AbstractModels/AbstractContrattoServizio.php 파일 보기

@@ -0,0 +1,105 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoServizio extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_servizio';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = [
33
+        'collegabile_segnalazioni' => 0,
34
+        'attivo' => 1
35
+    ];
36
+    
37
+    /**  
38
+     * The attributes that should be cast to native types.
39
+     * 
40
+     * @var array
41
+     */
42
+    protected $casts = [
43
+        'id' => 'integer',
44
+        'user_id' => 'integer',
45
+        'contratto_template_id' => 'integer',
46
+        'nome' => 'string',
47
+        'costo' => 'decimal:2',
48
+        'periodo' => 'string',
49
+        'data_inizio' => 'datetime:Y-m-d',
50
+        'data_fine' => 'datetime:Y-m-d',
51
+        'collegabile_segnalazioni' => 'boolean',
52
+        'quantita_residua' => 'decimal:2',
53
+        'note' => 'string',
54
+        'attivo' => 'boolean',
55
+        'created_at' => 'datetime',
56
+        'updated_at' => 'datetime'
57
+    ];
58
+    
59
+    /**  
60
+     * The attributes that are mass assignable.
61
+     * 
62
+     * @var array
63
+     */
64
+    protected $fillable = [
65
+        'id',
66
+        'user_id',
67
+        'contratto_template_id',
68
+        'nome',
69
+        'costo',
70
+        'periodo',
71
+        'data_inizio',
72
+        'data_fine',
73
+        'collegabile_segnalazioni',
74
+        'quantita_residua',
75
+        'note',
76
+        'attivo',
77
+        'created_at',
78
+        'updated_at'
79
+    ];
80
+    
81
+    public function user()
82
+    {
83
+        return $this->belongsTo('\App\Models\User', 'user_id', 'id');
84
+    }
85
+    
86
+    public function contrattoTemplate()
87
+    {
88
+        return $this->belongsTo('\App\Models\ContrattoTemplate', 'contratto_template_id', 'id');
89
+    }
90
+    
91
+    public function segnalazioni()
92
+    {
93
+        return $this->hasMany('\App\Models\Segnalazione', 'contratto_servizio_id', 'id');
94
+    }
95
+    
96
+    public function contrattoServizioTodos()
97
+    {
98
+        return $this->hasMany('\App\Models\ContrattoServizioTodo', 'contratto_servizio_id', 'id');
99
+    }
100
+    
101
+    public function contrattoServizioRigas()
102
+    {
103
+        return $this->hasMany('\App\Models\ContrattoServizioRiga', 'contratto_servizio_id', 'id');
104
+    }
105
+}

+ 81
- 0
app/Models/AbstractModels/AbstractContrattoServizioRiga.php 파일 보기

@@ -0,0 +1,81 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoServizioRiga extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_servizio_riga';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['ordine' => 0];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'contratto_servizio_id' => 'integer',
42
+        'prodotto_id' => 'integer',
43
+        'quantita' => 'decimal:2',
44
+        'unita_misura' => 'string',
45
+        'prezzo_unitario' => 'decimal:2',
46
+        'periodo' => 'string',
47
+        'note' => 'string',
48
+        'ordine' => 'integer',
49
+        'created_at' => 'datetime',
50
+        'updated_at' => 'datetime'
51
+    ];
52
+    
53
+    /**  
54
+     * The attributes that are mass assignable.
55
+     * 
56
+     * @var array
57
+     */
58
+    protected $fillable = [
59
+        'id',
60
+        'contratto_servizio_id',
61
+        'prodotto_id',
62
+        'quantita',
63
+        'unita_misura',
64
+        'prezzo_unitario',
65
+        'periodo',
66
+        'note',
67
+        'ordine',
68
+        'created_at',
69
+        'updated_at'
70
+    ];
71
+    
72
+    public function prodotto()
73
+    {
74
+        return $this->belongsTo('\App\Models\Prodotto', 'prodotto_id', 'id');
75
+    }
76
+    
77
+    public function contrattoServizio()
78
+    {
79
+        return $this->belongsTo('\App\Models\ContrattoServizio', 'contratto_servizio_id', 'id');
80
+    }
81
+}

+ 105
- 0
app/Models/AbstractModels/AbstractContrattoServizioTodo.php 파일 보기

@@ -0,0 +1,105 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoServizioTodo extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_servizio_todo';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = [
33
+        'ordine' => 0,
34
+        'attivo' => 1
35
+    ];
36
+    
37
+    /**  
38
+     * The attributes that should be cast to native types.
39
+     * 
40
+     * @var array
41
+     */
42
+    protected $casts = [
43
+        'id' => 'integer',
44
+        'contratto_servizio_id' => 'integer',
45
+        'contratto_template_todo_id' => 'integer',
46
+        'titolo' => 'string',
47
+        'descrizione' => 'string',
48
+        'tipo_schedulazione' => 'string',
49
+        'cron' => 'string',
50
+        'mese' => 'integer',
51
+        'giorno' => 'integer',
52
+        'offset_giorni' => 'integer',
53
+        'data_prossima' => 'datetime:Y-m-d',
54
+        'data_ultima' => 'datetime:Y-m-d',
55
+        'priorita' => 'string',
56
+        'tipo_segnalazione' => 'string',
57
+        'ordine' => 'integer',
58
+        'attivo' => 'boolean',
59
+        'meta' => 'array',
60
+        'created_at' => 'datetime',
61
+        'updated_at' => 'datetime'
62
+    ];
63
+    
64
+    /**  
65
+     * The attributes that are mass assignable.
66
+     * 
67
+     * @var array
68
+     */
69
+    protected $fillable = [
70
+        'id',
71
+        'contratto_servizio_id',
72
+        'contratto_template_todo_id',
73
+        'titolo',
74
+        'descrizione',
75
+        'tipo_schedulazione',
76
+        'cron',
77
+        'mese',
78
+        'giorno',
79
+        'offset_giorni',
80
+        'data_prossima',
81
+        'data_ultima',
82
+        'priorita',
83
+        'tipo_segnalazione',
84
+        'ordine',
85
+        'attivo',
86
+        'meta',
87
+        'created_at',
88
+        'updated_at'
89
+    ];
90
+    
91
+    public function contrattoServizio()
92
+    {
93
+        return $this->belongsTo('\App\Models\ContrattoServizio', 'contratto_servizio_id', 'id');
94
+    }
95
+    
96
+    public function contrattoTemplateTodo()
97
+    {
98
+        return $this->belongsTo('\App\Models\ContrattoTemplateTodo', 'contratto_template_todo_id', 'id');
99
+    }
100
+    
101
+    public function segnalazioni()
102
+    {
103
+        return $this->hasMany('\App\Models\Segnalazione', 'contratto_servizio_todo_id', 'id');
104
+    }
105
+}

+ 84
- 0
app/Models/AbstractModels/AbstractContrattoTemplate.php 파일 보기

@@ -0,0 +1,84 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoTemplate extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_template';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['attivo' => 1];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'codice' => 'string',
42
+        'nome' => 'string',
43
+        'descrizione' => 'string',
44
+        'periodo' => 'string',
45
+        'attivo' => 'boolean',
46
+        'costo_base' => 'decimal:2',
47
+        'meta' => 'array',
48
+        'created_at' => 'datetime',
49
+        'updated_at' => 'datetime'
50
+    ];
51
+    
52
+    /**  
53
+     * The attributes that are mass assignable.
54
+     * 
55
+     * @var array
56
+     */
57
+    protected $fillable = [
58
+        'id',
59
+        'codice',
60
+        'nome',
61
+        'descrizione',
62
+        'periodo',
63
+        'attivo',
64
+        'costo_base',
65
+        'meta',
66
+        'created_at',
67
+        'updated_at'
68
+    ];
69
+    
70
+    public function contrattiServizio()
71
+    {
72
+        return $this->hasMany('\App\Models\ContrattoServizio', 'contratto_template_id', 'id');
73
+    }
74
+    
75
+    public function contrattoTemplateTodos()
76
+    {
77
+        return $this->hasMany('\App\Models\ContrattoTemplateTodo', 'contratto_template_id', 'id');
78
+    }
79
+    
80
+    public function contrattoTemplateRigas()
81
+    {
82
+        return $this->hasMany('\App\Models\ContrattoTemplateRiga', 'contratto_template_id', 'id');
83
+    }
84
+}

+ 81
- 0
app/Models/AbstractModels/AbstractContrattoTemplateRiga.php 파일 보기

@@ -0,0 +1,81 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoTemplateRiga extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_template_riga';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['ordine' => 0];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'contratto_template_id' => 'integer',
42
+        'prodotto_id' => 'integer',
43
+        'quantita' => 'decimal:2',
44
+        'unita_misura' => 'string',
45
+        'prezzo_unitario' => 'decimal:2',
46
+        'periodo' => 'string',
47
+        'note' => 'string',
48
+        'ordine' => 'integer',
49
+        'created_at' => 'datetime',
50
+        'updated_at' => 'datetime'
51
+    ];
52
+    
53
+    /**  
54
+     * The attributes that are mass assignable.
55
+     * 
56
+     * @var array
57
+     */
58
+    protected $fillable = [
59
+        'id',
60
+        'contratto_template_id',
61
+        'prodotto_id',
62
+        'quantita',
63
+        'unita_misura',
64
+        'prezzo_unitario',
65
+        'periodo',
66
+        'note',
67
+        'ordine',
68
+        'created_at',
69
+        'updated_at'
70
+    ];
71
+    
72
+    public function prodotto()
73
+    {
74
+        return $this->belongsTo('\App\Models\Prodotto', 'prodotto_id', 'id');
75
+    }
76
+    
77
+    public function contrattoTemplate()
78
+    {
79
+        return $this->belongsTo('\App\Models\ContrattoTemplate', 'contratto_template_id', 'id');
80
+    }
81
+}

+ 94
- 0
app/Models/AbstractModels/AbstractContrattoTemplateTodo.php 파일 보기

@@ -0,0 +1,94 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractContrattoTemplateTodo extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'contratto_template_todo';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = [
33
+        'ordine' => 0,
34
+        'attivo' => 1
35
+    ];
36
+    
37
+    /**  
38
+     * The attributes that should be cast to native types.
39
+     * 
40
+     * @var array
41
+     */
42
+    protected $casts = [
43
+        'id' => 'integer',
44
+        'contratto_template_id' => 'integer',
45
+        'titolo' => 'string',
46
+        'descrizione' => 'string',
47
+        'tipo_schedulazione' => 'string',
48
+        'cron' => 'string',
49
+        'mese' => 'integer',
50
+        'giorno' => 'integer',
51
+        'offset_giorni' => 'integer',
52
+        'priorita' => 'string',
53
+        'tipo_segnalazione' => 'string',
54
+        'ordine' => 'integer',
55
+        'attivo' => 'boolean',
56
+        'meta' => 'array',
57
+        'created_at' => 'datetime',
58
+        'updated_at' => 'datetime'
59
+    ];
60
+    
61
+    /**  
62
+     * The attributes that are mass assignable.
63
+     * 
64
+     * @var array
65
+     */
66
+    protected $fillable = [
67
+        'id',
68
+        'contratto_template_id',
69
+        'titolo',
70
+        'descrizione',
71
+        'tipo_schedulazione',
72
+        'cron',
73
+        'mese',
74
+        'giorno',
75
+        'offset_giorni',
76
+        'priorita',
77
+        'tipo_segnalazione',
78
+        'ordine',
79
+        'attivo',
80
+        'meta',
81
+        'created_at',
82
+        'updated_at'
83
+    ];
84
+    
85
+    public function contrattoTemplate()
86
+    {
87
+        return $this->belongsTo('\App\Models\ContrattoTemplate', 'contratto_template_id', 'id');
88
+    }
89
+    
90
+    public function contrattoServizioTodos()
91
+    {
92
+        return $this->hasMany('\App\Models\ContrattoServizioTodo', 'contratto_template_todo_id', 'id');
93
+    }
94
+}

+ 62
- 0
app/Models/AbstractModels/AbstractGruppo.php 파일 보기

@@ -0,0 +1,62 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractGruppo extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'gruppo';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'label' => 'string',
35
+        'is_attivo' => 'boolean',
36
+        'created_at' => 'datetime',
37
+        'updated_at' => 'datetime'
38
+    ];
39
+    
40
+    /**  
41
+     * The attributes that are mass assignable.
42
+     * 
43
+     * @var array
44
+     */
45
+    protected $fillable = [
46
+        'id',
47
+        'label',
48
+        'is_attivo',
49
+        'created_at',
50
+        'updated_at'
51
+    ];
52
+    
53
+    public function assegnazioni()
54
+    {
55
+        return $this->hasMany('\App\Models\Assegnazione', 'gruppo_id', 'id');
56
+    }
57
+    
58
+    public function users()
59
+    {
60
+        return $this->belongsToMany('\App\Models\User', 'user_has_gruppo', 'gruppo_id', 'user_id');
61
+    }
62
+}

+ 73
- 0
app/Models/AbstractModels/AbstractLuogo.php 파일 보기

@@ -0,0 +1,73 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractLuogo extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'luogo';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['is_attivo' => 1];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'reparto_id' => 'integer',
42
+        'label' => 'string',
43
+        'descrizione' => 'string',
44
+        'is_attivo' => 'boolean',
45
+        'created_at' => 'datetime',
46
+        'updated_at' => 'datetime'
47
+    ];
48
+    
49
+    /**  
50
+     * The attributes that are mass assignable.
51
+     * 
52
+     * @var array
53
+     */
54
+    protected $fillable = [
55
+        'id',
56
+        'reparto_id',
57
+        'label',
58
+        'descrizione',
59
+        'is_attivo',
60
+        'created_at',
61
+        'updated_at'
62
+    ];
63
+    
64
+    public function reparto()
65
+    {
66
+        return $this->belongsTo('\App\Models\Reparto', 'reparto_id', 'id');
67
+    }
68
+    
69
+    public function segnalazioni()
70
+    {
71
+        return $this->hasMany('\App\Models\Segnalazione', 'luogo_id', 'id');
72
+    }
73
+}

+ 62
- 0
app/Models/AbstractModels/AbstractNazione.php 파일 보기

@@ -0,0 +1,62 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractNazione extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'nazione';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'nome' => 'string',
35
+        'sigla' => 'string',
36
+        'created_at' => 'datetime',
37
+        'updated_at' => 'datetime'
38
+    ];
39
+    
40
+    /**  
41
+     * The attributes that are mass assignable.
42
+     * 
43
+     * @var array
44
+     */
45
+    protected $fillable = [
46
+        'id',
47
+        'nome',
48
+        'sigla',
49
+        'created_at',
50
+        'updated_at'
51
+    ];
52
+    
53
+    public function provincie()
54
+    {
55
+        return $this->hasMany('\App\Models\Provincia', 'nazione_id', 'id');
56
+    }
57
+    
58
+    public function comuni()
59
+    {
60
+        return $this->hasMany('\App\Models\Comune', 'nazione_id', 'id');
61
+    }
62
+}

+ 83
- 0
app/Models/AbstractModels/AbstractOfferta.php 파일 보기

@@ -0,0 +1,83 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractOfferta extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'offerta';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The attributes that should be cast to native types.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $casts = [
33
+        'id' => 'integer',
34
+        'user_id' => 'integer',
35
+        'numero' => 'integer',
36
+        'titolo' => 'string',
37
+        'stato' => 'string',
38
+        'data' => 'datetime:Y-m-d',
39
+        'data_validita' => 'datetime:Y-m-d',
40
+        'totale' => 'decimal:2',
41
+        'note' => 'string',
42
+        'path_file' => 'string',
43
+        'meta' => 'array',
44
+        'created_at' => 'datetime',
45
+        'updated_at' => 'datetime'
46
+    ];
47
+    
48
+    /**  
49
+     * The attributes that are mass assignable.
50
+     * 
51
+     * @var array
52
+     */
53
+    protected $fillable = [
54
+        'id',
55
+        'user_id',
56
+        'numero',
57
+        'titolo',
58
+        'stato',
59
+        'data',
60
+        'data_validita',
61
+        'totale',
62
+        'note',
63
+        'path_file',
64
+        'meta',
65
+        'created_at',
66
+        'updated_at'
67
+    ];
68
+    
69
+    public function user()
70
+    {
71
+        return $this->belongsTo('\App\Models\User', 'user_id', 'id');
72
+    }
73
+    
74
+    public function offerteRiga()
75
+    {
76
+        return $this->hasMany('\App\Models\OffertaRiga', 'offerta_id', 'id');
77
+    }
78
+    
79
+    public function ordiniLavoro()
80
+    {
81
+        return $this->belongsToMany('\App\Models\OrdineLavoro', 'ordine_lavoro_has_offerta', 'offerta_id', 'ordine_lavoro_id');
82
+    }
83
+}

+ 85
- 0
app/Models/AbstractModels/AbstractOffertaRiga.php 파일 보기

@@ -0,0 +1,85 @@
1
+<?php
2
+/**
3
+ * Model object generated by: Skipper (http://www.skipper18.com)
4
+ * Do not modify this file manually.
5
+ */
6
+
7
+namespace App\Models\AbstractModels;
8
+
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+abstract class AbstractOffertaRiga extends Model
12
+{
13
+    /**  
14
+     * The table associated with the model.
15
+     * 
16
+     * @var string
17
+     */
18
+    protected $table = 'offerta_riga';
19
+    
20
+    /**  
21
+     * Primary key type.
22
+     * 
23
+     * @var string
24
+     */
25
+    protected $keyType = 'bigInteger';
26
+    
27
+    /**  
28
+     * The model's default values for attributes.
29
+     * 
30
+     * @var array
31
+     */
32
+    protected $attributes = ['ordine' => 0];
33
+    
34
+    /**  
35
+     * The attributes that should be cast to native types.
36
+     * 
37
+     * @var array
38
+     */
39
+    protected $casts = [
40
+        'id' => 'integer',
41
+        'offerta_id' => 'integer',
42
+        'prodotto_id' => 'integer',
43
+        'descrizione' => 'string',
44
+        'quantita' => 'decimal:2',
45
+        'unita_misura' => 'string',
46
+        'prezzo_unitario' => 'decimal:2',
47
+        'sconto' => 'decimal:2',
48
+        'ordine' => 'integer',
49
+        'note_private' => 'string',
50
+        'tempo_stimato' => 'decimal:2',
51
+        'created_at' => 'datetime',
52
+        'updated_at' => 'datetime'
53
+    ];
54
+    
55
+    /**  
56
+     * The attributes that are mass assignable.
57
+     * 
58
+     * @var array
59
+     */
60
+    protected $fillable = [
61
+        'id',
62
+        'offerta_id',
63
+        'prodotto_id',
64
+        'descrizione',
65
+        'quantita',
66
+        'unita_misura',
67
+        'prezzo_unitario',
68
+        'sconto',
69
+        'ordine',
70
+        'note_private',
71
+        'tempo_stimato',
72
+        'created_at',
73
+        'updated_at'
74
+    ];
75
+    
76
+    public function offerta()
77
+    {
78
+        return $this->belongsTo('\App\Models\Offerta', 'offerta_id', 'id');
79
+    }
80
+    
81
+    public function prodotto()
82
+    {
83
+        return $this->belongsTo('\App\Models\Prodotto', 'prodotto_id', 'id');
84
+    }
85
+}

+ 0
- 0
app/Models/AbstractModels/AbstractOrdineLavoro.php 파일 보기


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.

Loading…
취소
저장